modal 1.4.3.dev11__tar.gz → 1.4.3.dev12__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 (212) hide show
  1. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/PKG-INFO +1 -1
  2. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/execution_context.py +6 -3
  3. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/execution_context.pyi +5 -3
  4. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/sandbox_fs_utils.py +30 -0
  5. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/app.py +2 -1
  6. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/app.pyi +3 -0
  7. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/_help.py +79 -48
  8. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/run.py +8 -1
  9. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/utils.py +4 -5
  10. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/client.pyi +2 -2
  11. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/functions.pyi +6 -6
  12. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/runner.py +4 -1
  13. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/runner.pyi +3 -0
  14. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/sandbox_fs.py +53 -0
  15. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/sandbox_fs.pyi +72 -0
  16. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/serving.py +4 -1
  17. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/serving.pyi +3 -0
  18. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/volume.py +32 -4
  19. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/volume.pyi +9 -0
  20. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/PKG-INFO +1 -1
  21. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_version/__init__.py +1 -1
  22. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/LICENSE +0 -0
  23. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/README.md +0 -0
  24. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/__init__.py +0 -0
  25. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/__main__.py +0 -0
  26. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_billing.py +0 -0
  27. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_clustered_functions.py +0 -0
  28. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_clustered_functions.pyi +0 -0
  29. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_container_entrypoint.py +0 -0
  30. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_functions.py +0 -0
  31. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_grpc_client.py +0 -0
  32. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_ipython.py +0 -0
  33. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_load_context.py +0 -0
  34. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_location.py +0 -0
  35. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_logs.py +0 -0
  36. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_object.py +0 -0
  37. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_output/__init__.py +0 -0
  38. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_output/manager.py +0 -0
  39. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_output/pty.py +0 -0
  40. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_output/rich.py +0 -0
  41. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_output/status.py +0 -0
  42. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_partial_function.py +0 -0
  43. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_resolver.py +0 -0
  44. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_resources.py +0 -0
  45. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/__init__.py +0 -0
  46. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/asgi.py +0 -0
  47. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/container_io_manager.py +0 -0
  48. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
  49. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  50. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/telemetry.py +0 -0
  51. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
  52. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_runtime/user_code_imports.py +0 -0
  53. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_serialization.py +0 -0
  54. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_server.py +0 -0
  55. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_traceback.py +0 -0
  56. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_tunnel.py +0 -0
  57. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_tunnel.pyi +0 -0
  58. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_type_manager.py +0 -0
  59. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/__init__.py +0 -0
  60. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/app_utils.py +0 -0
  61. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/async_utils.py +0 -0
  62. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/auth_token_manager.py +0 -0
  63. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/blob_utils.py +0 -0
  64. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/browser_utils.py +0 -0
  65. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
  66. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/deprecation.py +0 -0
  67. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/docker_utils.py +0 -0
  68. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/function_utils.py +0 -0
  69. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/git_utils.py +0 -0
  70. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/grpc_testing.py +0 -0
  71. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/grpc_utils.py +0 -0
  72. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/hash_utils.py +0 -0
  73. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/http_utils.py +0 -0
  74. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/jwt_utils.py +0 -0
  75. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/logger.py +0 -0
  76. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/mount_utils.py +0 -0
  77. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/name_utils.py +0 -0
  78. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/package_utils.py +0 -0
  79. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/pattern_utils.py +0 -0
  80. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/rand_pb_testing.py +0 -0
  81. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/shell_utils.py +0 -0
  82. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/task_command_router_client.py +0 -0
  83. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_utils/time_utils.py +0 -0
  84. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_vendor/__init__.py +0 -0
  85. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  86. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_vendor/cloudpickle.py +0 -0
  87. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_vendor/tblib.py +0 -0
  88. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_vendor/version.py +0 -0
  89. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/_watcher.py +0 -0
  90. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/billing.py +0 -0
  91. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/2023.12.312.txt +0 -0
  92. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/2023.12.txt +0 -0
  93. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/2024.04.txt +0 -0
  94. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/2024.10.txt +0 -0
  95. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/2025.06.txt +0 -0
  96. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/PREVIEW.txt +0 -0
  97. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/README.md +0 -0
  98. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/builder/base-images.json +0 -0
  99. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/call_graph.py +0 -0
  100. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/__init__.py +0 -0
  101. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/_download.py +0 -0
  102. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/_traceback.py +0 -0
  103. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/app.py +0 -0
  104. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/billing.py +0 -0
  105. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/bootstrap.py +0 -0
  106. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/changelog.py +0 -0
  107. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/cluster.py +0 -0
  108. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/config.py +0 -0
  109. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/container.py +0 -0
  110. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/dashboard.py +0 -0
  111. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/dict.py +0 -0
  112. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/entry_point.py +0 -0
  113. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/environment.py +0 -0
  114. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/import_refs.py +0 -0
  115. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/launch.py +0 -0
  116. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/logo.py +0 -0
  117. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/network_file_system.py +0 -0
  118. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/profile.py +0 -0
  119. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/programs/__init__.py +0 -0
  120. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/programs/run_jupyter.py +0 -0
  121. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/programs/vscode.py +0 -0
  122. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/queues.py +0 -0
  123. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/secret.py +0 -0
  124. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/selector.py +0 -0
  125. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/shell.py +0 -0
  126. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/token.py +0 -0
  127. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cli/volume.py +0 -0
  128. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/client.py +0 -0
  129. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cloud_bucket_mount.py +0 -0
  130. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cloud_bucket_mount.pyi +0 -0
  131. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cls.py +0 -0
  132. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/cls.pyi +0 -0
  133. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/config.py +0 -0
  134. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/container_process.py +0 -0
  135. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/container_process.pyi +0 -0
  136. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/dict.py +0 -0
  137. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/dict.pyi +0 -0
  138. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/environments.py +0 -0
  139. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/environments.pyi +0 -0
  140. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/exception.py +0 -0
  141. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/experimental/__init__.py +0 -0
  142. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/experimental/flash.py +0 -0
  143. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/experimental/flash.pyi +0 -0
  144. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/experimental/ipython.py +0 -0
  145. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/file_io.py +0 -0
  146. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/file_io.pyi +0 -0
  147. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/file_pattern_matcher.py +0 -0
  148. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/functions.py +0 -0
  149. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/image.py +0 -0
  150. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/image.pyi +0 -0
  151. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/io_streams.py +0 -0
  152. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/io_streams.pyi +0 -0
  153. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/mount.py +0 -0
  154. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/mount.pyi +0 -0
  155. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/network_file_system.py +0 -0
  156. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/network_file_system.pyi +0 -0
  157. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/object.py +0 -0
  158. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/object.pyi +0 -0
  159. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/output.py +0 -0
  160. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/parallel_map.py +0 -0
  161. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/parallel_map.pyi +0 -0
  162. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/partial_function.py +0 -0
  163. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/partial_function.pyi +0 -0
  164. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/proxy.py +0 -0
  165. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/proxy.pyi +0 -0
  166. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/py.typed +0 -0
  167. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/queue.py +0 -0
  168. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/queue.pyi +0 -0
  169. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/retries.py +0 -0
  170. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/running_app.py +0 -0
  171. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/sandbox.py +0 -0
  172. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/sandbox.pyi +0 -0
  173. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/schedule.py +0 -0
  174. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/scheduler_placement.py +0 -0
  175. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/secret.py +0 -0
  176. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/secret.pyi +0 -0
  177. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/server.py +0 -0
  178. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/server.pyi +0 -0
  179. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/snapshot.py +0 -0
  180. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/snapshot.pyi +0 -0
  181. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/stream_type.py +0 -0
  182. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/token_flow.py +0 -0
  183. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal/token_flow.pyi +0 -0
  184. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/SOURCES.txt +0 -0
  185. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/dependency_links.txt +0 -0
  186. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/entry_points.txt +0 -0
  187. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/requires.txt +0 -0
  188. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal.egg-info/top_level.txt +0 -0
  189. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/__init__.py +0 -0
  190. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/gen_cli_docs.py +0 -0
  191. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
  192. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/gen_reference_docs.py +0 -0
  193. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
  194. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/mdmd/__init__.py +0 -0
  195. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/mdmd/mdmd.py +0 -0
  196. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_docs/mdmd/signatures.py +0 -0
  197. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/__init__.py +0 -0
  198. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/api_grpc.py +0 -0
  199. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/api_pb2.py +0 -0
  200. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/api_pb2.pyi +0 -0
  201. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/api_pb2_grpc.py +0 -0
  202. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/api_pb2_grpc.pyi +0 -0
  203. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/modal_api_grpc.py +0 -0
  204. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/py.typed +0 -0
  205. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/task_command_router_grpc.py +0 -0
  206. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/task_command_router_pb2.py +0 -0
  207. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/task_command_router_pb2.pyi +0 -0
  208. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  209. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  210. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/modal_version/__main__.py +0 -0
  211. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/pyproject.toml +0 -0
  212. {modal-1.4.3.dev11 → modal-1.4.3.dev12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.3.dev11
3
+ Version: 1.4.3.dev12
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -10,10 +10,13 @@ from .container_io_manager import _ContainerIOManager
10
10
 
11
11
 
12
12
  def is_local() -> bool:
13
- """Returns if we are currently on the machine launching/deploying a Modal app
13
+ """Indicate the execution context of the current process.
14
+
15
+ Note: this function specifically returns False when the current process is
16
+ running a Modal Function and True in all other cases. It will return True
17
+ when called from a child process of a Function or inside a Modal Sandbox,
18
+ even though those processes are running on Modal hardware.
14
19
 
15
- Returns `True` when executed locally on the user's machine.
16
- Returns `False` when executed from a Modal container in the cloud.
17
20
  """
18
21
  return not _ContainerIOManager._singleton
19
22
 
@@ -4,10 +4,12 @@ import typing
4
4
  import typing_extensions
5
5
 
6
6
  def is_local() -> bool:
7
- """Returns if we are currently on the machine launching/deploying a Modal app
7
+ """Indicate the execution context of the current process.
8
8
 
9
- Returns `True` when executed locally on the user's machine.
10
- Returns `False` when executed from a Modal container in the cloud.
9
+ Note: this function specifically returns False when the current process is
10
+ running a Modal Function and True in all other cases. It will return True
11
+ when called from a child process of a Function or inside a Modal Sandbox,
12
+ even though those processes are running on Modal hardware.
11
13
  """
12
14
  ...
13
15
 
@@ -255,6 +255,36 @@ def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
255
255
  return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
256
256
 
257
257
 
258
+ def raise_stat_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
259
+ if payload := try_parse_error_payload(stderr):
260
+ logger.debug(
261
+ f"sandbox-fs-tools stat error: path={remote_path}, "
262
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
263
+ )
264
+ if payload.error_kind == "NotFound":
265
+ raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
266
+ if payload.error_kind == "NotDirectory":
267
+ raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
268
+ if payload.error_kind == "PermissionDenied":
269
+ raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
270
+ raise SandboxFilesystemError(payload.message)
271
+
272
+ if stderr_text := _stderr_to_text(stderr):
273
+ logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
274
+ raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
275
+
276
+
277
+ def make_stat_command(remote_path: str) -> str:
278
+ """Build the JSON command string for a Stat operation.
279
+
280
+ The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
281
+ Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
282
+ this schema like protobuf changes: fields must not be removed or renamed,
283
+ only added with backwards-compatible defaults.
284
+ """
285
+ return json.dumps({"Stat": {"path": remote_path}})
286
+
287
+
258
288
  def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
259
289
  if not PurePosixPath(remote_path).is_absolute():
260
290
  raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
@@ -379,6 +379,7 @@ class _App:
379
379
  async def run(
380
380
  self,
381
381
  *,
382
+ name: Optional[str] = None,
382
383
  client: Optional[_Client] = None,
383
384
  detach: bool = False,
384
385
  interactive: bool = False,
@@ -426,7 +427,7 @@ class _App:
426
427
  from .runner import _run_app # Defer import of runner.py, which imports a lot from Rich
427
428
 
428
429
  async with _run_app(
429
- self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
430
+ self, name=name, client=client, detach=detach, interactive=interactive, environment_name=environment_name
430
431
  ):
431
432
  yield self
432
433
 
@@ -256,6 +256,7 @@ class _App:
256
256
  def run(
257
257
  self,
258
258
  *,
259
+ name: typing.Optional[str] = None,
259
260
  client: typing.Optional[modal.client._Client] = None,
260
261
  detach: bool = False,
261
262
  interactive: bool = False,
@@ -864,6 +865,7 @@ class App:
864
865
  self,
865
866
  /,
866
867
  *,
868
+ name: typing.Optional[str] = None,
867
869
  client: typing.Optional[modal.client.Client] = None,
868
870
  detach: bool = False,
869
871
  interactive: bool = False,
@@ -913,6 +915,7 @@ class App:
913
915
  self,
914
916
  /,
915
917
  *,
918
+ name: typing.Optional[str] = None,
916
919
  client: typing.Optional[modal.client.Client] = None,
917
920
  detach: bool = False,
918
921
  interactive: bool = False,
@@ -5,11 +5,12 @@ from __future__ import annotations
5
5
 
6
6
  import inspect
7
7
  import os
8
+ import shutil
8
9
  import sys
9
10
  from typing import Any, Optional
10
11
 
11
12
  import click
12
- from rich.console import Console
13
+ from rich.console import Console, Group, RenderableType
13
14
  from rich.markdown import Markdown
14
15
  from rich.padding import Padding
15
16
  from rich.panel import Panel
@@ -24,6 +25,7 @@ _OPTION_METAVAR_STYLE = "dim"
24
25
  _ERROR_STYLE = "red"
25
26
 
26
27
  _MAX_HELP_WIDTH = 80
28
+ _HELP_PADDING = 1
27
29
 
28
30
 
29
31
  def use_rich_style() -> bool:
@@ -34,25 +36,24 @@ def use_rich_style() -> bool:
34
36
  return sys.stdout.isatty()
35
37
 
36
38
 
37
- def _make_console(file: Optional[Any] = None) -> Console:
38
- return Console(file=file, highlight=False)
39
+ def _make_help_console() -> Console:
40
+ columns, _ = shutil.get_terminal_size()
41
+ return Console(highlight=False, width=min(_MAX_HELP_WIDTH, columns))
39
42
 
40
43
 
41
- def _render_usage(cmd: click.Command, ctx: click.Context, console: Console) -> None:
44
+ def _build_usage(cmd: click.Command, ctx: click.Context) -> RenderableType:
42
45
  pieces = " ".join(cmd.collect_usage_pieces(ctx))
43
46
  usage = Text()
44
47
  usage.append("Usage: ", style=_HEADING_STYLE)
45
48
  usage.append(f"{ctx.command_path} {pieces}".rstrip())
46
- console.print(usage)
47
- console.print()
49
+ return usage
48
50
 
49
51
 
50
- def _render_help_text(cmd: click.Command, console: Console) -> None:
52
+ def _build_help_text(cmd: click.Command) -> Optional[RenderableType]:
51
53
  text = cmd.help or cmd.short_help or ""
52
54
  if not text:
53
- return
54
- console.print(Padding(Markdown(inspect.cleandoc(text)), (0, 2)))
55
- console.print()
55
+ return None
56
+ return Markdown(inspect.cleandoc(text))
56
57
 
57
58
 
58
59
  def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
@@ -73,7 +74,7 @@ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
73
74
  return text
74
75
 
75
76
 
76
- def _render_options(cmd: click.Command, ctx: click.Context, console: Console) -> None:
77
+ def _build_options(cmd: click.Command, ctx: click.Context) -> Optional[RenderableType]:
77
78
  rows: list[tuple[Text, str]] = []
78
79
  for param in cmd.get_params(ctx):
79
80
  rec = param.get_help_record(ctx)
@@ -81,22 +82,20 @@ def _render_options(cmd: click.Command, ctx: click.Context, console: Console) ->
81
82
  continue
82
83
  rows.append((_option_label(param, ctx), rec[1] or ""))
83
84
  if not rows:
84
- return
85
+ return None
85
86
 
86
- console.print(Text("Options", style=_HEADING_STYLE))
87
87
  table = Table(box=None, show_header=False, pad_edge=False, padding=(0, 2))
88
88
  table.add_column(no_wrap=True) # styling lives in the Text cells
89
89
  table.add_column(overflow="fold")
90
90
  for label, help_str in rows:
91
91
  table.add_row(label, help_str)
92
- console.print(table)
93
- console.print()
92
+ return Group(Text("Options", style=_HEADING_STYLE), table)
94
93
 
95
94
 
96
- def _render_epilog(cmd: click.Command, console: Console) -> None:
97
- if cmd.epilog:
98
- console.print(cmd.epilog)
99
- console.print()
95
+ def _build_epilog(cmd: click.Command) -> Optional[RenderableType]:
96
+ if not cmd.epilog:
97
+ return None
98
+ return Text(cmd.epilog)
100
99
 
101
100
 
102
101
  def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, click.Command]]]:
@@ -109,23 +108,25 @@ def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, cl
109
108
  return panels
110
109
 
111
110
 
112
- def _render_commands(group: click.Group, console: Console) -> None:
111
+ def _build_commands(group: click.Group, available_width: int) -> Optional[RenderableType]:
113
112
  panels = _group_commands_by_panel(group)
114
113
  if not panels:
115
- return
114
+ return None
116
115
 
117
116
  # We want the name / description columns to be the same widths across groups
118
117
  name_width = max(len(name) for items in panels.values() for name, _ in items)
119
- table_width = min(_MAX_HELP_WIDTH, console.width)
120
118
 
119
+ parts: list[RenderableType] = []
121
120
  for panel_name, items in panels.items():
122
- console.print(Text(panel_name.ljust(table_width), style=f"{_HEADING_STYLE} underline"))
123
- _render_command_table(console, items, name_width, table_width)
124
- console.print()
121
+ if parts:
122
+ parts.append(Text(""))
123
+ parts.append(Text(panel_name.ljust(available_width), style=f"{_HEADING_STYLE} underline"))
124
+ parts.append(_build_command_table(items, name_width, available_width))
125
+ return Group(*parts)
125
126
 
126
127
 
127
128
  def build_command_table(name_width: int, table_width: Optional[int] = None) -> Table:
128
- kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": False, "padding": (0, 2)}
129
+ kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": True, "padding": (0, 1)}
129
130
  if table_width is not None:
130
131
  kwargs["width"] = table_width
131
132
  table = Table(**kwargs)
@@ -137,16 +138,39 @@ def build_command_table(name_width: int, table_width: Optional[int] = None) -> T
137
138
  return table
138
139
 
139
140
 
140
- def _render_command_table(
141
- console: Console,
141
+ def _build_command_table(
142
142
  items: list[tuple[str, click.Command]],
143
143
  name_width: int,
144
144
  table_width: int,
145
- ) -> None:
145
+ ) -> Table:
146
146
  table = build_command_table(name_width, table_width)
147
147
  for name, sub in items:
148
148
  table.add_row(name, sub.get_short_help_str(limit=80))
149
- console.print(table)
149
+ return table
150
+
151
+
152
+ def _available_width(console: Console) -> int:
153
+ return min(_MAX_HELP_WIDTH, console.width) - _HELP_PADDING * 2
154
+
155
+
156
+ def _emit(
157
+ console: Console,
158
+ sections: list[Optional[RenderableType]],
159
+ formatter: click.HelpFormatter,
160
+ ) -> None:
161
+ parts: list[RenderableType] = []
162
+ for section in sections:
163
+ if section is None:
164
+ continue
165
+ if parts:
166
+ parts.append(Text(""))
167
+ parts.append(section)
168
+ if parts:
169
+ # Always pad with a blank line before the next prompt
170
+ parts.append(Text(""))
171
+ with console.capture() as capture:
172
+ console.print(Padding(Group(*parts), (0, _HELP_PADDING)))
173
+ formatter.write(capture.get())
150
174
 
151
175
 
152
176
  # -- Public Command / Group subclasses ---------------------------------------
@@ -162,13 +186,17 @@ class ModalCommand(click.Command):
162
186
  def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
163
187
  if not use_rich_style():
164
188
  return super().format_help(ctx, formatter)
165
- console = _make_console()
166
- with console.capture() as capture:
167
- _render_usage(self, ctx, console)
168
- _render_help_text(self, console)
169
- _render_options(self, ctx, console)
170
- _render_epilog(self, console)
171
- formatter.write(capture.get())
189
+ console = _make_help_console()
190
+ _emit(
191
+ console,
192
+ [
193
+ _build_usage(self, ctx),
194
+ _build_help_text(self),
195
+ _build_options(self, ctx),
196
+ _build_epilog(self),
197
+ ],
198
+ formatter,
199
+ )
172
200
 
173
201
 
174
202
  class ModalGroup(click.Group):
@@ -209,22 +237,25 @@ class ModalGroup(click.Group):
209
237
  def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
210
238
  if not use_rich_style():
211
239
  return super().format_help(ctx, formatter)
212
- console = _make_console()
213
- with console.capture() as capture:
214
- _render_usage(self, ctx, console)
215
- _render_help_text(self, console)
216
- _render_options(self, ctx, console)
217
- _render_commands(self, console)
218
- _render_epilog(self, console)
219
- formatter.write(capture.get())
240
+ console = _make_help_console()
241
+ _emit(
242
+ console,
243
+ [
244
+ _build_usage(self, ctx),
245
+ _build_help_text(self),
246
+ _build_options(self, ctx),
247
+ _build_commands(self, _available_width(console)),
248
+ _build_epilog(self),
249
+ ],
250
+ formatter,
251
+ )
220
252
 
221
253
 
222
254
  # -- Error rendering ---------------------------------------------------------
223
255
 
224
256
 
225
257
  def _render_click_exception(exc: click.ClickException, file: Any) -> None:
226
- console = _make_console(file if file is not None else sys.stderr)
227
- title = "Error"
258
+ console = Console(file=file if file is not None else sys.stderr, highlight=False)
228
259
 
229
260
  if isinstance(exc, click.UsageError) and exc.ctx is not None:
230
261
  ctx = exc.ctx
@@ -236,7 +267,7 @@ def _render_click_exception(exc: click.ClickException, file: Any) -> None:
236
267
  console.print(
237
268
  Panel(
238
269
  Text(exc.format_message()),
239
- title=title,
270
+ title="Error",
240
271
  title_align="left",
241
272
  border_style=_ERROR_STYLE,
242
273
  expand=True,
@@ -235,6 +235,7 @@ def _make_click_function(app, signature: CliRunnableSignature, inner: Callable[[
235
235
  output_mgr.set_timestamps(ctx.obj["show_timestamps"])
236
236
  with run_app(
237
237
  app,
238
+ name=ctx.obj["name"],
238
239
  detach=ctx.obj["detach"],
239
240
  environment_name=ctx.obj["env"],
240
241
  interactive=ctx.obj["interactive"],
@@ -365,6 +366,7 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
365
366
  output_mgr.set_timestamps(ctx.obj["show_timestamps"])
366
367
  with run_app(
367
368
  app,
369
+ name=ctx.obj["name"],
368
370
  detach=ctx.obj["detach"],
369
371
  environment_name=ctx.obj["env"],
370
372
  interactive=ctx.obj["interactive"],
@@ -448,6 +450,7 @@ class RunGroup(ModalGroup):
448
450
  cls=RunGroup,
449
451
  subcommand_metavar="FUNC_REF",
450
452
  )
453
+ @click.option("-n", "--name", help="Name for this run of the App.")
451
454
  @click.option("-w", "--write-result", help="Write return value (which must be str or bytes) to this local path.")
452
455
  @click.option("-q", "--quiet", is_flag=True, help="Don't show Modal progress indicators.")
453
456
  @click.option("-d", "--detach", is_flag=True, help="Don't stop the app if the local process dies or disconnects.")
@@ -458,6 +461,7 @@ class RunGroup(ModalGroup):
458
461
  @click.pass_context
459
462
  def run(
460
463
  ctx: click.Context,
464
+ name: Optional[str],
461
465
  write_result: Optional[str],
462
466
  detach: bool,
463
467
  quiet: bool,
@@ -498,6 +502,7 @@ def run(
498
502
  ```
499
503
  """
500
504
  ctx.ensure_object(dict)
505
+ ctx.obj["name"] = name
501
506
  ctx.obj["result_path"] = write_result
502
507
  ctx.obj["detach"] = detach # if subcommand would be a click command...
503
508
  ctx.obj["show_progress"] = False if quiet else True
@@ -573,6 +578,7 @@ def deploy(
573
578
 
574
579
  @click.command("serve", cls=ModalCommand, no_args_is_help=True)
575
580
  @click.argument("app_ref")
581
+ @click.option("-n", "--name", help="Name for this run of the App.")
576
582
  @click.option("--timeout", default=None, type=float)
577
583
  @click.option("-e", "--env", default=None, help=ENV_OPTION_HELP)
578
584
  @click.option(
@@ -585,6 +591,7 @@ def deploy(
585
591
  @click.option("--timestamps", is_flag=True, default=False, help="Show timestamps for each log line.")
586
592
  def serve(
587
593
  app_ref: str,
594
+ name: Optional[str] = None,
588
595
  timeout: Optional[float] = None,
589
596
  env: Optional[str] = None,
590
597
  use_module_mode: bool = False,
@@ -611,7 +618,7 @@ def serve(
611
618
  app = import_app_from_ref(import_ref, base_cmd="modal serve")
612
619
  if app.description is None:
613
620
  app.set_description(_get_clean_app_description(app_ref))
614
- with serve_app(app, import_ref, environment_name=env):
621
+ with serve_app(app, import_ref, name=name, environment_name=env):
615
622
  if timeout is None:
616
623
  timeout = config["serve_timeout"]
617
624
  if timeout is None:
@@ -162,11 +162,10 @@ def display_table(
162
162
  output.print(table)
163
163
 
164
164
 
165
- ENV_OPTION_HELP = """Environment to interact with.
166
-
167
- If not specified, Modal will use the default environment of your current profile, or the `MODAL_ENVIRONMENT` variable.
168
- Otherwise, raises an error if the workspace has multiple environments.
169
- """
165
+ ENV_OPTION_HELP = (
166
+ "Environment to interact with. If unspecified, defers to `MODAL_ENVIRONMENT`, "
167
+ "your active local profile, or your workspace default, in that order."
168
+ )
170
169
 
171
170
 
172
171
  def env_option(func):
@@ -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.dev11",
38
+ version: str = "1.4.3.dev12",
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.dev11",
178
+ version: str = "1.4.3.dev12",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -347,7 +347,7 @@ class Function(
347
347
 
348
348
  _call_generator: ___call_generator_spec
349
349
 
350
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
350
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
351
351
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
352
352
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
353
353
  ...
@@ -356,7 +356,7 @@ class Function(
356
356
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
357
357
  ...
358
358
 
359
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
359
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
360
360
 
361
361
  class __remote_gen_spec(typing_extensions.Protocol):
362
362
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -383,7 +383,7 @@ class Function(
383
383
  """
384
384
  ...
385
385
 
386
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
386
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
387
387
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
388
388
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
389
389
 
@@ -406,7 +406,7 @@ class Function(
406
406
  """
407
407
  ...
408
408
 
409
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
409
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
410
410
 
411
411
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
412
412
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -414,7 +414,7 @@ class Function(
414
414
 
415
415
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
416
416
 
417
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
417
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
418
418
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
419
419
  """Calls the function with the given arguments, without waiting for the results.
420
420
 
@@ -435,7 +435,7 @@ class Function(
435
435
  """
436
436
  ...
437
437
 
438
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
438
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
439
439
 
440
440
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
441
441
  """Return the inner Python object wrapped by this Modal Function."""
@@ -360,6 +360,7 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
360
360
  async def _run_app(
361
361
  app: "modal.app._App",
362
362
  *,
363
+ name: Optional[str] = None,
363
364
  client: Optional[_Client] = None,
364
365
  detach: bool = False,
365
366
  environment_name: Optional[str] = None,
@@ -380,7 +381,9 @@ async def _run_app(
380
381
  "You should not use `app.run` or `run_app` within a Modal `local_entrypoint`"
381
382
  )
382
383
 
383
- if app.description is None:
384
+ if name:
385
+ app.set_description(name)
386
+ elif app.description is None:
384
387
  import __main__
385
388
 
386
389
  if "__file__" in dir(__main__):
@@ -75,6 +75,7 @@ async def _status_based_disconnect(
75
75
  def _run_app(
76
76
  app: modal.app._App,
77
77
  *,
78
+ name: typing.Optional[str] = None,
78
79
  client: typing.Optional[modal.client._Client] = None,
79
80
  detach: bool = False,
80
81
  environment_name: typing.Optional[str] = None,
@@ -170,6 +171,7 @@ class __run_app_spec(typing_extensions.Protocol):
170
171
  /,
171
172
  app: modal.app.App,
172
173
  *,
174
+ name: typing.Optional[str] = None,
173
175
  client: typing.Optional[modal.client.Client] = None,
174
176
  detach: bool = False,
175
177
  environment_name: typing.Optional[str] = None,
@@ -184,6 +186,7 @@ class __run_app_spec(typing_extensions.Protocol):
184
186
  /,
185
187
  app: modal.app.App,
186
188
  *,
189
+ name: typing.Optional[str] = None,
187
190
  client: typing.Optional[modal.client.Client] = None,
188
191
  detach: bool = False,
189
192
  environment_name: typing.Optional[str] = None,
@@ -18,11 +18,13 @@ from ._utils.sandbox_fs_utils import (
18
18
  make_make_directory_command,
19
19
  make_read_file_command,
20
20
  make_remove_command,
21
+ make_stat_command,
21
22
  make_write_file_command,
22
23
  raise_list_files_error,
23
24
  raise_make_directory_error,
24
25
  raise_read_file_error,
25
26
  raise_remove_error,
27
+ raise_stat_error,
26
28
  raise_write_file_error,
27
29
  translate_exec_errors,
28
30
  validate_absolute_remote_path,
@@ -419,6 +421,57 @@ class _SandboxFilesystem:
419
421
  if returncode != 0:
420
422
  raise_remove_error(returncode, stderr, remote_path)
421
423
 
424
+ async def stat(self, remote_path: str) -> FileInfo:
425
+ """Return metadata for a single file, directory, or symlink in the Sandbox.
426
+
427
+ `remote_path` must be an absolute path in the Sandbox. If `remote_path` is a symlink, the returned
428
+ `FileInfo` object describes the symlink, not the target it points to.
429
+
430
+ **Raises**
431
+
432
+ - `SandboxFilesystemNotFoundError`: the path does not exist.
433
+ - `SandboxFilesystemNotADirectoryError`: a non-leaf component of the path is not a directory.
434
+ - `SandboxFilesystemPermissionError`: a component of the path is not searchable.
435
+ - `SandboxFilesystemError`: the command fails for any other reason.
436
+
437
+ **Usage**
438
+
439
+ ```python fixture:sandbox
440
+ sandbox.filesystem.write_text("Hello, world!\\n", "/tmp/hello.txt")
441
+ info = sandbox.filesystem.stat("/tmp/hello.txt")
442
+ print(info.size, info.permissions, info.modified_time)
443
+ ```
444
+ """
445
+ validate_absolute_remote_path(remote_path, "stat")
446
+
447
+ t0 = time.monotonic()
448
+ with translate_exec_errors("stat", remote_path):
449
+ process = await self._sandbox.exec(_SANDBOX_FS_TOOLS_PATH, make_stat_command(remote_path))
450
+ stdout, stderr, returncode = await asyncio.gather(
451
+ process.stdout.read(), process.stderr.read(), process.wait()
452
+ )
453
+
454
+ if returncode != 0:
455
+ raise_stat_error(returncode, stderr, remote_path)
456
+
457
+ entry = json.loads(stdout)
458
+ result = FileInfo(
459
+ name=entry["name"],
460
+ path=entry["path"],
461
+ type=FileType(entry["type"]),
462
+ size=entry["size"],
463
+ mode=entry["mode"],
464
+ permissions=entry["permissions"],
465
+ owner=entry["owner"],
466
+ group=entry["group"],
467
+ modified_time=entry["modified_time"],
468
+ symlink_target=entry.get("symlink_target"),
469
+ )
470
+
471
+ dur_s = max(time.monotonic() - t0, 0.001)
472
+ logger.debug(f"sandbox stat {remote_path}: ({dur_s:.2f}s)")
473
+ return result
474
+
422
475
  async def write_bytes(self, data: Union[bytes, bytearray, memoryview], remote_path: str) -> None:
423
476
  """Write binary content to a file in the Sandbox.
424
477