modAL 1.3.6.dev14__tar.gz → 1.4.1__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 (209) hide show
  1. {modal-1.3.6.dev14 → modal-1.4.1}/PKG-INFO +2 -2
  2. {modal-1.3.6.dev14 → modal-1.4.1}/modal/__init__.py +2 -1
  3. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_functions.py +0 -3
  4. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/pty.py +5 -0
  5. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/async_utils.py +27 -9
  6. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/grpc_utils.py +2 -3
  7. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/sandbox_fs_utils.py +13 -1
  8. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/task_command_router_client.py +20 -4
  9. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/app.py +7 -1
  10. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/container.py +36 -5
  11. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/import_refs.py +13 -4
  12. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/run.py +4 -2
  13. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/shell.py +5 -1
  14. {modal-1.3.6.dev14 → modal-1.4.1}/modal/client.py +3 -1
  15. {modal-1.3.6.dev14 → modal-1.4.1}/modal/client.pyi +2 -10
  16. {modal-1.3.6.dev14 → modal-1.4.1}/modal/config.py +30 -0
  17. {modal-1.3.6.dev14 → modal-1.4.1}/modal/dict.py +2 -1
  18. {modal-1.3.6.dev14 → modal-1.4.1}/modal/dict.pyi +4 -2
  19. {modal-1.3.6.dev14 → modal-1.4.1}/modal/functions.pyi +13 -23
  20. {modal-1.3.6.dev14 → modal-1.4.1}/modal/mount.py +7 -5
  21. {modal-1.3.6.dev14 → modal-1.4.1}/modal/output.py +2 -5
  22. {modal-1.3.6.dev14 → modal-1.4.1}/modal/parallel_map.py +18 -59
  23. {modal-1.3.6.dev14 → modal-1.4.1}/modal/parallel_map.pyi +6 -13
  24. {modal-1.3.6.dev14 → modal-1.4.1}/modal/queue.py +2 -1
  25. {modal-1.3.6.dev14 → modal-1.4.1}/modal/queue.pyi +4 -2
  26. {modal-1.3.6.dev14 → modal-1.4.1}/modal/runner.py +14 -4
  27. {modal-1.3.6.dev14 → modal-1.4.1}/modal/runner.pyi +1 -1
  28. {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox.py +309 -10
  29. {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox.pyi +232 -0
  30. {modal-1.3.6.dev14 → modal-1.4.1}/modal/secret.py +2 -1
  31. {modal-1.3.6.dev14 → modal-1.4.1}/modal/secret.pyi +4 -2
  32. {modal-1.3.6.dev14 → modal-1.4.1}/modal/serving.py +2 -2
  33. {modal-1.3.6.dev14 → modal-1.4.1}/modal/volume.py +2 -1
  34. {modal-1.3.6.dev14 → modal-1.4.1}/modal/volume.pyi +4 -2
  35. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/PKG-INFO +2 -2
  36. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/requires.txt +1 -1
  37. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_grpc.py +16 -0
  38. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2.py +1060 -1040
  39. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2.pyi +39 -1
  40. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2_grpc.py +33 -0
  41. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2_grpc.pyi +10 -0
  42. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/modal_api_grpc.py +1 -0
  43. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2.py +43 -30
  44. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2.pyi +21 -1
  45. {modal-1.3.6.dev14 → modal-1.4.1}/modal_version/__init__.py +1 -1
  46. {modal-1.3.6.dev14 → modal-1.4.1}/pyproject.toml +2 -2
  47. {modal-1.3.6.dev14 → modal-1.4.1}/LICENSE +0 -0
  48. {modal-1.3.6.dev14 → modal-1.4.1}/README.md +0 -0
  49. {modal-1.3.6.dev14 → modal-1.4.1}/modal/__main__.py +0 -0
  50. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_billing.py +0 -0
  51. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_clustered_functions.py +0 -0
  52. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_clustered_functions.pyi +0 -0
  53. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_container_entrypoint.py +0 -0
  54. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_grpc_client.py +0 -0
  55. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_ipython.py +0 -0
  56. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_load_context.py +0 -0
  57. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_location.py +0 -0
  58. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_logs.py +0 -0
  59. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_object.py +0 -0
  60. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/__init__.py +0 -0
  61. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/manager.py +0 -0
  62. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/rich.py +0 -0
  63. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/status.py +0 -0
  64. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_partial_function.py +0 -0
  65. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_resolver.py +0 -0
  66. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_resources.py +0 -0
  67. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/__init__.py +0 -0
  68. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/asgi.py +0 -0
  69. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/container_io_manager.py +0 -0
  70. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/container_io_manager.pyi +0 -0
  71. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/execution_context.py +0 -0
  72. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/execution_context.pyi +0 -0
  73. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  74. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/telemetry.py +0 -0
  75. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/user_code_event_loop.py +0 -0
  76. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/user_code_imports.py +0 -0
  77. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_serialization.py +0 -0
  78. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_server.py +0 -0
  79. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_traceback.py +0 -0
  80. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_tunnel.py +0 -0
  81. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_tunnel.pyi +0 -0
  82. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_type_manager.py +0 -0
  83. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/__init__.py +0 -0
  84. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/app_utils.py +0 -0
  85. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/auth_token_manager.py +0 -0
  86. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/blob_utils.py +0 -0
  87. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/browser_utils.py +0 -0
  88. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
  89. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/deprecation.py +0 -0
  90. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/docker_utils.py +0 -0
  91. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/function_utils.py +0 -0
  92. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/git_utils.py +0 -0
  93. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/grpc_testing.py +0 -0
  94. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/hash_utils.py +0 -0
  95. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/http_utils.py +0 -0
  96. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/jwt_utils.py +0 -0
  97. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/logger.py +0 -0
  98. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/mount_utils.py +0 -0
  99. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/name_utils.py +0 -0
  100. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/package_utils.py +0 -0
  101. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/pattern_utils.py +0 -0
  102. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/rand_pb_testing.py +0 -0
  103. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/shell_utils.py +0 -0
  104. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/time_utils.py +0 -0
  105. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/__init__.py +0 -0
  106. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  107. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/cloudpickle.py +0 -0
  108. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/tblib.py +0 -0
  109. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/version.py +0 -0
  110. {modal-1.3.6.dev14 → modal-1.4.1}/modal/_watcher.py +0 -0
  111. {modal-1.3.6.dev14 → modal-1.4.1}/modal/app.py +0 -0
  112. {modal-1.3.6.dev14 → modal-1.4.1}/modal/app.pyi +0 -0
  113. {modal-1.3.6.dev14 → modal-1.4.1}/modal/billing.py +0 -0
  114. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2023.12.312.txt +0 -0
  115. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2023.12.txt +0 -0
  116. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2024.04.txt +0 -0
  117. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2024.10.txt +0 -0
  118. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2025.06.txt +0 -0
  119. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/PREVIEW.txt +0 -0
  120. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/README.md +0 -0
  121. {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/base-images.json +0 -0
  122. {modal-1.3.6.dev14 → modal-1.4.1}/modal/call_graph.py +0 -0
  123. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/__init__.py +0 -0
  124. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/_download.py +0 -0
  125. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/_traceback.py +0 -0
  126. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/billing.py +0 -0
  127. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/changelog.py +0 -0
  128. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/cluster.py +0 -0
  129. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/config.py +0 -0
  130. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/dashboard.py +0 -0
  131. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/dict.py +0 -0
  132. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/entry_point.py +0 -0
  133. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/environment.py +0 -0
  134. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/launch.py +0 -0
  135. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/network_file_system.py +0 -0
  136. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/profile.py +0 -0
  137. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/__init__.py +0 -0
  138. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/run_jupyter.py +0 -0
  139. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/vscode.py +0 -0
  140. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/queues.py +0 -0
  141. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/secret.py +0 -0
  142. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/selector.py +0 -0
  143. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/token.py +0 -0
  144. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/utils.py +0 -0
  145. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/volume.py +0 -0
  146. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cloud_bucket_mount.py +0 -0
  147. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cloud_bucket_mount.pyi +0 -0
  148. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cls.py +0 -0
  149. {modal-1.3.6.dev14 → modal-1.4.1}/modal/cls.pyi +0 -0
  150. {modal-1.3.6.dev14 → modal-1.4.1}/modal/container_process.py +0 -0
  151. {modal-1.3.6.dev14 → modal-1.4.1}/modal/container_process.pyi +0 -0
  152. {modal-1.3.6.dev14 → modal-1.4.1}/modal/environments.py +0 -0
  153. {modal-1.3.6.dev14 → modal-1.4.1}/modal/environments.pyi +0 -0
  154. {modal-1.3.6.dev14 → modal-1.4.1}/modal/exception.py +0 -0
  155. {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/__init__.py +0 -0
  156. {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/flash.py +0 -0
  157. {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/flash.pyi +0 -0
  158. {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/ipython.py +0 -0
  159. {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_io.py +0 -0
  160. {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_io.pyi +0 -0
  161. {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_pattern_matcher.py +0 -0
  162. {modal-1.3.6.dev14 → modal-1.4.1}/modal/functions.py +0 -0
  163. {modal-1.3.6.dev14 → modal-1.4.1}/modal/image.py +0 -0
  164. {modal-1.3.6.dev14 → modal-1.4.1}/modal/image.pyi +0 -0
  165. {modal-1.3.6.dev14 → modal-1.4.1}/modal/io_streams.py +0 -0
  166. {modal-1.3.6.dev14 → modal-1.4.1}/modal/io_streams.pyi +0 -0
  167. {modal-1.3.6.dev14 → modal-1.4.1}/modal/mount.pyi +0 -0
  168. {modal-1.3.6.dev14 → modal-1.4.1}/modal/network_file_system.py +0 -0
  169. {modal-1.3.6.dev14 → modal-1.4.1}/modal/network_file_system.pyi +0 -0
  170. {modal-1.3.6.dev14 → modal-1.4.1}/modal/object.py +0 -0
  171. {modal-1.3.6.dev14 → modal-1.4.1}/modal/object.pyi +0 -0
  172. {modal-1.3.6.dev14 → modal-1.4.1}/modal/partial_function.py +0 -0
  173. {modal-1.3.6.dev14 → modal-1.4.1}/modal/partial_function.pyi +0 -0
  174. {modal-1.3.6.dev14 → modal-1.4.1}/modal/proxy.py +0 -0
  175. {modal-1.3.6.dev14 → modal-1.4.1}/modal/proxy.pyi +0 -0
  176. {modal-1.3.6.dev14 → modal-1.4.1}/modal/py.typed +0 -0
  177. {modal-1.3.6.dev14 → modal-1.4.1}/modal/retries.py +0 -0
  178. {modal-1.3.6.dev14 → modal-1.4.1}/modal/running_app.py +0 -0
  179. {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox_fs.py +0 -0
  180. {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox_fs.pyi +0 -0
  181. {modal-1.3.6.dev14 → modal-1.4.1}/modal/schedule.py +0 -0
  182. {modal-1.3.6.dev14 → modal-1.4.1}/modal/scheduler_placement.py +0 -0
  183. {modal-1.3.6.dev14 → modal-1.4.1}/modal/server.py +0 -0
  184. {modal-1.3.6.dev14 → modal-1.4.1}/modal/server.pyi +0 -0
  185. {modal-1.3.6.dev14 → modal-1.4.1}/modal/serving.pyi +0 -0
  186. {modal-1.3.6.dev14 → modal-1.4.1}/modal/snapshot.py +0 -0
  187. {modal-1.3.6.dev14 → modal-1.4.1}/modal/snapshot.pyi +0 -0
  188. {modal-1.3.6.dev14 → modal-1.4.1}/modal/stream_type.py +0 -0
  189. {modal-1.3.6.dev14 → modal-1.4.1}/modal/token_flow.py +0 -0
  190. {modal-1.3.6.dev14 → modal-1.4.1}/modal/token_flow.pyi +0 -0
  191. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/SOURCES.txt +0 -0
  192. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/dependency_links.txt +0 -0
  193. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/entry_points.txt +0 -0
  194. {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/top_level.txt +0 -0
  195. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/__init__.py +0 -0
  196. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_cli_docs.py +0 -0
  197. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_cli_docs_main.py +0 -0
  198. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_reference_docs.py +0 -0
  199. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_reference_docs_main.py +0 -0
  200. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/__init__.py +0 -0
  201. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/mdmd.py +0 -0
  202. {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/signatures.py +0 -0
  203. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/__init__.py +0 -0
  204. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/py.typed +0 -0
  205. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_grpc.py +0 -0
  206. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  207. {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  208. {modal-1.3.6.dev14 → modal-1.4.1}/modal_version/__main__.py +0 -0
  209. {modal-1.3.6.dev14 → modal-1.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.6.dev14
3
+ Version: 1.4.1
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -23,7 +23,7 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
23
23
  Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
24
24
  Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
25
25
  Requires-Dist: rich>=12.0.0
26
- Requires-Dist: synchronicity~=0.11.1
26
+ Requires-Dist: synchronicity~=0.12.1
27
27
  Requires-Dist: toml
28
28
  Requires-Dist: typer>=0.9
29
29
  Requires-Dist: types-certifi
@@ -38,7 +38,7 @@ try:
38
38
  from .proxy import Proxy
39
39
  from .queue import Queue
40
40
  from .retries import Retries
41
- from .sandbox import Sandbox
41
+ from .sandbox import Probe, Sandbox
42
42
  from .schedule import Cron, Period
43
43
  from .scheduler_placement import SchedulerPlacement
44
44
  from .secret import Secret
@@ -68,6 +68,7 @@ __all__ = [
68
68
  "Image",
69
69
  "NetworkFileSystem",
70
70
  "Period",
71
+ "Probe",
71
72
  "Proxy",
72
73
  "Queue",
73
74
  "Retries",
@@ -1562,7 +1562,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1562
1562
  input_queue: _SynchronizedQueue,
1563
1563
  order_outputs: bool,
1564
1564
  return_exceptions: bool,
1565
- wrap_returned_exceptions: bool,
1566
1565
  ) -> AsyncGenerator[Any, None]:
1567
1566
  """mdmd:hidden
1568
1567
 
@@ -1588,7 +1587,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1588
1587
  self.client,
1589
1588
  order_outputs,
1590
1589
  return_exceptions,
1591
- wrap_returned_exceptions,
1592
1590
  count_update_callback,
1593
1591
  )
1594
1592
  ) as stream:
@@ -1602,7 +1600,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1602
1600
  self.client,
1603
1601
  order_outputs,
1604
1602
  return_exceptions,
1605
- wrap_returned_exceptions,
1606
1603
  count_update_callback,
1607
1604
  api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1608
1605
  )
@@ -147,6 +147,11 @@ async def get_app_logs_loop(
147
147
  # doesn't support server-side search)
148
148
  if search_text and search_text not in log.data:
149
149
  return
150
+ # Search results may lack trailing newlines; ensure each
151
+ # match is a complete line so the LineBufferedOutput buffer
152
+ # doesn't merge it with the next match.
153
+ if search_text and not log.data.endswith("\n"):
154
+ log.data += "\n"
150
155
  if pty_shell_finish_event:
151
156
  await output_mgr.put_pty_content(log)
152
157
  else:
@@ -349,19 +349,18 @@ def retry(
349
349
  direct_fn=None,
350
350
  *,
351
351
  n_attempts=3,
352
- base_delay=0,
352
+ base_delay=0.0,
353
353
  delay_factor=2,
354
+ max_delay: Optional[float] = None,
354
355
  attempt_timeout: Optional[float] = 90,
355
- attempt_timeout_factor=1,
356
+ total_timeout: Optional[float] = None,
356
357
  ):
357
358
  """Decorator that calls an async function multiple times, with a given timeout.
358
359
 
359
360
  If a `base_delay` is provided, the function is given an exponentially
360
361
  increasing delay on each run, up until the maximum number of attempts.
361
362
 
362
- If `attempt_timeout_factor` is set to a value > 1, the per-attempt timeout
363
- increases exponentially (starting from `attempt_timeout`, multiplied by
364
- `attempt_timeout_factor` after each failed attempt).
363
+ If `max_delay` is set, the delay between attempts is capped at this value.
365
364
 
366
365
  Usage:
367
366
 
@@ -377,6 +376,7 @@ def retry(
377
376
  pass
378
377
  ```
379
378
  """
379
+ ATTEMPT_TIMEOUT_FLOOR = 2.0
380
380
 
381
381
  def decorator(fn):
382
382
  @functools.wraps(fn)
@@ -387,17 +387,35 @@ def retry(
387
387
  local_n_attempts = n_attempts
388
388
 
389
389
  delay = base_delay
390
- current_attempt_timeout = attempt_timeout
390
+ if total_timeout is not None:
391
+ total_deadline = time.time() + total_timeout
392
+ else:
393
+ total_deadline = None
394
+
391
395
  for i in range(local_n_attempts):
392
396
  t0 = time.time()
397
+
398
+ timeouts = []
399
+ if attempt_timeout is not None:
400
+ timeouts.append(attempt_timeout)
401
+ if total_deadline is not None:
402
+ timeouts.append(max(total_deadline - time.time(), ATTEMPT_TIMEOUT_FLOOR))
403
+ timeout = min(timeouts) if timeouts else None
404
+
393
405
  try:
394
- return await asyncio.wait_for(fn(*args, **kwargs), timeout=current_attempt_timeout)
406
+ return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
395
407
  except asyncio.CancelledError:
396
408
  logger.debug(f"Function {fn} was cancelled")
397
409
  raise
398
410
  except Exception as e:
399
411
  if i >= local_n_attempts - 1:
400
412
  raise
413
+
414
+ # We check if the timeout will be reached **after** the sleep, so we can raise an error early
415
+ # without needing to actually sleep.
416
+ if total_deadline is not None and time.time() + delay + ATTEMPT_TIMEOUT_FLOOR >= total_deadline:
417
+ raise
418
+
401
419
  logger.debug(
402
420
  f"Failed invoking function {fn}: {e}"
403
421
  f" (took {time.time() - t0}s, sleeping {delay}s"
@@ -405,8 +423,8 @@ def retry(
405
423
  )
406
424
  await asyncio.sleep(delay)
407
425
  delay *= delay_factor
408
- if current_attempt_timeout is not None:
409
- current_attempt_timeout *= attempt_timeout_factor
426
+ if max_delay is not None:
427
+ delay = min(delay, max_delay)
410
428
 
411
429
  return f_wrapped
412
430
 
@@ -90,8 +90,6 @@ RETRYABLE_GRPC_STATUS_CODES = [
90
90
  ]
91
91
  SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
92
92
  # Initial per-attempt timeout for TCP/TLS handshake in connect_channel.
93
- # Doubles each attempt (attempt_timeout_factor=2): 0.5 + 1 + 2 + 4 + 8 = 15.5s + ~1.5s inter-attempt delays.
94
- CONNECT_TIMEOUT = 0.5
95
93
  DEFAULT_MAX_RETRIES = 3
96
94
 
97
95
 
@@ -225,7 +223,8 @@ def create_channel(
225
223
  return channel
226
224
 
227
225
 
228
- @retry(n_attempts=5, base_delay=0.1, attempt_timeout=CONNECT_TIMEOUT, attempt_timeout_factor=2)
226
+ # With 18 attempts, the max delays between calls is: 0.1 + 0.2 + 0.4 + 0.8 + 1.6 + 3.2 + 5*11 ~ 62
227
+ @retry(n_attempts=18, base_delay=0.1, attempt_timeout=10.0, max_delay=5.0, total_timeout=63.0)
229
228
  async def connect_channel(channel: grpclib.client.Channel):
230
229
  """Connect to socket and raise exceptions when there is a connection issue."""
231
230
  await channel.__connect__()
@@ -28,6 +28,7 @@ _EXEC_SANDBOX_UNAVAILABLE_ERROR_TYPES = (NotFoundError, ServiceError, Connection
28
28
  class ErrorPayload:
29
29
  error_kind: str
30
30
  message: str
31
+ detail: str = ""
31
32
 
32
33
 
33
34
  def _stderr_to_text(stderr: Union[str, bytes]) -> str:
@@ -51,11 +52,18 @@ def try_parse_error_payload(stderr: Union[str, bytes]) -> Optional[ErrorPayload]
51
52
  return None
52
53
  if not isinstance(message, str) or not message.strip():
53
54
  return None
54
- return ErrorPayload(error_kind=error_kind, message=message)
55
+ detail = payload.get("detail", "")
56
+ if not isinstance(detail, str):
57
+ detail = ""
58
+ return ErrorPayload(error_kind=error_kind, message=message, detail=detail)
55
59
 
56
60
 
57
61
  def raise_read_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
58
62
  if payload := try_parse_error_payload(stderr):
63
+ logger.debug(
64
+ f"sandbox-fs-tools read error: path={remote_path}, "
65
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
66
+ )
59
67
  if payload.error_kind == "NotFound":
60
68
  raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
61
69
  if payload.error_kind == "IsDirectory":
@@ -106,6 +114,10 @@ def translate_exec_unexpected_error(operation: str, path: str, exc: Exception) -
106
114
 
107
115
  def raise_write_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
108
116
  if payload := try_parse_error_payload(stderr):
117
+ logger.debug(
118
+ f"sandbox-fs-tools write error: path={remote_path}, "
119
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
120
+ )
109
121
  if payload.error_kind == "NotDirectory" or payload.error_kind == "AlreadyExists":
110
122
  raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
111
123
  if payload.error_kind == "IsDirectory":
@@ -2,6 +2,7 @@
2
2
  import asyncio
3
3
  import base64
4
4
  import json
5
+ import socket
5
6
  import ssl
6
7
  import time
7
8
  import urllib.parse
@@ -11,7 +12,6 @@ from typing import AsyncGenerator, Optional
11
12
 
12
13
  import grpclib.client
13
14
  import grpclib.config
14
- import grpclib.events
15
15
  from grpclib import GRPCError, Status
16
16
  from grpclib.exceptions import StreamTerminatedError
17
17
 
@@ -22,8 +22,21 @@ from modal_proto.task_command_router_grpc import TaskCommandRouterStub
22
22
 
23
23
  from .._grpc_client import grpc_error_converter
24
24
  from .._utils.grpc_utils import PermanentCloseableChannel
25
- from .async_utils import aclosing
26
- from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES, connect_channel
25
+ from .async_utils import aclosing, retry
26
+ from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
27
+
28
+
29
+ @retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
30
+ async def _connect_channel(channel: grpclib.client.Channel):
31
+ """Connect to the command router channel.
32
+
33
+ Uses a longer retry budget than grpc_utils.connect_channel. In rare cases the sandbox
34
+ may take a long time to start on the worker after scheduling.
35
+
36
+ Retries with exponential backoff (1, 2, 4, 8, 10, 10, ...) capped at 10s per delay.
37
+ Total sleep between attempts: 1 + 2 + 4 + 8 + 10*29 = 305s (~5 min).
38
+ """
39
+ await channel.__connect__()
27
40
 
28
41
 
29
42
  def _b64url_decode(data: str) -> bytes:
@@ -169,7 +182,10 @@ class TaskCommandRouterClient:
169
182
  closed_error_message="Unable to perform operation on a detached sandbox",
170
183
  )
171
184
 
172
- await connect_channel(channel)
185
+ try:
186
+ await _connect_channel(channel)
187
+ except socket.gaierror as exc:
188
+ raise ConnectionError(f"Could not resolve hostname '{host}': {exc}") from exc
173
189
  loop = asyncio.get_running_loop()
174
190
  jwt_refresh_lock = asyncio.Lock()
175
191
 
@@ -164,7 +164,7 @@ def logs(
164
164
  Get recent logs based on an app ID:
165
165
 
166
166
  ```
167
- modal app logs ap-123456 --tail 100
167
+ modal app logs ap-123456
168
168
  ```
169
169
 
170
170
  Get recent logs for a currently deployed App based on its name:
@@ -179,6 +179,12 @@ def logs(
179
179
  modal app logs my-app -f
180
180
  ```
181
181
 
182
+ Fetch the last 1000 entries:
183
+
184
+ ```
185
+ modal app logs my-app --tail 1000
186
+ ```
187
+
182
188
  Fetch logs from the last 2 hours:
183
189
 
184
190
  ```
@@ -1,4 +1,5 @@
1
1
  # Copyright Modal Labs 2022
2
+ import warnings
2
3
  from datetime import datetime, timezone
3
4
  from typing import Optional, Union
4
5
 
@@ -27,7 +28,11 @@ container_cli = typer.Typer(name="container", help="Manage and connect to runnin
27
28
 
28
29
  @container_cli.command("list")
29
30
  @synchronizer.create_blocking
30
- async def list_(env: Optional[str] = ENV_OPTION, json: bool = False, app_id: str = ""):
31
+ async def list_(
32
+ app_id: str = typer.Option("", "--app-id", help="List containers running for a specific App."),
33
+ env: Optional[str] = ENV_OPTION,
34
+ json: bool = False,
35
+ ):
31
36
  """List all containers that are currently running."""
32
37
  env = ensure_env(env)
33
38
  client = await _Client.from_env()
@@ -113,6 +118,12 @@ async def logs(
113
118
  modal container logs ta-123456 --since 2026-03-01T05:00:00 --until 2026-03-01T08:00:00
114
119
  ```
115
120
 
121
+ Fetch the last 1000 entries:
122
+
123
+ ```
124
+ modal container logs ta-123456 --tail 1000
125
+ ```
126
+
116
127
  Fetch all container logs:
117
128
 
118
129
  ```
@@ -175,20 +186,40 @@ async def logs(
175
186
  task_info_resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=task_id))
176
187
  app_id = task_info_resp.app_id
177
188
 
189
+ if not task_info_resp.info.started_at:
190
+ # Unlikely race or Modal backend issue, don't treat as a usage exception
191
+ return
192
+ container_started_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
193
+
178
194
  now = datetime.now(timezone.utc)
179
195
  if all_logs:
180
- since_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
196
+ since_dt = container_started_dt
181
197
  if task_info_resp.info.finished_at:
182
198
  until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
183
199
  else:
184
200
  until_dt = now
185
201
  else:
186
- since_dt = _parse_time_arg(since, default=now) if since else None
187
- until_dt = _parse_time_arg(until, default=now) if until else None
202
+ since_dt = _parse_time_arg(since, default=container_started_dt)
203
+ if task_info_resp.info.finished_at:
204
+ default_until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
205
+ else:
206
+ default_until_dt = now
207
+ until_dt = _parse_time_arg(until, default=default_until_dt)
188
208
 
189
- if since_dt is not None and until_dt is not None and since_dt >= until_dt:
209
+ if since is not None and until is not None and since_dt >= until_dt:
210
+ # User provided both --since and --until, but did so incorrectly
190
211
  raise UsageError("--since must be before --until.")
191
212
 
213
+ if since is not None and until is None and since_dt >= until_dt:
214
+ # User provided only --since and it is after the container finished
215
+ warnings.warn("--since time is after the Container finished, no logs to fetch.", UserWarning)
216
+ return
217
+
218
+ if until is not None and since is None and since_dt >= until_dt:
219
+ # User provided only --until and it is before the Container started
220
+ warnings.warn("--until time is before the Container started, no logs to fetch.", UserWarning)
221
+ return
222
+
192
223
  if since_dt is not None:
193
224
  effective_until = until_dt or now
194
225
  if effective_until - since_dt > _MAX_FETCH_RANGE:
@@ -127,6 +127,7 @@ class AutoRunPriority:
127
127
 
128
128
  def list_cli_commands(
129
129
  module_members: dict[str, typing.Any],
130
+ include_service_functions: bool = False,
130
131
  ) -> list[CLICommand]:
131
132
  """
132
133
  Extracts all runnables found either directly in the input module, or in any of the Apps listed in that module
@@ -149,8 +150,8 @@ def list_cli_commands(
149
150
  all_runnables[local_entrypoint].append(f"{app_name}.{name}")
150
151
  priorities[local_entrypoint] = AutoRunPriority.APP_LOCAL_ENTRYPOINT
151
152
  for name, function in app.registered_functions.items():
152
- if function.info.is_service_class():
153
- continue # Skip class and server service functions since they don't have associated cli commands
153
+ if function.info.is_service_class() and not include_service_functions:
154
+ continue # Skip class and server service functions for all commands except `modal shell`.
154
155
  all_runnables[function].append(f"{app_name}.{name}")
155
156
  priorities[function] = AutoRunPriority.APP_FUNCTION
156
157
  for cls_name, cls in app.registered_classes.items():
@@ -322,7 +323,12 @@ def _get_runnable_app(runnable: Runnable) -> App:
322
323
 
323
324
 
324
325
  def import_and_filter(
325
- import_ref: ImportRef, *, base_cmd: str, accept_local_entrypoint=True, accept_webhook=False
326
+ import_ref: ImportRef,
327
+ *,
328
+ base_cmd: str,
329
+ accept_local_entrypoint=True,
330
+ accept_webhook=False,
331
+ accept_service_functions=False,
326
332
  ) -> tuple[Optional[Runnable], list[CLICommand]]:
327
333
  """Takes a function ref string and returns a single determined "runnable" to use, and a list of all available
328
334
  runnables.
@@ -339,7 +345,10 @@ def import_and_filter(
339
345
  """
340
346
  # all commands:
341
347
  module = import_file_or_module(import_ref, base_cmd)
342
- cli_commands = list_cli_commands(dict(inspect.getmembers(module)))
348
+ cli_commands = list_cli_commands(
349
+ dict(inspect.getmembers(module)),
350
+ include_service_functions=accept_service_functions,
351
+ )
343
352
 
344
353
  # all commands that satisfy local entrypoint/accept webhook limitations AND object path prefix
345
354
 
@@ -7,7 +7,7 @@ import sys
7
7
  import time
8
8
  import typing
9
9
  from dataclasses import dataclass
10
- from typing import Any, Callable, Optional
10
+ from typing import Any, Callable, Optional, get_args
11
11
 
12
12
  import click
13
13
  import typer
@@ -504,7 +504,9 @@ def deploy(
504
504
  False, "-m", help="Interpret argument as a Python module path instead of a file/script path"
505
505
  ),
506
506
  timestamps: bool = typer.Option(False, "--timestamps", help="Show timestamps for each log line."),
507
- strategy: DEPLOYMENT_STRATEGY_TYPE = typer.Option("rolling", help="Deployment strategy"),
507
+ strategy: str = typer.Option(
508
+ "rolling", help="Deployment strategy", click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE))
509
+ ),
508
510
  ):
509
511
  """Deploy a Modal application.
510
512
 
@@ -142,7 +142,11 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
142
142
  def _function_spec_from_ref(ref: str, use_module_mode: bool) -> _FunctionSpec:
143
143
  import_ref = parse_import_ref(ref, use_module_mode=use_module_mode)
144
144
  runnable, all_usable_commands = import_and_filter(
145
- import_ref, base_cmd="modal shell", accept_local_entrypoint=False, accept_webhook=True
145
+ import_ref,
146
+ base_cmd="modal shell",
147
+ accept_local_entrypoint=False,
148
+ accept_webhook=True,
149
+ accept_service_functions=True,
146
150
  )
147
151
  if not runnable:
148
152
  help_header = (
@@ -22,7 +22,7 @@ from ._utils import async_utils
22
22
  from ._utils.async_utils import TaskContext, synchronize_api
23
23
  from ._utils.auth_token_manager import _AuthTokenManager
24
24
  from ._utils.grpc_utils import ConnectionManager
25
- from .config import _check_config, _is_remote, config, logger
25
+ from .config import _agent_environment, _check_config, _is_remote, config, logger
26
26
  from .exception import AuthError, ClientClosed
27
27
 
28
28
  HEARTBEAT_INTERVAL: float = config.get("heartbeat_interval")
@@ -56,6 +56,8 @@ def _get_metadata(client_type: int, credentials: Optional[tuple[str, str]], vers
56
56
  "x-modal-token-secret": token_secret,
57
57
  }
58
58
  )
59
+ if agent_env := _agent_environment():
60
+ metadata["x-modal-agent-harness"] = urllib.parse.quote(agent_env)
59
61
  return metadata
60
62
 
61
63
 
@@ -31,11 +31,7 @@ class _Client:
31
31
  client_type: int
32
32
 
33
33
  def __init__(
34
- self,
35
- server_url: str,
36
- client_type: int,
37
- credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.3.6.dev14",
34
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.4.1"
39
35
  ):
40
36
  """mdmd:hidden
41
37
  The Modal client object is not intended to be instantiated directly by users.
@@ -167,11 +163,7 @@ class Client:
167
163
  client_type: int
168
164
 
169
165
  def __init__(
170
- self,
171
- server_url: str,
172
- client_type: int,
173
- credentials: typing.Optional[tuple[str, str]],
174
- version: str = "1.3.6.dev14",
166
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.4.1"
175
167
  ):
176
168
  """mdmd:hidden
177
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -121,6 +121,36 @@ def _is_remote() -> bool:
121
121
  return os.environ.get("MODAL_IS_REMOTE") == "1"
122
122
 
123
123
 
124
+ def _agent_environment() -> Optional[str]:
125
+ """Detect if the current process is running inside an AI agent harness.
126
+
127
+ Returns the agent name if detected, or None otherwise.
128
+
129
+ Checks the emerging `AGENT` standard variable first, then falls back to
130
+ tool-specific environment variables used by known agent harnesses.
131
+ """
132
+ # Emerging standard: AGENT=<name>
133
+ if agent := os.environ.get("AGENT"):
134
+ return agent
135
+
136
+ # Tool-specific environment variables
137
+ _TOOL_SPECIFIC_ENV_VARS: list[tuple[str, str]] = [
138
+ ("CLAUDECODE", "claude-code"),
139
+ ("GEMINI_CLI", "gemini-cli"),
140
+ ("CURSOR_AGENT", "cursor"),
141
+ ("CLINE_ACTIVE", "cline"),
142
+ ("AUGMENT_AGENT", "augment"),
143
+ ("OPENCODE_CLIENT", "opencode"),
144
+ ("GOOSE_TERMINAL", "goose"),
145
+ ("TRAE_AI_SHELL_ID", "trae"),
146
+ ]
147
+ for env_var, agent_name in _TOOL_SPECIFIC_ENV_VARS:
148
+ if os.environ.get(env_var):
149
+ return agent_name
150
+
151
+ return None
152
+
153
+
124
154
  def _read_user_config():
125
155
  config_data = {}
126
156
  if not _is_remote() and os.path.exists(user_config_path):
@@ -298,7 +298,8 @@ class _Dict(_Object, type_prefix="di"):
298
298
  )
299
299
 
300
300
  @classproperty
301
- def objects(cls) -> _DictManager:
301
+ @classmethod
302
+ def objects(cls) -> type[_DictManager]:
302
303
  return _DictManager
303
304
 
304
305
  @property
@@ -412,7 +412,8 @@ class _Dict(modal._object._Object):
412
412
  ...
413
413
 
414
414
  @synchronicity.classproperty
415
- def objects(cls) -> _DictManager: ...
415
+ @classmethod
416
+ def objects(cls) -> type[_DictManager]: ...
416
417
  @property
417
418
  def name(self) -> typing.Optional[str]: ...
418
419
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
@@ -653,7 +654,8 @@ class Dict(modal.object.Object):
653
654
  ...
654
655
 
655
656
  @synchronicity.classproperty
656
- def objects(cls) -> DictManager: ...
657
+ @classmethod
658
+ def objects(cls) -> type[DictManager]: ...
657
659
  @property
658
660
  def name(self) -> typing.Optional[str]: ...
659
661
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...