modal 1.4.2.dev13__tar.gz → 1.4.2.dev15__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 (211) hide show
  1. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/PKG-INFO +1 -1
  2. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/sandbox_fs_utils.py +37 -0
  3. modal-1.4.2.dev15/modal/cli/bootstrap.py +136 -0
  4. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/entry_point.py +8 -24
  5. modal-1.4.2.dev15/modal/cli/logo.py +70 -0
  6. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/selector.py +12 -4
  7. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/client.pyi +2 -2
  8. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/exception.py +6 -0
  9. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox.py +36 -5
  10. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox.pyi +24 -6
  11. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox_fs.py +38 -1
  12. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/sandbox_fs.pyi +84 -3
  13. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/PKG-INFO +1 -1
  14. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/SOURCES.txt +2 -0
  15. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_grpc.py +80 -0
  16. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2.py +863 -749
  17. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2.pyi +273 -0
  18. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2_grpc.py +166 -0
  19. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/api_pb2_grpc.pyi +52 -0
  20. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/modal_api_grpc.py +5 -0
  21. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_version/__init__.py +1 -1
  22. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/LICENSE +0 -0
  23. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/README.md +0 -0
  24. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/__init__.py +0 -0
  25. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/__main__.py +0 -0
  26. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_billing.py +0 -0
  27. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_clustered_functions.py +0 -0
  28. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_clustered_functions.pyi +0 -0
  29. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_container_entrypoint.py +0 -0
  30. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_functions.py +0 -0
  31. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_grpc_client.py +0 -0
  32. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_ipython.py +0 -0
  33. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_load_context.py +0 -0
  34. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_location.py +0 -0
  35. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_logs.py +0 -0
  36. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_object.py +0 -0
  37. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/__init__.py +0 -0
  38. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/manager.py +0 -0
  39. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/pty.py +0 -0
  40. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/rich.py +0 -0
  41. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_output/status.py +0 -0
  42. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_partial_function.py +0 -0
  43. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_resolver.py +0 -0
  44. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_resources.py +0 -0
  45. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/__init__.py +0 -0
  46. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/asgi.py +0 -0
  47. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/container_io_manager.py +0 -0
  48. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
  49. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/execution_context.py +0 -0
  50. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/execution_context.pyi +0 -0
  51. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  52. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/telemetry.py +0 -0
  53. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
  54. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_runtime/user_code_imports.py +0 -0
  55. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_serialization.py +0 -0
  56. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_server.py +0 -0
  57. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_traceback.py +0 -0
  58. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_tunnel.py +0 -0
  59. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_tunnel.pyi +0 -0
  60. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_type_manager.py +0 -0
  61. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/__init__.py +0 -0
  62. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/app_utils.py +0 -0
  63. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/async_utils.py +0 -0
  64. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/auth_token_manager.py +0 -0
  65. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/blob_utils.py +0 -0
  66. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/browser_utils.py +0 -0
  67. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
  68. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/deprecation.py +0 -0
  69. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/docker_utils.py +0 -0
  70. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/function_utils.py +0 -0
  71. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/git_utils.py +0 -0
  72. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/grpc_testing.py +0 -0
  73. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/grpc_utils.py +0 -0
  74. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/hash_utils.py +0 -0
  75. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/http_utils.py +0 -0
  76. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/jwt_utils.py +0 -0
  77. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/logger.py +0 -0
  78. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/mount_utils.py +0 -0
  79. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/name_utils.py +0 -0
  80. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/package_utils.py +0 -0
  81. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/pattern_utils.py +0 -0
  82. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/rand_pb_testing.py +0 -0
  83. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/shell_utils.py +0 -0
  84. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/task_command_router_client.py +0 -0
  85. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_utils/time_utils.py +0 -0
  86. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/__init__.py +0 -0
  87. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  88. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/cloudpickle.py +0 -0
  89. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/tblib.py +0 -0
  90. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_vendor/version.py +0 -0
  91. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/_watcher.py +0 -0
  92. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/app.py +0 -0
  93. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/app.pyi +0 -0
  94. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/billing.py +0 -0
  95. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2023.12.312.txt +0 -0
  96. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2023.12.txt +0 -0
  97. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2024.04.txt +0 -0
  98. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2024.10.txt +0 -0
  99. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/2025.06.txt +0 -0
  100. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/PREVIEW.txt +0 -0
  101. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/README.md +0 -0
  102. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/builder/base-images.json +0 -0
  103. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/call_graph.py +0 -0
  104. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/__init__.py +0 -0
  105. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/_download.py +0 -0
  106. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/_traceback.py +0 -0
  107. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/app.py +0 -0
  108. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/billing.py +0 -0
  109. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/changelog.py +0 -0
  110. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/cluster.py +0 -0
  111. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/config.py +0 -0
  112. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/container.py +0 -0
  113. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/dashboard.py +0 -0
  114. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/dict.py +0 -0
  115. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/environment.py +0 -0
  116. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/import_refs.py +0 -0
  117. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/launch.py +0 -0
  118. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/network_file_system.py +0 -0
  119. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/profile.py +0 -0
  120. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/__init__.py +0 -0
  121. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/run_jupyter.py +0 -0
  122. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/programs/vscode.py +0 -0
  123. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/queues.py +0 -0
  124. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/run.py +0 -0
  125. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/secret.py +0 -0
  126. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/shell.py +0 -0
  127. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/token.py +0 -0
  128. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/utils.py +0 -0
  129. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cli/volume.py +0 -0
  130. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/client.py +0 -0
  131. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cloud_bucket_mount.py +0 -0
  132. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cloud_bucket_mount.pyi +0 -0
  133. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cls.py +0 -0
  134. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/cls.pyi +0 -0
  135. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/config.py +0 -0
  136. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/container_process.py +0 -0
  137. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/container_process.pyi +0 -0
  138. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/dict.py +0 -0
  139. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/dict.pyi +0 -0
  140. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/environments.py +0 -0
  141. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/environments.pyi +0 -0
  142. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/__init__.py +0 -0
  143. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/flash.py +0 -0
  144. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/flash.pyi +0 -0
  145. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/experimental/ipython.py +0 -0
  146. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_io.py +0 -0
  147. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_io.pyi +0 -0
  148. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/file_pattern_matcher.py +0 -0
  149. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/functions.py +0 -0
  150. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/functions.pyi +0 -0
  151. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/image.py +0 -0
  152. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/image.pyi +0 -0
  153. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/io_streams.py +0 -0
  154. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/io_streams.pyi +0 -0
  155. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/mount.py +0 -0
  156. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/mount.pyi +0 -0
  157. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/network_file_system.py +0 -0
  158. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/network_file_system.pyi +0 -0
  159. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/object.py +0 -0
  160. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/object.pyi +0 -0
  161. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/output.py +0 -0
  162. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/parallel_map.py +0 -0
  163. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/parallel_map.pyi +0 -0
  164. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/partial_function.py +0 -0
  165. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/partial_function.pyi +0 -0
  166. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/proxy.py +0 -0
  167. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/proxy.pyi +0 -0
  168. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/py.typed +0 -0
  169. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/queue.py +0 -0
  170. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/queue.pyi +0 -0
  171. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/retries.py +0 -0
  172. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/runner.py +0 -0
  173. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/runner.pyi +0 -0
  174. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/running_app.py +0 -0
  175. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/schedule.py +0 -0
  176. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/scheduler_placement.py +0 -0
  177. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/secret.py +0 -0
  178. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/secret.pyi +0 -0
  179. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/server.py +0 -0
  180. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/server.pyi +0 -0
  181. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/serving.py +0 -0
  182. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/serving.pyi +0 -0
  183. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/snapshot.py +0 -0
  184. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/snapshot.pyi +0 -0
  185. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/stream_type.py +0 -0
  186. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/token_flow.py +0 -0
  187. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/token_flow.pyi +0 -0
  188. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/volume.py +0 -0
  189. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal/volume.pyi +0 -0
  190. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/dependency_links.txt +0 -0
  191. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/entry_points.txt +0 -0
  192. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/requires.txt +0 -0
  193. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal.egg-info/top_level.txt +0 -0
  194. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/__init__.py +0 -0
  195. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_cli_docs.py +0 -0
  196. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
  197. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_reference_docs.py +0 -0
  198. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
  199. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/__init__.py +0 -0
  200. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/mdmd.py +0 -0
  201. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_docs/mdmd/signatures.py +0 -0
  202. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/__init__.py +0 -0
  203. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/py.typed +0 -0
  204. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_grpc.py +0 -0
  205. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2.py +0 -0
  206. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
  207. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  208. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  209. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/modal_version/__main__.py +0 -0
  210. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/pyproject.toml +0 -0
  211. {modal-1.4.2.dev13 → modal-1.4.2.dev15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.2.dev13
3
+ Version: 1.4.2.dev15
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -18,6 +18,7 @@ from ..exception import (
18
18
  SandboxFilesystemIsADirectoryError,
19
19
  SandboxFilesystemNotADirectoryError,
20
20
  SandboxFilesystemNotFoundError,
21
+ SandboxFilesystemPathAlreadyExistsError,
21
22
  SandboxFilesystemPermissionError,
22
23
  ServiceError,
23
24
  )
@@ -188,6 +189,42 @@ def raise_remove_error(returncode: int, stderr: Union[str, bytes], remote_path:
188
189
  raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
189
190
 
190
191
 
192
+ def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
193
+ """Build the JSON command string for a MakeDirectory operation.
194
+
195
+ The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
196
+ Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
197
+ this schema like protobuf changes: fields must not be removed or renamed,
198
+ only added with backwards-compatible defaults.
199
+ """
200
+ return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
201
+
202
+
203
+ def raise_make_directory_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
204
+ if payload := try_parse_error_payload(stderr):
205
+ logger.debug(
206
+ f"sandbox-fs-tools make_directory error: path={remote_path}, "
207
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
208
+ )
209
+ if payload.error_kind == "NotFound":
210
+ raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
211
+ if payload.error_kind == "PathAlreadyExists":
212
+ raise SandboxFilesystemPathAlreadyExistsError(f"{payload.message}: {remote_path}")
213
+ if payload.error_kind == "NotDirectory":
214
+ raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
215
+ if payload.error_kind == "PermissionDenied":
216
+ raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
217
+ if payload.error_kind == "NotSupported":
218
+ raise InvalidError(
219
+ f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
220
+ )
221
+ raise SandboxFilesystemError(payload.message)
222
+
223
+ if stderr_text := _stderr_to_text(stderr):
224
+ logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
225
+ raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
226
+
227
+
191
228
  def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
192
229
  if not PurePosixPath(remote_path).is_absolute():
193
230
  raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
@@ -0,0 +1,136 @@
1
+ # Copyright Modal Labs 2026
2
+ import io
3
+ import shutil
4
+ import zipfile
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import click
9
+ import typer
10
+
11
+ from modal._utils.async_utils import synchronizer
12
+ from modal._utils.http_utils import _http_client_with_tls
13
+ from modal.client import _Client
14
+ from modal.config import logger
15
+ from modal.output import OutputManager
16
+ from modal_proto import api_pb2
17
+
18
+ from .logo import GREEN, print_logo
19
+ from .selector import Selector
20
+
21
+
22
+ @synchronizer.create_blocking
23
+ async def bootstrap(
24
+ name: Optional[str] = typer.Argument(None, help="The name of the template to load."),
25
+ output: str = typer.Option(".", "-o", "--output", help="Location for storing the template."),
26
+ force: bool = typer.Option(False, "--force", help="Overwrite the output directory if it already exists."),
27
+ ) -> None:
28
+ """Initialize a sample Modal App."""
29
+ client = await _Client.from_env()
30
+ resp = await client.stub.TemplateList(api_pb2.TemplateListRequest())
31
+ names = sorted([item.name for item in resp.items])
32
+
33
+ output_manager = OutputManager.get()
34
+ print_logo()
35
+
36
+ if name is None:
37
+ if not names:
38
+ output_manager.print("No templates available. Try updating modal.")
39
+ raise typer.Exit(1)
40
+ try:
41
+ selector = Selector(names, title="Select a template", highlight_style=f"bold {GREEN}")
42
+ name = selector.run()
43
+ except Exception:
44
+ output_manager.print("Available templates:")
45
+ for n in names:
46
+ output_manager.print(f" - {n}")
47
+ output_manager.print("\nRun `modal bootstrap <name>` to select a template.")
48
+ raise typer.Exit(0)
49
+
50
+ item = next((item for item in resp.items if item.name == name), None)
51
+ if item is None:
52
+ output_manager.print(f"Unknown template: {name}")
53
+ output_manager.print(f"Available templates: {', '.join(names)}")
54
+ raise typer.Exit(1)
55
+
56
+ template_root = Path(output)
57
+ template_root.mkdir(parents=True, exist_ok=True)
58
+ template_written_to_cwd = template_root.resolve() == Path.cwd().resolve()
59
+
60
+ dest = template_root / name
61
+ if dest.exists() and not force:
62
+ raise click.UsageError(f"Output path '{dest}' already exists. Use --force to overwrite.")
63
+
64
+ # Download the repo archive
65
+ ref = item.ref or "main"
66
+ archive_url = item.repo.rstrip("/") + f"/archive/refs/heads/{ref}.zip"
67
+
68
+ with output_manager.status(f"Downloading template '{name}'..."):
69
+ try:
70
+ session = _http_client_with_tls(timeout=30)
71
+ try:
72
+ async with session.get(archive_url) as resp_http:
73
+ resp_http.raise_for_status()
74
+ data = await resp_http.read()
75
+ finally:
76
+ await session.close()
77
+ except Exception as e:
78
+ output_manager.print(f"Failed to download template from {archive_url}: {e}")
79
+ raise typer.Exit(1)
80
+
81
+ try:
82
+ zf = zipfile.ZipFile(io.BytesIO(data))
83
+ all_names = zf.namelist()
84
+ root_prefix = all_names[0].split("/")[0] + "/"
85
+ except Exception as e:
86
+ logger.debug(f"Failed to parse template archive: {e}")
87
+ output_manager.print(f"Unable to fetch template '{name}'.")
88
+ raise typer.Exit(1)
89
+
90
+ with zf:
91
+ template_prefix = root_prefix + name + "/"
92
+ logger.debug(f"Zip root: {root_prefix!r}, template: {template_prefix!r}, entries: {len(all_names)}")
93
+ for entry in all_names[:20]:
94
+ logger.debug(f" {entry}")
95
+
96
+ members = [m for m in all_names if m.startswith(template_prefix) and m != template_prefix]
97
+ logger.debug(f"Matched {len(members)} members under {template_prefix!r}")
98
+
99
+ if not members:
100
+ output_manager.print(f"Template '{name}' not found in repository.")
101
+ raise typer.Exit(1)
102
+
103
+ if dest.exists() and force:
104
+ shutil.rmtree(dest)
105
+
106
+ dest.mkdir(parents=True, exist_ok=True)
107
+
108
+ resolved_dest = dest.resolve()
109
+ for member in members:
110
+ rel_path = member[len(template_prefix) :]
111
+ out_path = (dest / rel_path).resolve()
112
+ if not out_path.is_relative_to(resolved_dest):
113
+ logger.debug(f"Zip entry escapes destination: {member!r}")
114
+ output_manager.print(f"Unable to fetch template '{name}'.")
115
+ raise typer.Exit(1)
116
+ if member.endswith("/"):
117
+ out_path.mkdir(parents=True, exist_ok=True)
118
+ else:
119
+ out_path.parent.mkdir(parents=True, exist_ok=True)
120
+ out_path.write_bytes(zf.read(member))
121
+
122
+ output_manager.print(f"[{GREEN}]✓[/{GREEN}] Template '{name}' written to {dest}", highlight=False)
123
+ output_manager.print(f"[{GREEN}]→[/{GREEN}] To see it in action, run the following command:")
124
+ amp = " [dim white]&&[/dim white] "
125
+ if template_written_to_cwd:
126
+ steps = []
127
+ else:
128
+ steps = [f"[#ff8de6]cd {template_root}[/#ff8de6]"]
129
+ steps.extend(
130
+ [
131
+ f"[{GREEN}]modal deploy -m {name}.app[/{GREEN}]",
132
+ f"[#91c8ef]python -m {name}.try[/#91c8ef]",
133
+ ]
134
+ )
135
+ command = amp.join(steps)
136
+ output_manager.print(command)
@@ -11,6 +11,7 @@ from modal.output import OutputManager
11
11
  from . import run, shell as shell_module
12
12
  from .app import app_cli
13
13
  from .billing import billing_cli
14
+ from .bootstrap import bootstrap
14
15
  from .changelog import changelog
15
16
  from .cluster import cluster_cli
16
17
  from .config import config_cli
@@ -19,6 +20,7 @@ from .dashboard import dashboard
19
20
  from .dict import dict_cli
20
21
  from .environment import environment_cli
21
22
  from .launch import launch_cli
23
+ from .logo import print_logo
22
24
  from .network_file_system import nfs_cli
23
25
  from .profile import profile_cli
24
26
  from .queues import queue_cli
@@ -88,28 +90,7 @@ def check_path():
88
90
  @synchronizer.create_blocking
89
91
  async def setup(profile: Optional[str] = None):
90
92
  check_path()
91
-
92
- art = """
93
- ############# #############
94
- #### ## #### ##
95
- ## ## ## ## ## ##
96
- ## ## ## ## ## ##
97
- ## ## #### ## ##
98
- ## ############# ## ##
99
- ## ## #### ## ##
100
- ## ## ## ## ## ##
101
- ## ## ## ## ## ##
102
- ## ## ## ## ## ##
103
- ## ## ## ## ## ##
104
- ## ## ## ## #############
105
- ## ## ## ## ## ##
106
- ## ## ## ## ## ##
107
- ## ## ## ## ## ##
108
- #### ## #### ##
109
- ############# #############
110
- """
111
-
112
- OutputManager.get().print(art, highlight=False, style="green")
93
+ print_logo()
113
94
 
114
95
  # Fetch a new token (same as `modal token new` but redirect to /home once finishes)
115
96
  await _new_token(profile=profile, next_url="/home")
@@ -145,8 +126,11 @@ entrypoint_cli_typer.add_typer(billing_cli, rich_help_panel="Observability")
145
126
  entrypoint_cli_typer.command("changelog", rich_help_panel="Observability")(changelog)
146
127
  entrypoint_cli_typer.command("dashboard", rich_help_panel="Observability")(dashboard)
147
128
 
148
- # Hide setup from help as it's redundant with modal token new, but nicer for onboarding
149
- entrypoint_cli_typer.command("setup", help="Bootstrap Modal's configuration.", rich_help_panel="Onboarding")(setup)
129
+ # Onboarding
130
+ entrypoint_cli_typer.command("setup", help="Get started using Modal.", rich_help_panel="Onboarding")(setup)
131
+ entrypoint_cli_typer.command("bootstrap", help="Initialize a sample Modal App.", rich_help_panel="Onboarding")(
132
+ bootstrap
133
+ )
150
134
 
151
135
  # Special handling for modal run, which is more complicated
152
136
  entrypoint_cli = typer.main.get_command(entrypoint_cli_typer)
@@ -0,0 +1,70 @@
1
+ # Copyright Modal Labs 2026
2
+ import sys
3
+
4
+ from modal.output import OutputManager
5
+
6
+ GREEN = "#7FEE64"
7
+
8
+ ASCII_LOGO = """
9
+ ############# #############
10
+ #### ## #### ##
11
+ ## ## ## ## ## ##
12
+ ## ## ## ## ## ##
13
+ ## ## #### ## ##
14
+ ## ############# ## ##
15
+ ## ## #### ## ##
16
+ ## ## ## ## ## ##
17
+ ## ## ## ## ## ##
18
+ ## ## ## ## ## ##
19
+ ## ## ## ## ## ##
20
+ ## ## ## ## #############
21
+ ## ## ## ## ## ##
22
+ ## ## ## ## ## ##
23
+ ## ## ## ## ## ##
24
+ #### ## #### ##
25
+ ############# #############
26
+ """
27
+
28
+ UNICODE_LOGO_LARGE = """
29
+ █████████████ █████████████
30
+ ████ ██ ████ ██
31
+ ██ ██ ██ ██ ██ ██
32
+ ██ ██ ██ ██ ██ ██
33
+ ██ ██ ████ ██ ██
34
+ ██ █████████████ ██ ██
35
+ ██ ██ ████ ██ ██
36
+ ██ ██ ██ ██ ██ ██
37
+ ██ ██ ██ ██ ██ ██
38
+ ██ ██ ██ ██ ██ ██
39
+ ██ ██ ██ ██ ██ ██
40
+ ██ ██ ██ ██ █████████████
41
+ ██ ██ ██ ██ ██ ██
42
+ ██ ██ ██ ██ ██ ██
43
+ ██ ██ ██ ██ ██ ██
44
+ ████ ██ ████ ██
45
+ █████████████ █████████████
46
+ """
47
+
48
+ UNICODE_LOGO_SMALL = """
49
+ ▟█▀▀▀▀▜▖ ▗█▛▀▀▀▀▙
50
+ ▟▘▝▙ ▜▖▗▛ ▜▖ ▝▙
51
+ ▟▘ ▝▙▄▄▄▄█▛ ▜▖ ▝▙
52
+ ▟▘ ▟▘ ▗▛▜▖ ▜▖ ▝▙
53
+ ▟▘ ▟▘ ▗▛ ▜▖ ▜▖ ▝▙
54
+ ▟▘ ▟▘ ▗▛ ▜▖ ▜▄▄▄▄▟▙
55
+ ▝▙ ▟▘ ▗▛ ▜▖ ▗▛ ▟▘
56
+ ▝▙▟▘ ▗▛ ▜▄▛ ▟▘
57
+ ▝▀▀▀▀▀▀ ▀▀▀▀▀▀▘
58
+ """
59
+
60
+
61
+ def get_logo():
62
+ if sys.stdout.encoding and sys.stdout.encoding.lower().startswith("utf"):
63
+ return UNICODE_LOGO_SMALL
64
+ else:
65
+ return ASCII_LOGO
66
+
67
+
68
+ def print_logo():
69
+ if sys.stdout.isatty():
70
+ OutputManager.get().print(get_logo(), highlight=False, style=GREEN)
@@ -19,6 +19,13 @@ def _cbreak_terminal():
19
19
  old_settings = termios.tcgetattr(fd)
20
20
  try:
21
21
  tty.setcbreak(fd, termios.TCSADRAIN)
22
+ # Disable ISIG so Ctrl-C is delivered as \x03 to os.read() rather than
23
+ # generating an asynchronous SIGINT. This lets _input_loop raise
24
+ # KeyboardInterrupt synchronously, ensuring context-manager cleanup
25
+ # (cursor visibility, terminal echo) completes without interruption.
26
+ attrs = termios.tcgetattr(fd)
27
+ attrs[3] &= ~termios.ISIG
28
+ termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
22
29
  yield
23
30
  finally:
24
31
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
@@ -44,10 +51,10 @@ class Selector:
44
51
  exiting.
45
52
  """
46
53
 
47
- def __init__(self, options: list[str], title: str = "Select an option"):
54
+ def __init__(self, options: list[str], title: str = "Select an option", highlight_style: str = "bold green"):
48
55
  if not options:
49
56
  raise ValueError("options must not be empty")
50
- if not sys.stdin.isatty() or not sys.stdout.isatty():
57
+ if not sys.stdin.isatty() or not sys.stdout.isatty() or Console().is_dumb_terminal:
51
58
  raise RuntimeError("Interactive selection requires a TTY")
52
59
  # Fail fast on platforms without termios (e.g. Windows).
53
60
  import termios # noqa: F401
@@ -55,6 +62,7 @@ class Selector:
55
62
  self.options = options
56
63
  self.title = title
57
64
  self.selected = 0
65
+ self.highlight_style = highlight_style
58
66
 
59
67
  # -- state manipulation ---------------------------------------------------
60
68
 
@@ -75,8 +83,8 @@ class Selector:
75
83
  text.append(f"{self.title}\n\n", style="bold")
76
84
  for i, opt in enumerate(self.options):
77
85
  if i == self.selected:
78
- text.append(" > ", style="bold green")
79
- text.append(f"{opt}\n", style="bold green")
86
+ text.append(" > ", style=self.highlight_style)
87
+ text.append(f"{opt}\n", style=self.highlight_style)
80
88
  else:
81
89
  text.append(f" {opt}\n")
82
90
  return text
@@ -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.2.dev13",
38
+ version: str = "1.4.2.dev15",
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.2.dev13",
178
+ version: str = "1.4.2.dev15",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -379,3 +379,9 @@ class SandboxFilesystemFileTooLargeError(SandboxFilesystemError):
379
379
  """Raised when a file exceeds the maximum allowed size for a read operation in the sandbox."""
380
380
 
381
381
  pass
382
+
383
+
384
+ class SandboxFilesystemPathAlreadyExistsError(SandboxFilesystemError):
385
+ """Raised when a path already exists and the operation requires it to be absent."""
386
+
387
+ pass
@@ -1110,7 +1110,12 @@ class _Sandbox(_Object, type_prefix="sb"):
1110
1110
 
1111
1111
  req = api_pb2.SandboxGetTunnelsRequest(sandbox_id=self.object_id, timeout=timeout)
1112
1112
  stub = self._client.stub
1113
- resp = await (stub.SandboxGetTunnelsV2(req) if self._is_v2 else stub.SandboxGetTunnels(req))
1113
+ if self._is_v2:
1114
+ assert self._client._auth_token_manager
1115
+ auth_token = await self._client._auth_token_manager.get_token()
1116
+ resp = await stub.SandboxGetTunnelsV2(req, metadata=[("x-modal-auth-token", auth_token)])
1117
+ else:
1118
+ resp = await stub.SandboxGetTunnels(req)
1114
1119
 
1115
1120
  # If we couldn't get the tunnels in time, report the timeout.
1116
1121
  if resp.result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
@@ -1179,7 +1184,12 @@ class _Sandbox(_Object, type_prefix="sb"):
1179
1184
  This is a no-op if the Sandbox has already finished running."""
1180
1185
  req = api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id)
1181
1186
  stub = self._client.stub
1182
- await (stub.SandboxTerminateV2(req) if self._is_v2 else stub.SandboxTerminate(req))
1187
+ if self._is_v2:
1188
+ assert self._client._auth_token_manager
1189
+ auth_token = await self._client._auth_token_manager.get_token()
1190
+ await stub.SandboxTerminateV2(req, metadata=[("x-modal-auth-token", auth_token)])
1191
+ else:
1192
+ await stub.SandboxTerminate(req)
1183
1193
  if wait:
1184
1194
  await self.wait(raise_on_termination=False)
1185
1195
  return self.returncode
@@ -1208,7 +1218,12 @@ class _Sandbox(_Object, type_prefix="sb"):
1208
1218
  while not self._task_id:
1209
1219
  req = api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
1210
1220
  stub = self._client.stub
1211
- resp = await (stub.SandboxGetTaskIdV2(req) if self._is_v2 else stub.SandboxGetTaskId(req))
1221
+ if self._is_v2:
1222
+ assert self._client._auth_token_manager
1223
+ auth_token = await self._client._auth_token_manager.get_token()
1224
+ resp = await stub.SandboxGetTaskIdV2(req, metadata=[("x-modal-auth-token", auth_token)])
1225
+ else:
1226
+ resp = await stub.SandboxGetTaskId(req)
1212
1227
  if not resp.task_id and raise_if_task_complete and resp.HasField("task_result"):
1213
1228
  msg = resp.task_result.exception or "Sandbox already finished"
1214
1229
  raise Error(msg)
@@ -1632,14 +1647,30 @@ class _Sandbox(_Object, type_prefix="sb"):
1632
1647
  return await ls(path, self._client, task_id)
1633
1648
 
1634
1649
  async def mkdir(self, path: str, parents: bool = False) -> None:
1635
- """[Alpha] Create a new directory in the Sandbox."""
1650
+ """[Alpha] Create a new directory in the Sandbox.
1651
+
1652
+ .. deprecated:: 2026-04-15
1653
+ """
1636
1654
  self._ensure_v1("mkdir")
1655
+ deprecation_warning(
1656
+ (2026, 4, 15),
1657
+ "`Sandbox.mkdir()` is deprecated. Use the `Sandbox.filesystem` APIs instead.",
1658
+ pending=True,
1659
+ )
1637
1660
  task_id = await self._get_task_id()
1638
1661
  return await mkdir(path, self._client, task_id, parents)
1639
1662
 
1640
1663
  async def rm(self, path: str, recursive: bool = False) -> None:
1641
- """[Alpha] Remove a file or directory in the Sandbox."""
1664
+ """[Alpha] Remove a file or directory in the Sandbox.
1665
+
1666
+ .. deprecated:: 2026-04-15
1667
+ """
1642
1668
  self._ensure_v1("rm")
1669
+ deprecation_warning(
1670
+ (2026, 4, 15),
1671
+ "`Sandbox.rm()` is deprecated. Use the `Sandbox.filesystem` APIs instead.",
1672
+ pending=True,
1673
+ )
1643
1674
  task_id = await self._get_task_id()
1644
1675
  return await rm(path, self._client, task_id, recursive)
1645
1676
 
@@ -617,11 +617,17 @@ class _Sandbox(modal._object._Object):
617
617
  ...
618
618
 
619
619
  async def mkdir(self, path: str, parents: bool = False) -> None:
620
- """[Alpha] Create a new directory in the Sandbox."""
620
+ """[Alpha] Create a new directory in the Sandbox.
621
+
622
+ .. deprecated:: 2026-04-15
623
+ """
621
624
  ...
622
625
 
623
626
  async def rm(self, path: str, recursive: bool = False) -> None:
624
- """[Alpha] Remove a file or directory in the Sandbox."""
627
+ """[Alpha] Remove a file or directory in the Sandbox.
628
+
629
+ .. deprecated:: 2026-04-15
630
+ """
625
631
  ...
626
632
 
627
633
  def watch(
@@ -1911,22 +1917,34 @@ class Sandbox(modal.object.Object):
1911
1917
 
1912
1918
  class __mkdir_spec(typing_extensions.Protocol):
1913
1919
  def __call__(self, /, path: str, parents: bool = False) -> None:
1914
- """[Alpha] Create a new directory in the Sandbox."""
1920
+ """[Alpha] Create a new directory in the Sandbox.
1921
+
1922
+ .. deprecated:: 2026-04-15
1923
+ """
1915
1924
  ...
1916
1925
 
1917
1926
  async def aio(self, /, path: str, parents: bool = False) -> None:
1918
- """[Alpha] Create a new directory in the Sandbox."""
1927
+ """[Alpha] Create a new directory in the Sandbox.
1928
+
1929
+ .. deprecated:: 2026-04-15
1930
+ """
1919
1931
  ...
1920
1932
 
1921
1933
  mkdir: __mkdir_spec
1922
1934
 
1923
1935
  class __rm_spec(typing_extensions.Protocol):
1924
1936
  def __call__(self, /, path: str, recursive: bool = False) -> None:
1925
- """[Alpha] Remove a file or directory in the Sandbox."""
1937
+ """[Alpha] Remove a file or directory in the Sandbox.
1938
+
1939
+ .. deprecated:: 2026-04-15
1940
+ """
1926
1941
  ...
1927
1942
 
1928
1943
  async def aio(self, /, path: str, recursive: bool = False) -> None:
1929
- """[Alpha] Remove a file or directory in the Sandbox."""
1944
+ """[Alpha] Remove a file or directory in the Sandbox.
1945
+
1946
+ .. deprecated:: 2026-04-15
1947
+ """
1930
1948
  ...
1931
1949
 
1932
1950
  rm: __rm_spec
@@ -11,9 +11,11 @@ from typing import Union, cast
11
11
  from ._utils.async_utils import synchronize_api
12
12
  from ._utils.logger import logger
13
13
  from ._utils.sandbox_fs_utils import (
14
+ make_make_directory_command,
14
15
  make_read_file_command,
15
16
  make_remove_command,
16
17
  make_write_file_command,
18
+ raise_make_directory_error,
17
19
  raise_read_file_error,
18
20
  raise_remove_error,
19
21
  raise_write_file_error,
@@ -338,7 +340,7 @@ class _SandboxFilesystem:
338
340
  To remove a directory and all its contents:
339
341
 
340
342
  ```python fixture:sandbox
341
- sandbox.exec("mkdir", "-p", "/tmp/mydir/subdir").wait()
343
+ sandbox.filesystem.make_directory("/tmp/mydir/subdir")
342
344
  sandbox.filesystem.remove("/tmp/mydir", recursive=True)
343
345
  ```
344
346
  """
@@ -351,5 +353,40 @@ class _SandboxFilesystem:
351
353
  if returncode != 0:
352
354
  raise_remove_error(returncode, stderr, remote_path)
353
355
 
356
+ async def make_directory(self, remote_path: str, *, create_parents: bool = True) -> None:
357
+ """Create a new directory in the Sandbox.
358
+
359
+ `remote_path` must be an absolute path in the Sandbox.
360
+
361
+ When `create_parents` is `True` (the default), any missing parent directories are created and the call is
362
+ idempotent (succeeds silently if the directory already exists). When `create_parents` is `False`, the
363
+ immediate parent directory must already exist and the path must not already exist.
364
+
365
+ **Raises**
366
+
367
+ - `SandboxFilesystemNotFoundError`: the parent directory does not exist and `create_parents` is `False`.
368
+ - `SandboxFilesystemPathAlreadyExistsError`: the path already exists.
369
+ - `SandboxFilesystemNotADirectoryError`: a path component is not a directory.
370
+ - `SandboxFilesystemPermissionError`: creation is not permitted.
371
+ - `InvalidError`: the operation is not supported by the mount.
372
+ - `SandboxFilesystemError`: the command fails for any other reason.
373
+
374
+ **Usage**
375
+
376
+ ```python fixture:sandbox
377
+ sandbox.filesystem.make_directory("/tmp/a/b/c")
378
+ ```
379
+ """
380
+ validate_absolute_remote_path(remote_path, "make_directory")
381
+
382
+ with translate_exec_errors("make_directory", remote_path):
383
+ process = await self._sandbox.exec(
384
+ _SANDBOX_FS_TOOLS_PATH, make_make_directory_command(remote_path, create_parents)
385
+ )
386
+ stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
387
+
388
+ if returncode != 0:
389
+ raise_make_directory_error(returncode, stderr, remote_path)
390
+
354
391
 
355
392
  SandboxFilesystem = synchronize_api(_SandboxFilesystem)