modal 1.5.2.dev21__tar.gz → 1.5.2.dev22__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 (231) hide show
  1. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/PKG-INFO +1 -1
  2. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/task_command_router_client.py +127 -3
  3. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_help.py +7 -1
  4. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/client.pyi +2 -2
  5. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/container_process.py +13 -1
  6. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/container_process.pyi +27 -0
  7. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox.py +26 -12
  8. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox.pyi +12 -3
  9. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox_fs.py +32 -80
  10. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/sandbox_fs.pyi +24 -0
  11. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/PKG-INFO +1 -1
  12. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_grpc.py +32 -0
  13. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2.py +928 -861
  14. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2.pyi +149 -0
  15. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.py +67 -1
  16. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.pyi +23 -3
  17. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/modal_api_grpc.py +2 -0
  18. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_version/__init__.py +1 -1
  19. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/LICENSE +0 -0
  20. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/README.md +0 -0
  21. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/__init__.py +0 -0
  22. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/__main__.py +0 -0
  23. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_billing.py +0 -0
  24. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_clustered_functions.py +0 -0
  25. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_clustered_functions.pyi +0 -0
  26. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_container_entrypoint.py +0 -0
  27. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_environments.py +0 -0
  28. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_function_variants.py +0 -0
  29. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_functions.py +0 -0
  30. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_grpc_client.py +0 -0
  31. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_image.py +0 -0
  32. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_ipython.py +0 -0
  33. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_load_context.py +0 -0
  34. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_location.py +0 -0
  35. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_logs.py +0 -0
  36. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_object.py +0 -0
  37. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/__init__.py +0 -0
  38. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/manager.py +0 -0
  39. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/pty.py +0 -0
  40. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/rich.py +0 -0
  41. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_output/status.py +0 -0
  42. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_partial_function.py +0 -0
  43. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_resolver.py +0 -0
  44. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_resources.py +0 -0
  45. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/__init__.py +0 -0
  46. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/asgi.py +0 -0
  47. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.py +0 -0
  48. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
  49. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/execution_context.py +0 -0
  50. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/execution_context.pyi +0 -0
  51. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  52. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
  53. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  54. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/telemetry.py +0 -0
  55. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
  56. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_runtime/user_code_imports.py +0 -0
  57. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_serialization.py +0 -0
  58. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_server.py +0 -0
  59. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_traceback.py +0 -0
  60. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_tunnel.py +0 -0
  61. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_tunnel.pyi +0 -0
  62. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_type_manager.py +0 -0
  63. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/__init__.py +0 -0
  64. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/app_utils.py +0 -0
  65. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/async_utils.py +0 -0
  66. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/auth_token_manager.py +0 -0
  67. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/blob_utils.py +0 -0
  68. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/browser_utils.py +0 -0
  69. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
  70. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/curl_utils.py +0 -0
  71. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/deprecation.py +0 -0
  72. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/docker_utils.py +0 -0
  73. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/function_utils.py +0 -0
  74. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/git_utils.py +0 -0
  75. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/grpc_testing.py +0 -0
  76. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/grpc_utils.py +0 -0
  77. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/hash_utils.py +0 -0
  78. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/http_utils.py +0 -0
  79. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/jwt_utils.py +0 -0
  80. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/logger.py +0 -0
  81. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/mount_utils.py +0 -0
  82. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/name_utils.py +0 -0
  83. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/package_utils.py +0 -0
  84. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/pattern_utils.py +0 -0
  85. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/proxy_support.py +0 -0
  86. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/rand_pb_testing.py +0 -0
  87. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
  88. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/shell_utils.py +0 -0
  89. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_utils/time_utils.py +0 -0
  90. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/__init__.py +0 -0
  91. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  92. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/cloudpickle.py +0 -0
  93. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/tblib.py +0 -0
  94. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_vendor/version.py +0 -0
  95. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_watcher.py +0 -0
  96. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/_workspace.py +0 -0
  97. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/app.py +0 -0
  98. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/app.pyi +0 -0
  99. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/billing.py +0 -0
  100. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/billing.pyi +0 -0
  101. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2023.12.312.txt +0 -0
  102. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2023.12.txt +0 -0
  103. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2024.04.txt +0 -0
  104. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2024.10.txt +0 -0
  105. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/2025.06.txt +0 -0
  106. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/PREVIEW.txt +0 -0
  107. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/README.md +0 -0
  108. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/builder/base-images.json +0 -0
  109. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/call_graph.py +0 -0
  110. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/__init__.py +0 -0
  111. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_download.py +0 -0
  112. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/_traceback.py +0 -0
  113. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/app.py +0 -0
  114. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/billing.py +0 -0
  115. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/bootstrap.py +0 -0
  116. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/changelog.py +0 -0
  117. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/cluster.py +0 -0
  118. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/config.py +0 -0
  119. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/container.py +0 -0
  120. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/curl.py +0 -0
  121. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/dashboard.py +0 -0
  122. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/dict.py +0 -0
  123. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/endpoint.py +0 -0
  124. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/entry_point.py +0 -0
  125. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/environment.py +0 -0
  126. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/image.py +0 -0
  127. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/import_refs.py +0 -0
  128. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/launch.py +0 -0
  129. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/logo.py +0 -0
  130. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/network_file_system.py +0 -0
  131. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/profile.py +0 -0
  132. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/__init__.py +0 -0
  133. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/run_jupyter.py +0 -0
  134. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/programs/vscode.py +0 -0
  135. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/queues.py +0 -0
  136. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/run.py +0 -0
  137. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/secret.py +0 -0
  138. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/selector.py +0 -0
  139. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/shell.py +0 -0
  140. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/skills.py +0 -0
  141. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/token.py +0 -0
  142. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/utils.py +0 -0
  143. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/volume.py +0 -0
  144. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cli/workspace.py +0 -0
  145. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/client.py +0 -0
  146. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.py +0 -0
  147. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.pyi +0 -0
  148. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cls.py +0 -0
  149. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/cls.pyi +0 -0
  150. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/config.py +0 -0
  151. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/dict.py +0 -0
  152. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/dict.pyi +0 -0
  153. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/environments.py +0 -0
  154. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/environments.pyi +0 -0
  155. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/exception.py +0 -0
  156. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/__init__.py +0 -0
  157. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/flash.py +0 -0
  158. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/flash.pyi +0 -0
  159. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/experimental/ipython.py +0 -0
  160. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_io.py +0 -0
  161. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_io.pyi +0 -0
  162. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/file_pattern_matcher.py +0 -0
  163. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/functions.py +0 -0
  164. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/functions.pyi +0 -0
  165. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/image.py +0 -0
  166. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/image.pyi +0 -0
  167. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/io_streams.py +0 -0
  168. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/io_streams.pyi +0 -0
  169. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/mount.py +0 -0
  170. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/mount.pyi +0 -0
  171. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/network_file_system.py +0 -0
  172. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/network_file_system.pyi +0 -0
  173. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/object.py +0 -0
  174. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/object.pyi +0 -0
  175. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/output.py +0 -0
  176. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/parallel_map.py +0 -0
  177. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/parallel_map.pyi +0 -0
  178. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/partial_function.py +0 -0
  179. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/partial_function.pyi +0 -0
  180. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/proxy.py +0 -0
  181. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/proxy.pyi +0 -0
  182. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/py.typed +0 -0
  183. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/queue.py +0 -0
  184. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/queue.pyi +0 -0
  185. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/retries.py +0 -0
  186. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/runner.py +0 -0
  187. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/runner.pyi +0 -0
  188. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/running_app.py +0 -0
  189. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/schedule.py +0 -0
  190. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/scheduler_placement.py +0 -0
  191. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/secret.py +0 -0
  192. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/secret.pyi +0 -0
  193. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/server.py +0 -0
  194. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/server.pyi +0 -0
  195. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/serving.py +0 -0
  196. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/serving.pyi +0 -0
  197. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/skills/modal/SKILL.md +0 -0
  198. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/snapshot.py +0 -0
  199. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/snapshot.pyi +0 -0
  200. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/stream_type.py +0 -0
  201. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/token_flow.py +0 -0
  202. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/token_flow.pyi +0 -0
  203. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/types.py +0 -0
  204. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/volume.py +0 -0
  205. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/volume.pyi +0 -0
  206. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/workspace.py +0 -0
  207. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal/workspace.pyi +0 -0
  208. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/SOURCES.txt +0 -0
  209. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/dependency_links.txt +0 -0
  210. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/entry_points.txt +0 -0
  211. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/requires.txt +0 -0
  212. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal.egg-info/top_level.txt +0 -0
  213. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/__init__.py +0 -0
  214. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs.py +0 -0
  215. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
  216. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs.py +0 -0
  217. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
  218. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/__init__.py +0 -0
  219. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/mdmd.py +0 -0
  220. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/signatures.py +0 -0
  221. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_docs/mdmd/types.py +0 -0
  222. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/__init__.py +0 -0
  223. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/py.typed +0 -0
  224. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_grpc.py +0 -0
  225. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.py +0 -0
  226. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.pyi +0 -0
  227. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  228. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  229. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/modal_version/__main__.py +0 -0
  230. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/pyproject.toml +0 -0
  231. {modal-1.5.2.dev21 → modal-1.5.2.dev22}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.2.dev21
3
+ Version: 1.5.2.dev22
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import asyncio
3
3
  import base64
4
+ import io
4
5
  import json
5
6
  import socket
6
7
  import ssl
@@ -8,9 +9,9 @@ import time
8
9
  import typing
9
10
  import urllib.parse
10
11
  import weakref
11
- from collections.abc import AsyncGenerator, Callable
12
+ from collections.abc import AsyncGenerator, AsyncIterable, Callable
12
13
  from contextlib import suppress
13
- from typing import TypeVar
14
+ from typing import BinaryIO, TypeVar
14
15
 
15
16
  import grpclib.client
16
17
  import grpclib.config
@@ -18,7 +19,7 @@ from grpclib import GRPCError, Status
18
19
  from grpclib.exceptions import StreamTerminatedError
19
20
 
20
21
  from modal.config import logger
21
- from modal.exception import ExecTimeoutError, TimeoutError as ModalTimeoutError
22
+ from modal.exception import AuthError, ExecTimeoutError, InternalError, ServiceError, TimeoutError as ModalTimeoutError
22
23
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
23
24
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
24
25
 
@@ -27,6 +28,17 @@ from .._utils.grpc_utils import ModalChannel
27
28
  from .async_utils import aclosing, retry
28
29
  from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
29
30
 
31
+ STREAMING_STDIN_CHUNK_SIZE = 256 * 1024
32
+
33
+ _STREAMING_STDIN_RESUMABLE_EXCEPTIONS = (
34
+ AuthError,
35
+ InternalError,
36
+ ServiceError,
37
+ StreamTerminatedError,
38
+ OSError,
39
+ asyncio.TimeoutError,
40
+ )
41
+
30
42
 
31
43
  @retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
32
44
  async def _connect_channel(channel: grpclib.client.Channel):
@@ -472,6 +484,118 @@ class TaskCommandRouterClient:
472
484
  lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
473
485
  )
474
486
 
487
+ async def exec_stdin_write_stream(
488
+ self,
489
+ task_id: str,
490
+ exec_id: str,
491
+ source: BinaryIO,
492
+ chunk_size: int = STREAMING_STDIN_CHUNK_SIZE,
493
+ max_resume_attempts: int = 9,
494
+ ) -> int:
495
+ """Stream `source` into the exec's stdin, with bounded resume on transient failures.
496
+
497
+ Streams the full contents of `source` in one client-streaming RPC and
498
+ closes stdin (EOF) on success. On a resumable error, queries
499
+ `exec_stdin_status` for the server's canonical offset, seeks `source`
500
+ to that point, and reopens the stream.
501
+
502
+ Args:
503
+ task_id: The task ID of the task running the exec'd command.
504
+ exec_id: The execution ID of the command to write to.
505
+ source: A seekable byte source (file, BytesIO, etc.).
506
+ chunk_size: Bytes per outbound message.
507
+ max_resume_attempts: Upper bound on resume retries before giving up.
508
+
509
+ Returns:
510
+ The total number of bytes streamed into the exec's stdin.
511
+ """
512
+ offset = 0
513
+ attempt = 0
514
+ while True:
515
+
516
+ async def _chunks():
517
+ while True:
518
+ chunk = source.read(chunk_size)
519
+ if not chunk:
520
+ return
521
+ yield chunk
522
+
523
+ try:
524
+ source.seek(offset)
525
+ await self._exec_stdin_write_stream(task_id, exec_id, offset, _chunks())
526
+ return source.tell()
527
+ except (*_STREAMING_STDIN_RESUMABLE_EXCEPTIONS, AttributeError) as e:
528
+ # StreamTerminatedError surfaces as AttributeError in grpclib<=0.4.7;
529
+ # see call_with_retries_on_transient_errors.
530
+ if isinstance(e, AttributeError) and "_write_appdata" not in str(e):
531
+ raise
532
+ attempt += 1
533
+ if attempt > max_resume_attempts:
534
+ raise
535
+ status = await self.exec_stdin_status(task_id=task_id, exec_id=exec_id)
536
+ if status.closed:
537
+ pos = source.tell()
538
+ if status.num_bytes_written == pos and source.seek(0, io.SEEK_END) == pos:
539
+ logger.debug(f"exec_stdin_write_stream completed but response was lost: {e}")
540
+ return pos
541
+ raise
542
+ offset = status.num_bytes_written
543
+ logger.debug(f"exec_stdin_write_stream resuming from offset {offset} after error: {e}")
544
+
545
+ async def _exec_stdin_write_stream(
546
+ self,
547
+ task_id: str,
548
+ exec_id: str,
549
+ start_offset: int,
550
+ chunks: AsyncIterable[bytes],
551
+ ) -> sr_pb2.TaskExecStdinWriteStreamResponse | None:
552
+ """Single client-streaming attempt: Start, Data chunks, then End (EOF).
553
+
554
+ Does not retry; `exec_stdin_write_stream` owns resume.
555
+ """
556
+ with grpc_error_converter():
557
+ stream = self._stub.TaskExecStdinWriteStream.open(metadata=self._get_metadata())
558
+ async with stream as s:
559
+ start = sr_pb2.TaskExecStdinWriteStreamRequest(
560
+ start=sr_pb2.TaskExecStdinWriteStreamStart(
561
+ task_id=task_id,
562
+ exec_id=exec_id,
563
+ offset=start_offset,
564
+ ),
565
+ )
566
+ await s.send_message(start)
567
+ async for data in chunks:
568
+ if not data:
569
+ continue
570
+ await s.send_message(sr_pb2.TaskExecStdinWriteStreamRequest(data=data))
571
+ # The server closes stdin only on this explicit End message. A
572
+ # stream that breaks before it leaves stdin open for resume.
573
+ await s.send_message(
574
+ sr_pb2.TaskExecStdinWriteStreamRequest(end=sr_pb2.TaskExecStdinWriteStreamEnd()),
575
+ end=True,
576
+ )
577
+ return await s.recv_message()
578
+
579
+ async def exec_stdin_status(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecStdinStatusResponse:
580
+ """Read the current stdin write status for an exec'd command, to support retries from the right offset.
581
+
582
+ Used by streaming clients to find the resume offset after a stream
583
+ failure. Evicts any in-flight stdin stream for the exec, so it is not
584
+ safe to call for read-only observability.
585
+
586
+ Args:
587
+ task_id: The task ID of the task running the exec'd command.
588
+ exec_id: The execution ID of the command to query.
589
+ Raises:
590
+ Other errors: If retries are exhausted on transient errors or if there's an error
591
+ from the RPC itself.
592
+ """
593
+ request = sr_pb2.TaskExecStdinStatusRequest(task_id=task_id, exec_id=exec_id)
594
+ with grpc_error_converter():
595
+ return await call_with_retries_on_transient_errors(
596
+ lambda: self._call_with_auth_retry(self._stub.TaskExecStdinStatus, request)
597
+ )
598
+
475
599
  async def sandbox_stdin_write_v2(
476
600
  self, task_id: str, offset: int, data: bytes, eof: bool
477
601
  ) -> sr_pb2.SandboxStdinWriteV2Response:
@@ -70,7 +70,13 @@ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
70
70
  text.append(opt, style=_OPTION_FLAG_STYLE)
71
71
  if not getattr(param, "is_flag", False) and not getattr(param, "count", False):
72
72
  text.append(" ")
73
- text.append(param.make_metavar(ctx), style=_OPTION_METAVAR_STYLE)
73
+ if "ctx" in inspect.signature(click.Parameter.make_metavar).parameters:
74
+ # click >= 8.2.0
75
+ metavar = param.make_metavar(ctx)
76
+ else:
77
+ # backwards compatibility
78
+ metavar = param.make_metavar() # type: ignore[call-arg]
79
+ text.append(metavar, style=_OPTION_METAVAR_STYLE)
74
80
  return text
75
81
 
76
82
 
@@ -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.5.2.dev21",
38
+ version: str = "1.5.2.dev22",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -205,7 +205,7 @@ class Client:
205
205
  server_url: str,
206
206
  client_type: int,
207
207
  credentials: typing.Optional[tuple[str, str]],
208
- version: str = "1.5.2.dev21",
208
+ version: str = "1.5.2.dev22",
209
209
  ):
210
210
  """mdmd:hidden
211
211
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,7 +1,7 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
3
  import platform
4
- from typing import Generic, TypeVar
4
+ from typing import BinaryIO, Generic, TypeVar
5
5
 
6
6
  from modal_proto import api_pb2
7
7
 
@@ -117,6 +117,18 @@ class _ContainerProcess(Generic[T]):
117
117
  )
118
118
  return self._returncode
119
119
 
120
+ async def _stdin_write_stream(self, source: BinaryIO) -> int:
121
+ """mdmd:hidden
122
+ Stream `source` into the process's stdin and close it (EOF) on success.
123
+
124
+ Returns the total bytes streamed.
125
+ """
126
+ return await self._command_router_client.exec_stdin_write_stream(
127
+ task_id=self._task_id,
128
+ exec_id=self._process_id,
129
+ source=source,
130
+ )
131
+
120
132
  async def poll(self) -> int | None:
121
133
  """Check if the container process has finished running.
122
134
 
@@ -53,6 +53,14 @@ class _ContainerProcess(typing.Generic[T]):
53
53
 
54
54
  @property
55
55
  def returncode(self) -> int: ...
56
+ async def _stdin_write_stream(self, source: typing.BinaryIO) -> int:
57
+ """mdmd:hidden
58
+ Stream `source` into the process's stdin and close it (EOF) on success.
59
+
60
+ Returns the total bytes streamed.
61
+ """
62
+ ...
63
+
56
64
  async def poll(self) -> typing.Optional[int]:
57
65
  """Check if the container process has finished running.
58
66
 
@@ -105,6 +113,25 @@ class ContainerProcess(typing.Generic[T]):
105
113
  @property
106
114
  def returncode(self) -> int: ...
107
115
 
116
+ class ___stdin_write_stream_spec(typing_extensions.Protocol):
117
+ def __call__(self, /, source: typing.BinaryIO) -> int:
118
+ """mdmd:hidden
119
+ Stream `source` into the process's stdin and close it (EOF) on success.
120
+
121
+ Returns the total bytes streamed.
122
+ """
123
+ ...
124
+
125
+ async def aio(self, /, source: typing.BinaryIO) -> int:
126
+ """mdmd:hidden
127
+ Stream `source` into the process's stdin and close it (EOF) on success.
128
+
129
+ Returns the total bytes streamed.
130
+ """
131
+ ...
132
+
133
+ _stdin_write_stream: ___stdin_write_stream_spec
134
+
108
135
  class __poll_spec(typing_extensions.Protocol):
109
136
  def __call__(self, /) -> typing.Optional[int]:
110
137
  """Check if the container process has finished running.
@@ -816,6 +816,7 @@ class _Sandbox(_Object, type_prefix="sb"):
816
816
  *args: str,
817
817
  app: "modal.app._App | None" = None,
818
818
  name: str | None = None,
819
+ tags: dict[str, str] | None = None,
819
820
  image: _Image | None = None,
820
821
  env: dict[str, str | None] | None = None,
821
822
  secrets: Collection[_Secret] | None = None,
@@ -851,7 +852,7 @@ class _Sandbox(_Object, type_prefix="sb"):
851
852
  `secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
852
853
  filesystem snapshots.
853
854
 
854
- Features like tags, memory snapshots, network file systems, GPUs, and custom
855
+ Features like memory snapshots, network file systems, GPUs, and custom
855
856
  domains are not supported.
856
857
 
857
858
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
@@ -993,10 +994,12 @@ class _Sandbox(_Object, type_prefix="sb"):
993
994
  ),
994
995
  )
995
996
 
997
+ tag_protos = [api_pb2.SandboxTag(tag_name=k, tag_value=v) for k, v in tags.items()] if tags else []
996
998
  create_req = api_pb2.SandboxCreateV2Request(
997
999
  app_id=load_context.app_id,
998
1000
  definition=definition,
999
1001
  ephemeral_secrets=api_pb2.StringMap(contents=env_dict) if env_dict else None,
1002
+ tags=tag_protos,
1000
1003
  )
1001
1004
  assert load_context.client._auth_token_manager
1002
1005
  auth_token = await load_context.client._auth_token_manager.get_token()
@@ -1267,22 +1270,27 @@ class _Sandbox(_Object, type_prefix="sb"):
1267
1270
  Returns:
1268
1271
  Tags as a map from tag name to tag value.
1269
1272
  """
1270
- self._ensure_v1("get_tags")
1271
1273
  req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
1272
- resp = await self._client.stub.SandboxTagsGet(req)
1274
+ stub = self._client.stub
1275
+ if self._is_v2:
1276
+ assert self._client._auth_token_manager
1277
+ auth_token = await self._client._auth_token_manager.get_token()
1278
+ resp = await stub.SandboxTagsGetV2(req, metadata=[("x-modal-auth-token", auth_token)])
1279
+ else:
1280
+ resp = await stub.SandboxTagsGet(req)
1273
1281
 
1274
1282
  return {tag.tag_name: tag.tag_value for tag in resp.tags}
1275
1283
 
1276
1284
  async def set_tags(self, tags: dict[str, str], *, client: _Client | None = None) -> None:
1277
1285
  """Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
1278
1286
 
1287
+ Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
1288
+
1279
1289
  Args:
1280
1290
  tags: Tag names and values to set on this sandbox.
1281
1291
  client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
1282
1292
 
1283
1293
  """
1284
- self._ensure_v1("set_tags")
1285
- environment_name = _get_environment_name()
1286
1294
  if client is not None:
1287
1295
  deprecation_warning(
1288
1296
  (2025, 9, 18),
@@ -1291,13 +1299,19 @@ class _Sandbox(_Object, type_prefix="sb"):
1291
1299
  )
1292
1300
 
1293
1301
  tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()]
1294
-
1295
- req = api_pb2.SandboxTagsSetRequest(
1296
- environment_name=environment_name,
1297
- sandbox_id=self.object_id,
1298
- tags=tags_list,
1299
- )
1300
- await self._client.stub.SandboxTagsSet(req)
1302
+ stub = self._client.stub
1303
+ if self._is_v2:
1304
+ assert self._client._auth_token_manager
1305
+ auth_token = await self._client._auth_token_manager.get_token()
1306
+ req = api_pb2.SandboxTagsSetRequest(sandbox_id=self.object_id, tags=tags_list)
1307
+ await stub.SandboxTagsSetV2(req, metadata=[("x-modal-auth-token", auth_token)])
1308
+ else:
1309
+ req = api_pb2.SandboxTagsSetRequest(
1310
+ environment_name=_get_environment_name(),
1311
+ sandbox_id=self.object_id,
1312
+ tags=tags_list,
1313
+ )
1314
+ await stub.SandboxTagsSet(req)
1301
1315
 
1302
1316
  async def _experimental_set_outbound_network_policy(
1303
1317
  self,
@@ -361,6 +361,7 @@ class _Sandbox(modal._object._Object):
361
361
  *args: str,
362
362
  app: typing.Optional[modal.app._App] = None,
363
363
  name: typing.Optional[str] = None,
364
+ tags: typing.Optional[dict[str, str]] = None,
364
365
  image: typing.Optional[modal._image._Image] = None,
365
366
  env: typing.Optional[dict[str, typing.Optional[str]]] = None,
366
367
  secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
@@ -399,7 +400,7 @@ class _Sandbox(modal._object._Object):
399
400
  `secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
400
401
  filesystem snapshots.
401
402
 
402
- Features like tags, memory snapshots, network file systems, GPUs, and custom
403
+ Features like memory snapshots, network file systems, GPUs, and custom
403
404
  domains are not supported.
404
405
 
405
406
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
@@ -518,6 +519,8 @@ class _Sandbox(modal._object._Object):
518
519
  async def set_tags(self, tags: dict[str, str], *, client: typing.Optional[modal.client._Client] = None) -> None:
519
520
  """Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
520
521
 
522
+ Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
523
+
521
524
  Args:
522
525
  tags: Tag names and values to set on this sandbox.
523
526
  client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
@@ -1711,6 +1714,7 @@ class Sandbox(modal.object.Object):
1711
1714
  *args: str,
1712
1715
  app: typing.Optional[modal.app.App] = None,
1713
1716
  name: typing.Optional[str] = None,
1717
+ tags: typing.Optional[dict[str, str]] = None,
1714
1718
  image: typing.Optional[modal.image.Image] = None,
1715
1719
  env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1716
1720
  secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
@@ -1749,7 +1753,7 @@ class Sandbox(modal.object.Object):
1749
1753
  `secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
1750
1754
  filesystem snapshots.
1751
1755
 
1752
- Features like tags, memory snapshots, network file systems, GPUs, and custom
1756
+ Features like memory snapshots, network file systems, GPUs, and custom
1753
1757
  domains are not supported.
1754
1758
 
1755
1759
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
@@ -1772,6 +1776,7 @@ class Sandbox(modal.object.Object):
1772
1776
  *args: str,
1773
1777
  app: typing.Optional[modal.app.App] = None,
1774
1778
  name: typing.Optional[str] = None,
1779
+ tags: typing.Optional[dict[str, str]] = None,
1775
1780
  image: typing.Optional[modal.image.Image] = None,
1776
1781
  env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1777
1782
  secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
@@ -1810,7 +1815,7 @@ class Sandbox(modal.object.Object):
1810
1815
  `secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
1811
1816
  filesystem snapshots.
1812
1817
 
1813
- Features like tags, memory snapshots, network file systems, GPUs, and custom
1818
+ Features like memory snapshots, network file systems, GPUs, and custom
1814
1819
  domains are not supported.
1815
1820
 
1816
1821
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
@@ -2035,6 +2040,8 @@ class Sandbox(modal.object.Object):
2035
2040
  def __call__(self, /, tags: dict[str, str], *, client: typing.Optional[modal.client.Client] = None) -> None:
2036
2041
  """Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
2037
2042
 
2043
+ Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
2044
+
2038
2045
  Args:
2039
2046
  tags: Tag names and values to set on this sandbox.
2040
2047
  client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
@@ -2044,6 +2051,8 @@ class Sandbox(modal.object.Object):
2044
2051
  async def aio(self, /, tags: dict[str, str], *, client: typing.Optional[modal.client.Client] = None) -> None:
2045
2052
  """Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
2046
2053
 
2054
+ Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
2055
+
2047
2056
  Args:
2048
2057
  tags: Tag names and values to set on this sandbox.
2049
2058
  client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2026
2
2
  import asyncio
3
+ import io
3
4
  import json
4
5
  import os
5
6
  import random
@@ -7,7 +8,7 @@ import string
7
8
  import time
8
9
  import weakref
9
10
  from pathlib import Path
10
- from typing import TYPE_CHECKING, AsyncIterator, Optional, Union, cast
11
+ from typing import TYPE_CHECKING, AsyncIterator, BinaryIO, Optional, Union, cast
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  import modal.sandbox
@@ -33,7 +34,6 @@ from ._utils.sandbox_fs_utils import (
33
34
  validate_absolute_remote_path,
34
35
  )
35
36
  from .exception import ConflictError
36
- from .io_streams import TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE
37
37
  from .types import FileInfo, FileType, FileWatchEvent, FileWatchEventType
38
38
 
39
39
  _SANDBOX_FS_TOOLS_PATH = "/__modal/.bin/modal-sandbox-fs-tools"
@@ -111,69 +111,8 @@ class _SandboxFilesystem:
111
111
  validate_absolute_remote_path(remote_path, "copy_from_local")
112
112
 
113
113
  t0 = time.monotonic()
114
- total_bytes = 0
115
114
  with open(local_path, "rb") as file_obj:
116
- with translate_exec_errors("copy_from_local", remote_path):
117
- process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
118
- try:
119
- while True:
120
- logger.debug(
121
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): reading "
122
- f"{TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE} bytes from {local_path} at offset {total_bytes}"
123
- )
124
- # TODO(saltzm): If this fails, the ContainerProcess will remain alive indefinitely since
125
- # stdin will remain open. Unfortunately we can't just call write_eof either, since that
126
- # would lead to a partially written file being persisted. We should catch exceptions
127
- # from this and kill the ContainerProcess when we have a way to do this.
128
- chunk = file_obj.read(TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE)
129
- if not chunk:
130
- logger.debug(
131
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): read no data from "
132
- f"{local_path}, finished reading file"
133
- )
134
- break
135
- total_bytes += len(chunk)
136
- logger.debug(
137
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing {len(chunk)} bytes "
138
- f"from {local_path} to remote {remote_path}"
139
- )
140
- process.stdin.write(chunk)
141
- await process.stdin.drain()
142
- logger.debug(
143
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing "
144
- f"{len(chunk)} bytes from {local_path} to remote {remote_path}"
145
- )
146
- logger.debug(
147
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing eof to remote {remote_path}"
148
- )
149
- process.stdin.write_eof()
150
- await process.stdin.drain()
151
- logger.debug(
152
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing eof to remote "
153
- f"{remote_path}"
154
- )
155
- # When the FS tools binary exits early on an error, the worker
156
- # reports the dropped stdin write as ConflictError.
157
- except ConflictError:
158
- pass
159
-
160
- async def read_stderr():
161
- stderr = await process.stderr.read()
162
- logger.debug(f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished reading stderr")
163
- return stderr
164
-
165
- async def wait_for_process_completion():
166
- returncode = await process.wait()
167
- logger.debug(
168
- f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished waiting for process "
169
- "completion"
170
- )
171
- return returncode
172
-
173
- stderr, returncode = await asyncio.gather(read_stderr(), wait_for_process_completion())
174
-
175
- if returncode != 0:
176
- raise_write_file_error(returncode, stderr, remote_path)
115
+ total_bytes = await self._exec_fs_tool_write(file_obj, remote_path, "copy_from_local")
177
116
 
178
117
  dur_s = max(time.monotonic() - t0, 0.001)
179
118
  _log_throughput(f"copy_from_local('{local_path}', '{remote_path}')", total_bytes, dur_s)
@@ -638,22 +577,7 @@ class _SandboxFilesystem:
638
577
  raise TypeError("data must be bytes-like")
639
578
 
640
579
  t0 = time.monotonic()
641
- with translate_exec_errors("write_bytes", remote_path):
642
- process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
643
- try:
644
- for offset in range(0, max(len(data), 1), TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE):
645
- process.stdin.write(data[offset : offset + TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE])
646
- await process.stdin.drain()
647
- process.stdin.write_eof()
648
- await process.stdin.drain()
649
- # When the FS tools binary exits early on an error, the worker
650
- # reports the dropped stdin write as ConflictError.
651
- except ConflictError:
652
- pass
653
- stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
654
-
655
- if returncode != 0:
656
- raise_write_file_error(returncode, stderr, remote_path)
580
+ await self._exec_fs_tool_write(io.BytesIO(data), remote_path, "write_bytes")
657
581
 
658
582
  dur_s = max(time.monotonic() - t0, 0.001)
659
583
  _log_throughput(f"write_bytes {remote_path}", len(data), dur_s)
@@ -686,5 +610,33 @@ class _SandboxFilesystem:
686
610
  raise TypeError("data must be str")
687
611
  await self.write_bytes(data.encode("utf-8"), remote_path)
688
612
 
613
+ async def _exec_fs_tool_write(self, source: BinaryIO, remote_path: str, op_name: str) -> int:
614
+ """Exec the FS-tools write command and stream `source` into its stdin.
615
+
616
+ Returns the number of bytes streamed.
617
+ """
618
+ with translate_exec_errors(op_name, remote_path):
619
+ process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
620
+ # TODO(saltzm): If streaming fails after resume attempts are exhausted, the
621
+ # ContainerProcess will remain alive indefinitely since stdin will remain open.
622
+ # We should catch exceptions from this and kill the ContainerProcess when we
623
+ # have a way to do this.
624
+ try:
625
+ total_bytes = await process._stdin_write_stream(source)
626
+ # When the FS tools binary exits early on an error, the worker
627
+ # reports the dropped stdin write as ConflictError.
628
+ except ConflictError:
629
+ # ConflictError can come from a failure in fs-tools or server-side.
630
+ # - if server-side, the process won't exit, so the gather below would hang forever -> raise
631
+ # - else if fs-tools, process will be closed, use raise_write_file_error below
632
+ if await process.poll() is None:
633
+ raise
634
+ total_bytes = source.tell()
635
+ stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
636
+
637
+ if returncode != 0:
638
+ raise_write_file_error(returncode, stderr, remote_path)
639
+ return total_bytes
640
+
689
641
 
690
642
  SandboxFilesystem = synchronize_api(_SandboxFilesystem)
@@ -348,6 +348,13 @@ class _SandboxFilesystem:
348
348
  """
349
349
  ...
350
350
 
351
+ async def _exec_fs_tool_write(self, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
352
+ """Exec the FS-tools write command and stream `source` into its stdin.
353
+
354
+ Returns the number of bytes streamed.
355
+ """
356
+ ...
357
+
351
358
  class SandboxFilesystem:
352
359
  """mdmd:namespace
353
360
  Namespace for Sandbox filesystem APIs.
@@ -1041,3 +1048,20 @@ class SandboxFilesystem:
1041
1048
  ...
1042
1049
 
1043
1050
  write_text: __write_text_spec
1051
+
1052
+ class ___exec_fs_tool_write_spec(typing_extensions.Protocol):
1053
+ def __call__(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
1054
+ """Exec the FS-tools write command and stream `source` into its stdin.
1055
+
1056
+ Returns the number of bytes streamed.
1057
+ """
1058
+ ...
1059
+
1060
+ async def aio(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
1061
+ """Exec the FS-tools write command and stream `source` into its stdin.
1062
+
1063
+ Returns the number of bytes streamed.
1064
+ """
1065
+ ...
1066
+
1067
+ _exec_fs_tool_write: ___exec_fs_tool_write_spec
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.2.dev21
3
+ Version: 1.5.2.dev22
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0