modal 1.4.3.dev19__tar.gz → 1.4.3.dev21__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.3.dev19 → modal-1.4.3.dev21}/PKG-INFO +1 -1
  2. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/rich.py +4 -4
  3. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/deprecation.py +5 -8
  4. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/docker_utils.py +10 -1
  5. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/task_command_router_client.py +14 -5
  6. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/cluster.py +4 -1
  7. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/container.py +3 -1
  8. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/environment.py +2 -1
  9. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/client.pyi +2 -2
  10. modal-1.4.3.dev21/modal/container_process.py +470 -0
  11. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/container_process.pyi +120 -11
  12. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/exception.py +0 -5
  13. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_io.py +0 -3
  14. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/functions.pyi +6 -6
  15. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/image.py +29 -1
  16. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/image.pyi +54 -0
  17. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/io_streams.py +199 -69
  18. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/io_streams.pyi +64 -125
  19. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox.py +93 -50
  20. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox.pyi +85 -27
  21. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/PKG-INFO +1 -1
  22. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2.py +600 -600
  23. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2.pyi +4 -1
  24. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_version/__init__.py +1 -1
  25. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/pyproject.toml +5 -0
  26. modal-1.4.3.dev19/modal/container_process.py +0 -224
  27. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/LICENSE +0 -0
  28. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/README.md +0 -0
  29. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/__init__.py +0 -0
  30. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/__main__.py +0 -0
  31. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_billing.py +0 -0
  32. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_clustered_functions.py +0 -0
  33. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_clustered_functions.pyi +0 -0
  34. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_container_entrypoint.py +0 -0
  35. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_environments.py +0 -0
  36. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_functions.py +0 -0
  37. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_grpc_client.py +0 -0
  38. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_ipython.py +0 -0
  39. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_load_context.py +0 -0
  40. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_location.py +0 -0
  41. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_logs.py +0 -0
  42. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_object.py +0 -0
  43. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/__init__.py +0 -0
  44. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/manager.py +0 -0
  45. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/pty.py +0 -0
  46. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_output/status.py +0 -0
  47. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_partial_function.py +0 -0
  48. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_resolver.py +0 -0
  49. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_resources.py +0 -0
  50. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/__init__.py +0 -0
  51. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/asgi.py +0 -0
  52. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/container_io_manager.py +0 -0
  53. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/container_io_manager.pyi +0 -0
  54. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/execution_context.py +0 -0
  55. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/execution_context.pyi +0 -0
  56. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  57. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/telemetry.py +0 -0
  58. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/user_code_event_loop.py +0 -0
  59. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_runtime/user_code_imports.py +0 -0
  60. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_serialization.py +0 -0
  61. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_server.py +0 -0
  62. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_traceback.py +0 -0
  63. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_tunnel.py +0 -0
  64. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_tunnel.pyi +0 -0
  65. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_type_manager.py +0 -0
  66. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/__init__.py +0 -0
  67. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/app_utils.py +0 -0
  68. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/async_utils.py +0 -0
  69. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/auth_token_manager.py +0 -0
  70. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/blob_utils.py +0 -0
  71. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/browser_utils.py +0 -0
  72. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/bytes_io_segment_payload.py +0 -0
  73. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/function_utils.py +0 -0
  74. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/git_utils.py +0 -0
  75. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/grpc_testing.py +0 -0
  76. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/grpc_utils.py +0 -0
  77. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/hash_utils.py +0 -0
  78. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/http_utils.py +0 -0
  79. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/jwt_utils.py +0 -0
  80. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/logger.py +0 -0
  81. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/mount_utils.py +0 -0
  82. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/name_utils.py +0 -0
  83. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/package_utils.py +0 -0
  84. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/pattern_utils.py +0 -0
  85. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/rand_pb_testing.py +0 -0
  86. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/sandbox_fs_utils.py +0 -0
  87. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/shell_utils.py +0 -0
  88. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_utils/time_utils.py +0 -0
  89. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/__init__.py +0 -0
  90. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  91. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/cloudpickle.py +0 -0
  92. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/tblib.py +0 -0
  93. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_vendor/version.py +0 -0
  94. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/_watcher.py +0 -0
  95. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/app.py +0 -0
  96. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/app.pyi +0 -0
  97. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/billing.py +0 -0
  98. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2023.12.312.txt +0 -0
  99. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2023.12.txt +0 -0
  100. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2024.04.txt +0 -0
  101. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2024.10.txt +0 -0
  102. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/2025.06.txt +0 -0
  103. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/PREVIEW.txt +0 -0
  104. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/README.md +0 -0
  105. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/builder/base-images.json +0 -0
  106. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/call_graph.py +0 -0
  107. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/__init__.py +0 -0
  108. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_download.py +0 -0
  109. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_help.py +0 -0
  110. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/_traceback.py +0 -0
  111. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/app.py +0 -0
  112. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/billing.py +0 -0
  113. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/bootstrap.py +0 -0
  114. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/changelog.py +0 -0
  115. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/config.py +0 -0
  116. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/dashboard.py +0 -0
  117. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/dict.py +0 -0
  118. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/entry_point.py +0 -0
  119. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/import_refs.py +0 -0
  120. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/launch.py +0 -0
  121. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/logo.py +0 -0
  122. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/network_file_system.py +0 -0
  123. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/profile.py +0 -0
  124. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/__init__.py +0 -0
  125. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/run_jupyter.py +0 -0
  126. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/programs/vscode.py +0 -0
  127. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/queues.py +0 -0
  128. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/run.py +0 -0
  129. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/secret.py +0 -0
  130. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/selector.py +0 -0
  131. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/shell.py +0 -0
  132. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/token.py +0 -0
  133. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/utils.py +0 -0
  134. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cli/volume.py +0 -0
  135. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/client.py +0 -0
  136. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cloud_bucket_mount.py +0 -0
  137. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cloud_bucket_mount.pyi +0 -0
  138. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cls.py +0 -0
  139. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/cls.pyi +0 -0
  140. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/config.py +0 -0
  141. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/dict.py +0 -0
  142. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/dict.pyi +0 -0
  143. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/environments.py +0 -0
  144. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/environments.pyi +0 -0
  145. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/__init__.py +0 -0
  146. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/flash.py +0 -0
  147. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/flash.pyi +0 -0
  148. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/experimental/ipython.py +0 -0
  149. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_io.pyi +0 -0
  150. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/file_pattern_matcher.py +0 -0
  151. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/functions.py +0 -0
  152. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/mount.py +0 -0
  153. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/mount.pyi +0 -0
  154. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/network_file_system.py +0 -0
  155. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/network_file_system.pyi +0 -0
  156. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/object.py +0 -0
  157. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/object.pyi +0 -0
  158. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/output.py +0 -0
  159. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/parallel_map.py +0 -0
  160. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/parallel_map.pyi +0 -0
  161. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/partial_function.py +0 -0
  162. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/partial_function.pyi +0 -0
  163. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/proxy.py +0 -0
  164. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/proxy.pyi +0 -0
  165. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/py.typed +0 -0
  166. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/queue.py +0 -0
  167. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/queue.pyi +0 -0
  168. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/retries.py +0 -0
  169. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/runner.py +0 -0
  170. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/runner.pyi +0 -0
  171. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/running_app.py +0 -0
  172. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox_fs.py +0 -0
  173. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/sandbox_fs.pyi +0 -0
  174. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/schedule.py +0 -0
  175. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/scheduler_placement.py +0 -0
  176. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/secret.py +0 -0
  177. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/secret.pyi +0 -0
  178. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/server.py +0 -0
  179. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/server.pyi +0 -0
  180. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/serving.py +0 -0
  181. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/serving.pyi +0 -0
  182. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/snapshot.py +0 -0
  183. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/snapshot.pyi +0 -0
  184. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/stream_type.py +0 -0
  185. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/token_flow.py +0 -0
  186. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/token_flow.pyi +0 -0
  187. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/volume.py +0 -0
  188. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal/volume.pyi +0 -0
  189. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/SOURCES.txt +0 -0
  190. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/dependency_links.txt +0 -0
  191. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/entry_points.txt +0 -0
  192. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/requires.txt +0 -0
  193. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal.egg-info/top_level.txt +0 -0
  194. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/__init__.py +0 -0
  195. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_cli_docs.py +0 -0
  196. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_cli_docs_main.py +0 -0
  197. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_reference_docs.py +0 -0
  198. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/gen_reference_docs_main.py +0 -0
  199. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/__init__.py +0 -0
  200. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/mdmd.py +0 -0
  201. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_docs/mdmd/signatures.py +0 -0
  202. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/__init__.py +0 -0
  203. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_grpc.py +0 -0
  204. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2_grpc.py +0 -0
  205. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/api_pb2_grpc.pyi +0 -0
  206. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/modal_api_grpc.py +0 -0
  207. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/py.typed +0 -0
  208. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_grpc.py +0 -0
  209. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2.py +0 -0
  210. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2.pyi +0 -0
  211. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  212. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  213. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/modal_version/__main__.py +0 -0
  214. {modal-1.4.3.dev19 → modal-1.4.3.dev21}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.3.dev19
3
+ Version: 1.4.3.dev21
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -49,7 +49,7 @@ from modal._output.manager import (
49
49
  )
50
50
  from modal._utils.time_utils import timestamp_to_localized_str
51
51
  from modal.config import logger
52
- from modal.exception import DeprecationError, PendingDeprecationError, ServerWarning
52
+ from modal.exception import DeprecationError, ServerWarning
53
53
  from modal_proto import api_pb2
54
54
 
55
55
  if platform.system() == "Windows":
@@ -359,14 +359,14 @@ class RichOutputManager(OutputManager):
359
359
  ) -> None:
360
360
  """Display a warning, using rich formatting for Modal-specific warnings.
361
361
 
362
- Modal warnings (DeprecationError, PendingDeprecationError, ServerWarning) are shown
362
+ Modal warnings (DeprecationError, ServerWarning) are shown
363
363
  in a yellow-bordered panel with source context. Other warnings fall back to the
364
364
  default Python warning display.
365
365
  """
366
366
  # For non-Modal warnings, fall back to the default display
367
367
  import modal
368
368
 
369
- is_modal_warning = issubclass(category, (DeprecationError, PendingDeprecationError, ServerWarning))
369
+ is_modal_warning = issubclass(category, (DeprecationError, ServerWarning))
370
370
  filename_in_modal = modal.__path__ and Path(filename).is_relative_to(modal.__path__[0])
371
371
  if not is_modal_warning and not filename_in_modal:
372
372
  base_showwarning(warning, category, filename, lineno, file=None, line=None)
@@ -392,7 +392,7 @@ class RichOutputManager(OutputManager):
392
392
  pass
393
393
 
394
394
  # Build title
395
- if issubclass(category, (DeprecationError, PendingDeprecationError)):
395
+ if issubclass(category, DeprecationError):
396
396
  title = "Modal Deprecation Warning"
397
397
  else:
398
398
  title = "Modal Warning"
@@ -7,7 +7,7 @@ from typing import Any, Callable, TypeVar
7
7
 
8
8
  from typing_extensions import ParamSpec # Needed for Python 3.9
9
9
 
10
- from ..exception import DeprecationError, PendingDeprecationError
10
+ from ..exception import DeprecationError
11
11
 
12
12
  _INTERNAL_MODULES = ["modal", "synchronicity"]
13
13
 
@@ -21,12 +21,11 @@ def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
21
21
  raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
22
22
 
23
23
 
24
- def deprecation_warning(
25
- deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
26
- ) -> None:
24
+ def deprecation_warning(deprecated_on: tuple[int, int, int], msg: str, *, show_source: bool = True) -> None:
27
25
  """Issue a Modal deprecation warning with source optionally attributed to user code.
28
26
 
29
- See the implementation of the built-in [warnings.warn](https://docs.python.org/3/library/warnings.html#available-functions).
27
+ See the implementation of the built-in warnings.warn:
28
+ https://docs.python.org/3/library/warnings.html#available-functions.
30
29
  """
31
30
  filename, lineno = "<unknown>", 0
32
31
  if show_source:
@@ -42,10 +41,8 @@ def deprecation_warning(
42
41
  # Use the defaults from above
43
42
  pass
44
43
 
45
- warning_cls = PendingDeprecationError if pending else DeprecationError
46
-
47
44
  # This is a lower-level function that warnings.warn uses
48
- warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", warning_cls, filename, lineno)
45
+ warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", DeprecationError, filename, lineno)
49
46
 
50
47
 
51
48
  P = ParamSpec("P")
@@ -40,10 +40,19 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
40
40
 
41
41
  # COPY --from=... commands reference external sources and do not need a context mount.
42
42
  # https://docs.docker.com/reference/dockerfile/#copy---from
43
- if parts[0].startswith("--from="):
43
+ if any(p.startswith("--from=") for p in parts):
44
44
  current_command = ""
45
45
  continue
46
46
 
47
+ # Strip known COPY flags (--chmod, --chown, --link) before processing sources.
48
+ known_flag_prefixes = ("--chmod=", "--chown=", "--link=")
49
+ parts = [
50
+ p
51
+ for p in parts
52
+ # link has a special handling - it can be "--link" or like "--link=true"
53
+ if p != "--link" and not any(p.startswith(prefix) for prefix in known_flag_prefixes)
54
+ ]
55
+
47
56
  if len(parts) >= 2:
48
57
  # Last part is destination, everything else is a mount source
49
58
  sources = parts[:-1]
@@ -16,7 +16,7 @@ from grpclib import GRPCError, Status
16
16
  from grpclib.exceptions import StreamTerminatedError
17
17
 
18
18
  from modal.config import logger
19
- from modal.exception import ExecTimeoutError
19
+ from modal.exception import ConflictError, ExecTimeoutError
20
20
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
21
21
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
22
22
 
@@ -196,12 +196,21 @@ class TaskCommandRouterClient:
196
196
  return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
197
197
 
198
198
  @classmethod
199
- async def init(
199
+ async def try_init(
200
200
  cls,
201
201
  server_client,
202
202
  task_id: str,
203
- ) -> "TaskCommandRouterClient":
204
- resp = await fetch_command_router_access(server_client, task_id)
203
+ ) -> Optional["TaskCommandRouterClient"]:
204
+ """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
205
+
206
+ Returns None if command router access is not enabled (FAILED_PRECONDITION).
207
+ """
208
+ try:
209
+ resp = await fetch_command_router_access(server_client, task_id)
210
+ except ConflictError:
211
+ logger.debug(f"Command router access is not enabled for task {task_id}")
212
+ return None
213
+
205
214
  logger.debug(f"Using command router access for task {task_id}")
206
215
  return await cls._connect(server_client, task_id, resp.url, resp.jwt)
207
216
 
@@ -232,7 +241,7 @@ class TaskCommandRouterClient:
232
241
  stream_stdio_retry_delay_factor: float = 2,
233
242
  stream_stdio_max_retries: int = 10,
234
243
  ) -> None:
235
- """Callers should not use this directly. Use TaskCommandRouterClient.init() instead."""
244
+ """Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
236
245
  # Record the loop this instance is bound to so __del__ can safely schedule cleanup
237
246
  # even if finalization happens from a different thread (e.g. via synchronicity).
238
247
  self._loop = loop
@@ -16,6 +16,7 @@ from modal.cli.utils import display_table, env_option, is_tty
16
16
  from modal.client import _Client
17
17
  from modal.config import config
18
18
  from modal.container_process import _ContainerProcess
19
+ from modal.exception import InvalidError
19
20
  from modal.output import OutputManager
20
21
  from modal.stream_type import StreamType
21
22
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
@@ -78,7 +79,9 @@ async def shell(cluster_id: str, rank: int = 0):
78
79
 
79
80
  pty = is_tty()
80
81
 
81
- command_router_client = await TaskCommandRouterClient.init(client, task_id)
82
+ command_router_client = await TaskCommandRouterClient.try_init(client, task_id)
83
+ if command_router_client is None:
84
+ raise InvalidError(f"Command router access is not available for container {task_id}")
82
85
 
83
86
  process_id = str(uuid.uuid4())
84
87
 
@@ -278,7 +278,9 @@ async def _exec_impl(
278
278
 
279
279
  client = await _Client.from_env()
280
280
 
281
- command_router_client = await TaskCommandRouterClient.init(client, container_id)
281
+ command_router_client = await TaskCommandRouterClient.try_init(client, container_id)
282
+ if command_router_client is None:
283
+ raise InvalidError(f"Command router access is not available for container {container_id}")
282
284
 
283
285
  process_id = str(uuid.uuid4())
284
286
 
@@ -67,7 +67,8 @@ def list_(json: bool = False):
67
67
  def create(name: str, restricted: bool = False):
68
68
  check_environment_name(name)
69
69
  Environment.objects.create(name, restricted=restricted)
70
- rich.print(f"[green]✓[/green] Environment created: {name}")
70
+ prefix = "Restricted " if restricted else ""
71
+ rich.print(f"[green]✓[/green] {prefix}Environment created: {name}")
71
72
 
72
73
 
73
74
  ENVIRONMENT_DELETE_HELP = """Delete an environment in the current workspace.
@@ -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.3.dev19",
38
+ version: str = "1.4.3.dev21",
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.3.dev19",
178
+ version: str = "1.4.3.dev21",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  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.shell_utils import stream_from_stdin, write_to_fd
11
+ from ._utils.task_command_router_client import TaskCommandRouterClient
12
+ from .client import _Client
13
+ from .config import logger
14
+ from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
15
+ from .io_streams import _StreamReader, _StreamWriter
16
+ from .stream_type import StreamType
17
+
18
+ T = TypeVar("T", str, bytes)
19
+
20
+
21
+ class _ContainerProcessThroughServer(Generic[T]):
22
+ _process_id: Optional[str] = None
23
+ _stdout: _StreamReader[T]
24
+ _stderr: _StreamReader[T]
25
+ _stdin: _StreamWriter
26
+ _exec_deadline: Optional[float] = None
27
+ _text: bool
28
+ _by_line: bool
29
+ _returncode: Optional[int] = None
30
+
31
+ def __init__(
32
+ self,
33
+ process_id: str,
34
+ task_id: str,
35
+ client: _Client,
36
+ stdout: StreamType = StreamType.PIPE,
37
+ stderr: StreamType = StreamType.PIPE,
38
+ exec_deadline: Optional[float] = None,
39
+ text: bool = True,
40
+ by_line: bool = False,
41
+ ) -> None:
42
+ self._process_id = process_id
43
+ self._client = client
44
+ self._exec_deadline = exec_deadline
45
+ self._text = text
46
+ self._by_line = by_line
47
+ self._stdout = _StreamReader[T](
48
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
49
+ process_id,
50
+ "container_process",
51
+ self._client,
52
+ stream_type=stdout,
53
+ text=text,
54
+ by_line=by_line,
55
+ deadline=exec_deadline,
56
+ task_id=task_id,
57
+ )
58
+ self._stderr = _StreamReader[T](
59
+ api_pb2.FILE_DESCRIPTOR_STDERR,
60
+ process_id,
61
+ "container_process",
62
+ self._client,
63
+ stream_type=stderr,
64
+ text=text,
65
+ by_line=by_line,
66
+ deadline=exec_deadline,
67
+ task_id=task_id,
68
+ )
69
+ self._stdin = _StreamWriter(process_id, "container_process", self._client)
70
+
71
+ def __repr__(self) -> str:
72
+ return f"ContainerProcess(process_id={self._process_id!r})"
73
+
74
+ @property
75
+ def stdout(self) -> _StreamReader[T]:
76
+ """StreamReader for the container process's stdout stream."""
77
+ return self._stdout
78
+
79
+ @property
80
+ def stderr(self) -> _StreamReader[T]:
81
+ """StreamReader for the container process's stderr stream."""
82
+ return self._stderr
83
+
84
+ @property
85
+ def stdin(self) -> _StreamWriter:
86
+ """StreamWriter for the container process's stdin stream."""
87
+ return self._stdin
88
+
89
+ @property
90
+ def returncode(self) -> int:
91
+ if self._returncode is None:
92
+ raise InvalidError(
93
+ "You must call wait() before accessing the returncode. "
94
+ "To poll for the status of a running process, use poll() instead."
95
+ )
96
+ return self._returncode
97
+
98
+ async def poll(self) -> Optional[int]:
99
+ """Check if the container process has finished running.
100
+
101
+ Returns `None` if the process is still running, else returns the exit code.
102
+ """
103
+ assert self._process_id
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 = await 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
+ assert self._process_id
123
+ while True:
124
+ req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
125
+ resp = await self._client.stub.ContainerExecWait(req)
126
+ if resp.completed:
127
+ return resp.exit_code
128
+
129
+ async def wait(self) -> int:
130
+ """Wait for the container process to finish running. Returns the exit code."""
131
+ if self._returncode is not None:
132
+ return self._returncode
133
+
134
+ try:
135
+ timeout = None
136
+ if self._exec_deadline:
137
+ timeout = self._exec_deadline - time.monotonic()
138
+ if timeout <= 0:
139
+ raise TimeoutError()
140
+ self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
141
+ except (asyncio.TimeoutError, TimeoutError):
142
+ self._returncode = -1
143
+ logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
144
+ return self._returncode
145
+
146
+ async def attach(self):
147
+ """mdmd:hidden"""
148
+ if platform.system() == "Windows":
149
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
150
+ return
151
+
152
+ from .output import OutputManager
153
+
154
+ output = OutputManager.get()
155
+ connecting_status = output.status("Connecting...")
156
+ connecting_status.start()
157
+ on_connect = asyncio.Event()
158
+
159
+ async def _write_to_fd_loop(stream: _StreamReader):
160
+ # This is required to make modal shell to an existing task work,
161
+ # since that uses ContainerExec RPCs directly, but this is hacky.
162
+ #
163
+ # TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
164
+ from .io_streams import _StreamReaderThroughServer
165
+
166
+ assert isinstance(stream._impl, _StreamReaderThroughServer)
167
+ stream_impl = stream._impl
168
+ # Don't skip empty messages so we can detect when the process has booted.
169
+ async for chunk in stream_impl._get_logs(skip_empty_messages=False):
170
+ if not on_connect.is_set():
171
+ connecting_status.stop()
172
+ on_connect.set()
173
+
174
+ await write_to_fd(stream.file_descriptor, chunk)
175
+
176
+ async def _handle_input(data: bytes, message_index: int):
177
+ self.stdin.write(data)
178
+ await self.stdin.drain()
179
+
180
+ async with TaskContext() as tc:
181
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
182
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
183
+
184
+ try:
185
+ # time out if we can't connect to the server fast enough
186
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
187
+
188
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
189
+ await stdout_task
190
+ await stderr_task
191
+
192
+ # TODO: this doesn't work right now.
193
+ # if exit_status != 0:
194
+ # raise ExecutionError(f"Process exited with status code {exit_status}")
195
+
196
+ except (asyncio.TimeoutError, TimeoutError):
197
+ connecting_status.stop()
198
+ stdout_task.cancel()
199
+ stderr_task.cancel()
200
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
201
+
202
+
203
+ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
204
+ """Yield raw bytes from a StreamReader regardless of text mode/backend."""
205
+ async for part in stream:
206
+ if isinstance(part, str):
207
+ yield part.encode("utf-8")
208
+ else:
209
+ yield part
210
+
211
+
212
+ class _ContainerProcessThroughCommandRouter(Generic[T]):
213
+ """
214
+ Container process implementation that works via direct communication with
215
+ the Modal worker where the container is running.
216
+ """
217
+
218
+ def __init__(
219
+ self,
220
+ process_id: str,
221
+ client: _Client,
222
+ command_router_client: TaskCommandRouterClient,
223
+ task_id: str,
224
+ *,
225
+ stdout: StreamType = StreamType.PIPE,
226
+ stderr: StreamType = StreamType.PIPE,
227
+ exec_deadline: Optional[float] = None,
228
+ text: bool = True,
229
+ by_line: bool = False,
230
+ ) -> None:
231
+ self._client = client
232
+ self._command_router_client = command_router_client
233
+ self._process_id = process_id
234
+ self._exec_deadline = exec_deadline
235
+ self._text = text
236
+ self._by_line = by_line
237
+ self._task_id = task_id
238
+ self._stdout = _StreamReader[T](
239
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
240
+ process_id,
241
+ "container_process",
242
+ self._client,
243
+ stream_type=stdout,
244
+ text=text,
245
+ by_line=by_line,
246
+ deadline=exec_deadline,
247
+ command_router_client=self._command_router_client,
248
+ task_id=self._task_id,
249
+ )
250
+ self._stderr = _StreamReader[T](
251
+ api_pb2.FILE_DESCRIPTOR_STDERR,
252
+ process_id,
253
+ "container_process",
254
+ self._client,
255
+ stream_type=stderr,
256
+ text=text,
257
+ by_line=by_line,
258
+ deadline=exec_deadline,
259
+ command_router_client=self._command_router_client,
260
+ task_id=self._task_id,
261
+ )
262
+ self._stdin = _StreamWriter(
263
+ process_id,
264
+ "container_process",
265
+ self._client,
266
+ command_router_client=self._command_router_client,
267
+ task_id=self._task_id,
268
+ )
269
+ self._returncode = None
270
+
271
+ def __repr__(self) -> str:
272
+ return f"ContainerProcess(process_id={self._process_id!r})"
273
+
274
+ @property
275
+ def stdout(self) -> _StreamReader[T]:
276
+ return self._stdout
277
+
278
+ @property
279
+ def stderr(self) -> _StreamReader[T]:
280
+ return self._stderr
281
+
282
+ @property
283
+ def stdin(self) -> _StreamWriter:
284
+ return self._stdin
285
+
286
+ @property
287
+ def returncode(self) -> int:
288
+ if self._returncode is None:
289
+ raise InvalidError(
290
+ "You must call wait() before accessing the returncode. "
291
+ "To poll for the status of a running process, use poll() instead."
292
+ )
293
+ return self._returncode
294
+
295
+ async def poll(self) -> Optional[int]:
296
+ if self._returncode is not None:
297
+ return self._returncode
298
+ try:
299
+ resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
300
+ which = resp.WhichOneof("exit_status")
301
+ if which is None:
302
+ return None
303
+
304
+ if which == "code":
305
+ self._returncode = int(resp.code)
306
+ return self._returncode
307
+ elif which == "signal":
308
+ self._returncode = 128 + int(resp.signal)
309
+ return self._returncode
310
+ else:
311
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
312
+ raise InvalidError("Unexpected exit status")
313
+ except ExecTimeoutError:
314
+ logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
315
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
316
+ # should probably raise an ExecTimeoutError instead.
317
+ self._returncode = -1
318
+ return self._returncode
319
+ except Exception as e:
320
+ # Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
321
+ logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
322
+ raise
323
+
324
+ async def wait(self) -> int:
325
+ if self._returncode is not None:
326
+ return self._returncode
327
+
328
+ try:
329
+ resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
330
+ which = resp.WhichOneof("exit_status")
331
+ if which == "code":
332
+ self._returncode = int(resp.code)
333
+ elif which == "signal":
334
+ self._returncode = 128 + int(resp.signal)
335
+ else:
336
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
337
+ self._returncode = -1
338
+ raise InvalidError("Unexpected exit status")
339
+ except ExecTimeoutError:
340
+ logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
341
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
342
+ # should be a ExecTimeoutError.
343
+ self._returncode = -1
344
+
345
+ return self._returncode
346
+
347
+ async def attach(self):
348
+ if platform.system() == "Windows":
349
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
350
+ return
351
+
352
+ from .output import OutputManager
353
+
354
+ output = OutputManager.get()
355
+ connecting_status = output.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)