modal 1.4.1.dev5__tar.gz → 1.4.2__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.1.dev5 → modal-1.4.2}/PKG-INFO +2 -2
  2. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_functions.py +0 -3
  3. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/asgi.py +5 -2
  4. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/grpc_utils.py +29 -5
  5. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/rand_pb_testing.py +3 -1
  6. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/sandbox_fs_utils.py +72 -0
  7. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/task_command_router_client.py +79 -24
  8. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/time_utils.py +55 -0
  9. {modal-1.4.1.dev5 → modal-1.4.2}/modal/app.py +2 -6
  10. {modal-1.4.1.dev5 → modal-1.4.2}/modal/app.pyi +4 -8
  11. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/app.py +171 -21
  12. modal-1.4.2/modal/cli/bootstrap.py +136 -0
  13. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/container.py +20 -2
  14. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/entry_point.py +8 -24
  15. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/import_refs.py +21 -2
  16. modal-1.4.2/modal/cli/logo.py +70 -0
  17. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/run.py +3 -3
  18. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/selector.py +12 -4
  19. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/shell.py +1 -1
  20. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/utils.py +10 -17
  21. {modal-1.4.1.dev5 → modal-1.4.2}/modal/client.py +7 -0
  22. {modal-1.4.1.dev5 → modal-1.4.2}/modal/client.pyi +14 -10
  23. {modal-1.4.1.dev5 → modal-1.4.2}/modal/config.py +2 -1
  24. {modal-1.4.1.dev5 → modal-1.4.2}/modal/dict.py +5 -5
  25. {modal-1.4.1.dev5 → modal-1.4.2}/modal/dict.pyi +8 -8
  26. {modal-1.4.1.dev5 → modal-1.4.2}/modal/exception.py +12 -0
  27. {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_io.py +2 -2
  28. {modal-1.4.1.dev5 → modal-1.4.2}/modal/functions.pyi +0 -1
  29. {modal-1.4.1.dev5 → modal-1.4.2}/modal/image.py +2 -0
  30. {modal-1.4.1.dev5 → modal-1.4.2}/modal/image.pyi +2 -0
  31. {modal-1.4.1.dev5 → modal-1.4.2}/modal/queue.py +5 -5
  32. {modal-1.4.1.dev5 → modal-1.4.2}/modal/queue.pyi +8 -8
  33. {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox.py +116 -40
  34. {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox.pyi +106 -45
  35. {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox_fs.py +91 -6
  36. modal-1.4.2/modal/sandbox_fs.pyi +672 -0
  37. {modal-1.4.1.dev5 → modal-1.4.2}/modal/secret.py +5 -5
  38. {modal-1.4.1.dev5 → modal-1.4.2}/modal/secret.pyi +8 -8
  39. {modal-1.4.1.dev5 → modal-1.4.2}/modal/volume.py +5 -5
  40. {modal-1.4.1.dev5 → modal-1.4.2}/modal/volume.pyi +8 -8
  41. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/PKG-INFO +2 -2
  42. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/SOURCES.txt +2 -0
  43. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/requires.txt +1 -1
  44. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/mdmd.py +30 -13
  45. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_grpc.py +128 -0
  46. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2.py +1250 -1051
  47. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2.pyi +453 -5
  48. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2_grpc.py +266 -0
  49. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2_grpc.pyi +84 -0
  50. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/modal_api_grpc.py +8 -0
  51. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_grpc.py +16 -0
  52. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2.py +19 -9
  53. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2.pyi +17 -0
  54. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2_grpc.py +34 -0
  55. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
  56. {modal-1.4.1.dev5 → modal-1.4.2}/modal_version/__init__.py +1 -1
  57. {modal-1.4.1.dev5 → modal-1.4.2}/pyproject.toml +5 -5
  58. modal-1.4.1.dev5/modal/sandbox_fs.pyi +0 -507
  59. {modal-1.4.1.dev5 → modal-1.4.2}/LICENSE +0 -0
  60. {modal-1.4.1.dev5 → modal-1.4.2}/README.md +0 -0
  61. {modal-1.4.1.dev5 → modal-1.4.2}/modal/__init__.py +0 -0
  62. {modal-1.4.1.dev5 → modal-1.4.2}/modal/__main__.py +0 -0
  63. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_billing.py +0 -0
  64. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_clustered_functions.py +0 -0
  65. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_clustered_functions.pyi +0 -0
  66. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_container_entrypoint.py +0 -0
  67. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_grpc_client.py +0 -0
  68. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_ipython.py +0 -0
  69. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_load_context.py +0 -0
  70. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_location.py +0 -0
  71. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_logs.py +0 -0
  72. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_object.py +0 -0
  73. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/__init__.py +0 -0
  74. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/manager.py +0 -0
  75. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/pty.py +0 -0
  76. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/rich.py +0 -0
  77. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/status.py +0 -0
  78. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_partial_function.py +0 -0
  79. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_resolver.py +0 -0
  80. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_resources.py +0 -0
  81. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/__init__.py +0 -0
  82. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/container_io_manager.py +0 -0
  83. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/container_io_manager.pyi +0 -0
  84. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/execution_context.py +0 -0
  85. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/execution_context.pyi +0 -0
  86. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  87. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/telemetry.py +0 -0
  88. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/user_code_event_loop.py +0 -0
  89. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/user_code_imports.py +0 -0
  90. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_serialization.py +0 -0
  91. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_server.py +0 -0
  92. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_traceback.py +0 -0
  93. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_tunnel.py +0 -0
  94. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_tunnel.pyi +0 -0
  95. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_type_manager.py +0 -0
  96. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/__init__.py +0 -0
  97. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/app_utils.py +0 -0
  98. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/async_utils.py +0 -0
  99. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/auth_token_manager.py +0 -0
  100. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/blob_utils.py +0 -0
  101. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/browser_utils.py +0 -0
  102. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
  103. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/deprecation.py +0 -0
  104. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/docker_utils.py +0 -0
  105. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/function_utils.py +0 -0
  106. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/git_utils.py +0 -0
  107. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/grpc_testing.py +0 -0
  108. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/hash_utils.py +0 -0
  109. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/http_utils.py +0 -0
  110. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/jwt_utils.py +0 -0
  111. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/logger.py +0 -0
  112. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/mount_utils.py +0 -0
  113. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/name_utils.py +0 -0
  114. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/package_utils.py +0 -0
  115. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/pattern_utils.py +0 -0
  116. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/shell_utils.py +0 -0
  117. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/__init__.py +0 -0
  118. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  119. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/cloudpickle.py +0 -0
  120. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/tblib.py +0 -0
  121. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/version.py +0 -0
  122. {modal-1.4.1.dev5 → modal-1.4.2}/modal/_watcher.py +0 -0
  123. {modal-1.4.1.dev5 → modal-1.4.2}/modal/billing.py +0 -0
  124. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2023.12.312.txt +0 -0
  125. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2023.12.txt +0 -0
  126. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2024.04.txt +0 -0
  127. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2024.10.txt +0 -0
  128. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2025.06.txt +0 -0
  129. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/PREVIEW.txt +0 -0
  130. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/README.md +0 -0
  131. {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/base-images.json +0 -0
  132. {modal-1.4.1.dev5 → modal-1.4.2}/modal/call_graph.py +0 -0
  133. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/__init__.py +0 -0
  134. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/_download.py +0 -0
  135. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/_traceback.py +0 -0
  136. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/billing.py +0 -0
  137. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/changelog.py +0 -0
  138. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/cluster.py +0 -0
  139. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/config.py +0 -0
  140. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/dashboard.py +0 -0
  141. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/dict.py +0 -0
  142. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/environment.py +0 -0
  143. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/launch.py +0 -0
  144. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/network_file_system.py +0 -0
  145. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/profile.py +0 -0
  146. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/__init__.py +0 -0
  147. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/run_jupyter.py +0 -0
  148. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/vscode.py +0 -0
  149. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/queues.py +0 -0
  150. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/secret.py +0 -0
  151. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/token.py +0 -0
  152. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/volume.py +0 -0
  153. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cloud_bucket_mount.py +0 -0
  154. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cloud_bucket_mount.pyi +0 -0
  155. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cls.py +0 -0
  156. {modal-1.4.1.dev5 → modal-1.4.2}/modal/cls.pyi +0 -0
  157. {modal-1.4.1.dev5 → modal-1.4.2}/modal/container_process.py +0 -0
  158. {modal-1.4.1.dev5 → modal-1.4.2}/modal/container_process.pyi +0 -0
  159. {modal-1.4.1.dev5 → modal-1.4.2}/modal/environments.py +0 -0
  160. {modal-1.4.1.dev5 → modal-1.4.2}/modal/environments.pyi +0 -0
  161. {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/__init__.py +0 -0
  162. {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/flash.py +0 -0
  163. {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/flash.pyi +0 -0
  164. {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/ipython.py +0 -0
  165. {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_io.pyi +0 -0
  166. {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_pattern_matcher.py +0 -0
  167. {modal-1.4.1.dev5 → modal-1.4.2}/modal/functions.py +0 -0
  168. {modal-1.4.1.dev5 → modal-1.4.2}/modal/io_streams.py +0 -0
  169. {modal-1.4.1.dev5 → modal-1.4.2}/modal/io_streams.pyi +0 -0
  170. {modal-1.4.1.dev5 → modal-1.4.2}/modal/mount.py +0 -0
  171. {modal-1.4.1.dev5 → modal-1.4.2}/modal/mount.pyi +0 -0
  172. {modal-1.4.1.dev5 → modal-1.4.2}/modal/network_file_system.py +0 -0
  173. {modal-1.4.1.dev5 → modal-1.4.2}/modal/network_file_system.pyi +0 -0
  174. {modal-1.4.1.dev5 → modal-1.4.2}/modal/object.py +0 -0
  175. {modal-1.4.1.dev5 → modal-1.4.2}/modal/object.pyi +0 -0
  176. {modal-1.4.1.dev5 → modal-1.4.2}/modal/output.py +0 -0
  177. {modal-1.4.1.dev5 → modal-1.4.2}/modal/parallel_map.py +0 -0
  178. {modal-1.4.1.dev5 → modal-1.4.2}/modal/parallel_map.pyi +0 -0
  179. {modal-1.4.1.dev5 → modal-1.4.2}/modal/partial_function.py +0 -0
  180. {modal-1.4.1.dev5 → modal-1.4.2}/modal/partial_function.pyi +0 -0
  181. {modal-1.4.1.dev5 → modal-1.4.2}/modal/proxy.py +0 -0
  182. {modal-1.4.1.dev5 → modal-1.4.2}/modal/proxy.pyi +0 -0
  183. {modal-1.4.1.dev5 → modal-1.4.2}/modal/py.typed +0 -0
  184. {modal-1.4.1.dev5 → modal-1.4.2}/modal/retries.py +0 -0
  185. {modal-1.4.1.dev5 → modal-1.4.2}/modal/runner.py +0 -0
  186. {modal-1.4.1.dev5 → modal-1.4.2}/modal/runner.pyi +0 -0
  187. {modal-1.4.1.dev5 → modal-1.4.2}/modal/running_app.py +0 -0
  188. {modal-1.4.1.dev5 → modal-1.4.2}/modal/schedule.py +0 -0
  189. {modal-1.4.1.dev5 → modal-1.4.2}/modal/scheduler_placement.py +0 -0
  190. {modal-1.4.1.dev5 → modal-1.4.2}/modal/server.py +0 -0
  191. {modal-1.4.1.dev5 → modal-1.4.2}/modal/server.pyi +0 -0
  192. {modal-1.4.1.dev5 → modal-1.4.2}/modal/serving.py +0 -0
  193. {modal-1.4.1.dev5 → modal-1.4.2}/modal/serving.pyi +0 -0
  194. {modal-1.4.1.dev5 → modal-1.4.2}/modal/snapshot.py +0 -0
  195. {modal-1.4.1.dev5 → modal-1.4.2}/modal/snapshot.pyi +0 -0
  196. {modal-1.4.1.dev5 → modal-1.4.2}/modal/stream_type.py +0 -0
  197. {modal-1.4.1.dev5 → modal-1.4.2}/modal/token_flow.py +0 -0
  198. {modal-1.4.1.dev5 → modal-1.4.2}/modal/token_flow.pyi +0 -0
  199. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/dependency_links.txt +0 -0
  200. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/entry_points.txt +0 -0
  201. {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/top_level.txt +0 -0
  202. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/__init__.py +0 -0
  203. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_cli_docs.py +0 -0
  204. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_cli_docs_main.py +0 -0
  205. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_reference_docs.py +0 -0
  206. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_reference_docs_main.py +0 -0
  207. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/__init__.py +0 -0
  208. {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/signatures.py +0 -0
  209. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/__init__.py +0 -0
  210. {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/py.typed +0 -0
  211. {modal-1.4.1.dev5 → modal-1.4.2}/modal_version/__main__.py +0 -0
  212. {modal-1.4.1.dev5 → modal-1.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.1.dev5
3
+ Version: 1.4.2
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.12.0
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
@@ -700,7 +700,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
700
700
  ephemeral_disk: Optional[int] = None,
701
701
  include_source: bool = True,
702
702
  experimental_options: Optional[dict[str, str]] = None,
703
- _experimental_proxy_ip: Optional[str] = None,
704
703
  restrict_output: bool = False,
705
704
  http_config: Optional[api_pb2.HTTPConfig] = None,
706
705
  ) -> "_Function":
@@ -1041,7 +1040,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1041
1040
  # ---
1042
1041
  _experimental_group_size=cluster_size or 0, # Experimental: Clustered functions
1043
1042
  _experimental_concurrent_cancellations=True,
1044
- _experimental_proxy_ip=_experimental_proxy_ip,
1045
1043
  # --- These are deprecated in favor of autoscaler_settings
1046
1044
  warm_pool_size=min_containers or 0,
1047
1045
  concurrency_limit=max_containers or 0,
@@ -1084,7 +1082,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1084
1082
  _experimental_group_size=function_definition._experimental_group_size,
1085
1083
  _experimental_buffer_containers=function_definition._experimental_buffer_containers,
1086
1084
  _experimental_custom_scaling=function_definition._experimental_custom_scaling,
1087
- _experimental_proxy_ip=function_definition._experimental_proxy_ip,
1088
1085
  snapshot_debug=function_definition.snapshot_debug,
1089
1086
  runtime_perf_record=function_definition.runtime_perf_record,
1090
1087
  function_schema=function_schema,
@@ -474,8 +474,11 @@ async def _proxy_lifespan_request(base_url, scope, receive, send) -> None:
474
474
  auto_decompress=False,
475
475
  read_bufsize=1024 * 1024, # 1 MiB
476
476
  connector=aiohttp.TCPConnector(
477
- limit=1000
478
- ), # 100 is the default max, but 1000 is the max for `@modal.concurrent`.
477
+ limit=1000, # 100 is the default max, but 1000 is the max for `@modal.concurrent`.
478
+ # Disable keep-alive connection reuse to avoid ServerDisconnectedError
479
+ # from stale pooled connections.
480
+ force_close=True,
481
+ ),
479
482
  # Note: these values will need to be kept in sync.
480
483
  **(
481
484
  # These options were introduced in aiohttp 3.9, and we can remove the
@@ -216,7 +216,11 @@ def create_channel(
216
216
  for k, v in metadata.items():
217
217
  event.metadata[k] = v
218
218
 
219
- logger.debug(f"Sending request to {event.method_name}")
219
+ idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
220
+ if idempotency_key is None:
221
+ logger.debug(f"Sending request to {event.method_name}")
222
+ else:
223
+ logger.debug(f"Sending request to {event.method_name} ({idempotency_key[:8]})")
220
224
 
221
225
  grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
222
226
 
@@ -288,12 +292,14 @@ def process_exception_before_retry(
288
292
  n_retries: int,
289
293
  delay: float,
290
294
  idempotency_key: str,
295
+ rpc_elapsed: float,
291
296
  ):
292
297
  """Process exception before retry, used by `_retry_transient_errors`."""
293
298
  with suppress_tb_frame():
294
299
  if final_attempt:
295
300
  logger.debug(
296
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
301
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
302
+ f"for {fn_name} ({idempotency_key[:8]})"
297
303
  )
298
304
  if isinstance(exc, OSError):
299
305
  raise ConnectionError(str(exc))
@@ -310,7 +316,10 @@ def process_exception_before_retry(
310
316
  # we handle in the retry logic once we drop this check!
311
317
  raise exc
312
318
 
313
- logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
319
+ logger.debug(
320
+ f"Retryable failure {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
321
+ f"for {fn_name} ({idempotency_key[:8]})"
322
+ )
314
323
 
315
324
 
316
325
  async def _retry_transient_errors(
@@ -373,6 +382,7 @@ async def _retry_transient_errors(
373
382
  else:
374
383
  timeout = None
375
384
 
385
+ attempt_started_at = time.monotonic()
376
386
  try:
377
387
  with suppress_tb_frame():
378
388
  return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
@@ -409,7 +419,13 @@ async def _retry_transient_errors(
409
419
 
410
420
  with suppress_tb_frame():
411
421
  process_exception_before_retry(
412
- exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
422
+ exc,
423
+ final_attempt,
424
+ fn.name,
425
+ n_retries,
426
+ server_delay,
427
+ idempotency_key,
428
+ time.monotonic() - attempt_started_at,
413
429
  )
414
430
 
415
431
  now = time.time()
@@ -438,7 +454,15 @@ async def _retry_transient_errors(
438
454
  final_attempt = False
439
455
 
440
456
  with suppress_tb_frame():
441
- process_exception_before_retry(exc, final_attempt, fn.name, n_retries, delay, idempotency_key)
457
+ process_exception_before_retry(
458
+ exc,
459
+ final_attempt,
460
+ fn.name,
461
+ n_retries,
462
+ delay,
463
+ idempotency_key,
464
+ time.monotonic() - attempt_started_at,
465
+ )
442
466
 
443
467
  n_retries += 1
444
468
 
@@ -52,8 +52,9 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
52
52
  if hasattr(field, "is_repeated"):
53
53
  is_repeated = field.is_repeated # type: ignore
54
54
  else:
55
- is_repeated = field.label == FieldDescriptor.LABEL_REPEATED
55
+ is_repeated = field.label == FieldDescriptor.LABEL_REPEATED # type: ignore[attr-defined]
56
56
  if is_message:
57
+ assert field.message_type is not None
57
58
  msg_field = getattr(msg, field.name)
58
59
  if is_repeated:
59
60
  num = rand.randint(0, 2)
@@ -64,6 +65,7 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
64
65
  _fill(msg_field, field.message_type, rand)
65
66
  else:
66
67
  if field.type == FieldDescriptor.TYPE_ENUM:
68
+ assert field.enum_type is not None
67
69
  enum_values = [x.number for x in field.enum_type.values]
68
70
  generator = lambda rand: rand.choice(enum_values) # noqa: E731
69
71
 
@@ -12,11 +12,13 @@ from ..exception import (
12
12
  Error as ModalError,
13
13
  InvalidError,
14
14
  NotFoundError,
15
+ SandboxFilesystemDirectoryNotEmptyError,
15
16
  SandboxFilesystemError,
16
17
  SandboxFilesystemFileTooLargeError,
17
18
  SandboxFilesystemIsADirectoryError,
18
19
  SandboxFilesystemNotADirectoryError,
19
20
  SandboxFilesystemNotFoundError,
21
+ SandboxFilesystemPathAlreadyExistsError,
20
22
  SandboxFilesystemPermissionError,
21
23
  ServiceError,
22
24
  )
@@ -153,6 +155,76 @@ def make_read_file_command(remote_path: str) -> str:
153
155
  return json.dumps({"ReadFile": {"path": remote_path}})
154
156
 
155
157
 
158
+ def make_remove_command(remote_path: str, recursive: bool) -> str:
159
+ """Build the JSON command string for a Remove operation.
160
+
161
+ The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
162
+ Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
163
+ this schema like protobuf changes: fields must not be removed or renamed,
164
+ only added with backwards-compatible defaults.
165
+ """
166
+ return json.dumps({"Remove": {"path": remote_path, "recursive": recursive}})
167
+
168
+
169
+ def raise_remove_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
170
+ if payload := try_parse_error_payload(stderr):
171
+ logger.debug(
172
+ f"sandbox-fs-tools remove error: path={remote_path}, "
173
+ f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
174
+ )
175
+ if payload.error_kind == "NotFound":
176
+ raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
177
+ if payload.error_kind == "DirectoryNotEmpty":
178
+ raise SandboxFilesystemDirectoryNotEmptyError(f"{payload.message}: {remote_path}")
179
+ if payload.error_kind == "NotSupported":
180
+ raise InvalidError(
181
+ f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
182
+ )
183
+ if payload.error_kind == "PermissionDenied":
184
+ raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
185
+ raise SandboxFilesystemError(payload.message)
186
+
187
+ if stderr_text := _stderr_to_text(stderr):
188
+ logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
189
+ raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
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
+
156
228
  def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
157
229
  if not PurePosixPath(remote_path).is_absolute():
158
230
  raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
@@ -122,6 +122,18 @@ async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.Ta
122
122
  )
123
123
 
124
124
 
125
+ async def fetch_command_router_access_v2(
126
+ server_client, sandbox_id: str
127
+ ) -> api_pb2.SandboxGetCommandRouterAccessResponse:
128
+ """Fetch direct command router access info from Modal server for a V2 sandbox."""
129
+ assert server_client._auth_token_manager
130
+ auth_token = await server_client._auth_token_manager.get_token()
131
+ return await server_client.stub.SandboxGetCommandRouterAccess(
132
+ api_pb2.SandboxGetCommandRouterAccessRequest(sandbox_id=sandbox_id),
133
+ metadata=[("x-modal-auth-token", auth_token)],
134
+ )
135
+
136
+
125
137
  def _finalize_channel(loop, channel):
126
138
  if not loop.is_closed():
127
139
  # only run if loop has not shut down
@@ -139,27 +151,19 @@ class TaskCommandRouterClient:
139
151
  """
140
152
 
141
153
  @classmethod
142
- async def try_init(
154
+ async def _connect(
143
155
  cls,
144
156
  server_client,
145
157
  task_id: str,
146
- ) -> Optional["TaskCommandRouterClient"]:
147
- """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
148
-
149
- Returns None if command router access is not enabled (FAILED_PRECONDITION).
150
- """
151
- try:
152
- resp = await fetch_command_router_access(server_client, task_id)
153
- except ConflictError:
154
- logger.debug(f"Command router access is not enabled for task {task_id}")
155
- return None
156
-
157
- logger.debug(f"Using command router access for task {task_id}")
158
-
159
- # Build and connect a channel to the task command router now that we have access info.
160
- o = urllib.parse.urlparse(resp.url)
158
+ url: str,
159
+ jwt: str,
160
+ *,
161
+ sandbox_id: Optional[str] = None,
162
+ ) -> "TaskCommandRouterClient":
163
+ """Build a connected client from a jwt and url."""
164
+ o = urllib.parse.urlparse(url)
161
165
  if o.scheme != "https":
162
- raise ValueError(f"Task router URL must be https, got: {resp.url}")
166
+ raise ValueError(f"Task router URL must be https, got: {url}")
163
167
 
164
168
  host, _, port_str = o.netloc.partition(":")
165
169
  port = int(port_str) if port_str else 443
@@ -189,7 +193,38 @@ class TaskCommandRouterClient:
189
193
  loop = asyncio.get_running_loop()
190
194
  jwt_refresh_lock = asyncio.Lock()
191
195
 
192
- return cls(server_client, task_id, resp.url, resp.jwt, channel, loop, jwt_refresh_lock)
196
+ return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
197
+
198
+ @classmethod
199
+ async def try_init(
200
+ cls,
201
+ server_client,
202
+ task_id: str,
203
+ ) -> Optional["TaskCommandRouterClient"]:
204
+ """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
205
+
206
+ Returns None if command router access is not enabled (FAILED_PRECONDITION).
207
+ """
208
+ try:
209
+ resp = await fetch_command_router_access(server_client, task_id)
210
+ except ConflictError:
211
+ logger.debug(f"Command router access is not enabled for task {task_id}")
212
+ return None
213
+
214
+ logger.debug(f"Using command router access for task {task_id}")
215
+ return await cls._connect(server_client, task_id, resp.url, resp.jwt)
216
+
217
+ @classmethod
218
+ async def init_v2(
219
+ cls,
220
+ server_client,
221
+ sandbox_id: str,
222
+ task_id: str,
223
+ ) -> "TaskCommandRouterClient":
224
+ """Initialize a TaskCommandRouterClient for a V2 sandbox."""
225
+ resp = await fetch_command_router_access_v2(server_client, sandbox_id)
226
+ logger.debug(f"Using command router access for sandbox {sandbox_id}")
227
+ return await cls._connect(server_client, task_id, resp.url, resp.jwt, sandbox_id=sandbox_id)
193
228
 
194
229
  def __init__(
195
230
  self,
@@ -201,6 +236,7 @@ class TaskCommandRouterClient:
201
236
  loop: asyncio.AbstractEventLoop,
202
237
  jwt_refresh_lock: asyncio.Lock,
203
238
  *,
239
+ sandbox_id: Optional[str] = None,
204
240
  stream_stdio_retry_delay_secs: float = 0.01,
205
241
  stream_stdio_retry_delay_factor: float = 2,
206
242
  stream_stdio_max_retries: int = 10,
@@ -213,6 +249,7 @@ class TaskCommandRouterClient:
213
249
  # Attach bearer token on all requests to the worker-side router service.
214
250
  self._server_client = server_client
215
251
  self._task_id = task_id
252
+ self._sandbox_id = sandbox_id
216
253
  self._server_url = server_url
217
254
  self._jwt = jwt
218
255
  self._channel = channel
@@ -237,6 +274,10 @@ class TaskCommandRouterClient:
237
274
 
238
275
  self._stub = TaskCommandRouterStub(self._channel)
239
276
 
277
+ @property
278
+ def _is_v2_sandbox(self) -> bool:
279
+ return self._sandbox_id is not None
280
+
240
281
  def _get_metadata(self):
241
282
  return {"authorization": f"Bearer {self._jwt}"}
242
283
 
@@ -452,14 +493,22 @@ class TaskCommandRouterClient:
452
493
  )
453
494
  return
454
495
 
455
- logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
456
- resp = await fetch_command_router_access(self._server_client, self._task_id)
457
- logger.debug(f"Finished refreshing JWT for exec with task ID {self._task_id}")
496
+ if self._is_v2_sandbox:
497
+ logger.debug(f"Refreshing JWT for exec with sandbox ID {self._sandbox_id}")
498
+ v2_resp = await fetch_command_router_access_v2(self._server_client, self._sandbox_id)
499
+ logger.debug(f"Finished refreshing JWT for exec with sandbox ID {self._sandbox_id}")
500
+ jwt, url = v2_resp.jwt, v2_resp.url
501
+ else:
502
+ logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
503
+ v1_resp = await fetch_command_router_access(self._server_client, self._task_id)
504
+ logger.debug(f"Finished refreshing JWT for exec with task ID {self._task_id}")
505
+ jwt, url = v1_resp.jwt, v1_resp.url
458
506
 
459
507
  # Ensure the server URL remains stable for the lifetime of this client.
460
- assert resp.url == self._server_url, "Task router URL changed during session"
461
- self._jwt = resp.jwt
462
- self._jwt_exp = _parse_jwt_expiration(resp.jwt)
508
+ if url != self._server_url:
509
+ logger.warning("Task router URL changed during session")
510
+ self._jwt = jwt
511
+ self._jwt_exp = _parse_jwt_expiration(jwt)
463
512
 
464
513
  async def _call_with_auth_retry(self, func, *args, **kwargs):
465
514
  try:
@@ -571,6 +620,12 @@ class TaskCommandRouterClient:
571
620
  lambda: self._call_with_auth_retry(self._stub.TaskMountDirectory, request)
572
621
  )
573
622
 
623
+ async def unmount_image(self, request: sr_pb2.TaskUnmountDirectoryRequest):
624
+ with grpc_error_converter():
625
+ return await call_with_retries_on_transient_errors(
626
+ lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
627
+ )
628
+
574
629
  async def snapshot_directory(
575
630
  self, request: sr_pb2.TaskSnapshotDirectoryRequest
576
631
  ) -> sr_pb2.TaskSnapshotDirectoryResponse:
@@ -164,6 +164,61 @@ def parse_date_range(s: str, tz: Optional[tzinfo] = None) -> tuple[datetime, dat
164
164
  raise ValueError(f"Unrecognized range: '{s}'. Accepted values: {accepted}")
165
165
 
166
166
 
167
+ def relative_timestamp(dt: datetime) -> str:
168
+ """Convert a tz-aware datetime to a human-readable relative time string.
169
+
170
+ Examples: "just now", "30 seconds ago", "5 minutes ago", "2 hours ago",
171
+ "yesterday", "3 days ago", "2 weeks ago", "3 months ago", "1 year ago".
172
+
173
+ Raises ValueError if the datetime is naive (no tzinfo).
174
+ """
175
+ if dt.tzinfo is None:
176
+ raise ValueError("datetime must be timezone-aware")
177
+
178
+ now = datetime.now(timezone.utc)
179
+ delta = now - dt
180
+ total_seconds = int(delta.total_seconds())
181
+
182
+ if total_seconds < 0:
183
+ return "just now"
184
+
185
+ if total_seconds < 10:
186
+ return "just now"
187
+ if total_seconds < 60:
188
+ return f"{total_seconds} seconds ago"
189
+ if total_seconds < 120:
190
+ return "1 minute ago"
191
+
192
+ minutes = total_seconds // 60
193
+ if minutes < 60:
194
+ return f"{minutes} minutes ago"
195
+ if minutes < 120:
196
+ return "1 hour ago"
197
+
198
+ hours = minutes // 60
199
+ if hours < 24:
200
+ return f"{hours} hours ago"
201
+ if hours < 48:
202
+ return "yesterday"
203
+
204
+ days = hours // 24
205
+ if days < 14:
206
+ return f"{days} days ago"
207
+
208
+ weeks = days // 7
209
+ if days < 60:
210
+ return f"{weeks} weeks ago"
211
+
212
+ months = days // 30
213
+ if days < 365:
214
+ return f"{months} months ago"
215
+
216
+ years = days // 365
217
+ if years == 1:
218
+ return "1 year ago"
219
+ return f"{years} years ago"
220
+
221
+
167
222
  def locale_tz() -> tzinfo:
168
223
  return datetime.now().astimezone().tzinfo
169
224
 
@@ -753,7 +753,6 @@ class _App:
753
753
  include_source: Optional[bool] = None,
754
754
  experimental_options: Optional[dict[str, Any]] = None,
755
755
  # Parameters below here are experimental. Use with caution!
756
- _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
757
756
  _experimental_restrict_output: bool = False, # Don't use pickle for return values
758
757
  # Parameters below here are deprecated. Please update your code as suggested
759
758
  max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
@@ -914,7 +913,6 @@ class _App:
914
913
  rdma=rdma,
915
914
  include_source=include_source if include_source is not None else local_state.include_source_default,
916
915
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
917
- _experimental_proxy_ip=_experimental_proxy_ip,
918
916
  restrict_output=_experimental_restrict_output,
919
917
  )
920
918
 
@@ -968,7 +966,6 @@ class _App:
968
966
  include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
969
967
  experimental_options: Optional[dict[str, Any]] = None,
970
968
  # Parameters below here are experimental. Use with caution!
971
- _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
972
969
  _experimental_restrict_output: bool = False, # Don't use pickle for return values
973
970
  # Parameters below here are deprecated. Please update your code as suggested
974
971
  max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
@@ -1115,7 +1112,6 @@ class _App:
1115
1112
  rdma=rdma,
1116
1113
  include_source=include_source if include_source is not None else local_state.include_source_default,
1117
1114
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
1118
- _experimental_proxy_ip=_experimental_proxy_ip,
1119
1115
  restrict_output=_experimental_restrict_output,
1120
1116
  )
1121
1117
 
@@ -1290,12 +1286,12 @@ class _App:
1290
1286
 
1291
1287
  ```python
1292
1288
  app_a = modal.App("a")
1293
- @app.function()
1289
+ @app_a.function()
1294
1290
  def foo():
1295
1291
  ...
1296
1292
 
1297
1293
  app_b = modal.App("b")
1298
- @app.function()
1294
+ @app_b.function()
1299
1295
  def bar():
1300
1296
  ...
1301
1297
 
@@ -497,7 +497,6 @@ class _App:
497
497
  i6pn: typing.Optional[bool] = None,
498
498
  include_source: typing.Optional[bool] = None,
499
499
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
500
- _experimental_proxy_ip: typing.Optional[str] = None,
501
500
  _experimental_restrict_output: bool = False,
502
501
  max_inputs: typing.Optional[int] = None,
503
502
  ) -> _FunctionDecoratorType:
@@ -545,7 +544,6 @@ class _App:
545
544
  i6pn: typing.Optional[bool] = None,
546
545
  include_source: typing.Optional[bool] = None,
547
546
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
548
- _experimental_proxy_ip: typing.Optional[str] = None,
549
547
  _experimental_restrict_output: bool = False,
550
548
  max_inputs: typing.Optional[int] = None,
551
549
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
@@ -618,12 +616,12 @@ class _App:
618
616
 
619
617
  ```python
620
618
  app_a = modal.App("a")
621
- @app.function()
619
+ @app_a.function()
622
620
  def foo():
623
621
  ...
624
622
 
625
623
  app_b = modal.App("b")
626
- @app.function()
624
+ @app_b.function()
627
625
  def bar():
628
626
  ...
629
627
 
@@ -1215,7 +1213,6 @@ class App:
1215
1213
  i6pn: typing.Optional[bool] = None,
1216
1214
  include_source: typing.Optional[bool] = None,
1217
1215
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1218
- _experimental_proxy_ip: typing.Optional[str] = None,
1219
1216
  _experimental_restrict_output: bool = False,
1220
1217
  max_inputs: typing.Optional[int] = None,
1221
1218
  ) -> _FunctionDecoratorType:
@@ -1263,7 +1260,6 @@ class App:
1263
1260
  i6pn: typing.Optional[bool] = None,
1264
1261
  include_source: typing.Optional[bool] = None,
1265
1262
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1266
- _experimental_proxy_ip: typing.Optional[str] = None,
1267
1263
  _experimental_restrict_output: bool = False,
1268
1264
  max_inputs: typing.Optional[int] = None,
1269
1265
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
@@ -1334,12 +1330,12 @@ class App:
1334
1330
 
1335
1331
  ```python
1336
1332
  app_a = modal.App("a")
1337
- @app.function()
1333
+ @app_a.function()
1338
1334
  def foo():
1339
1335
  ...
1340
1336
 
1341
1337
  app_b = modal.App("b")
1342
- @app.function()
1338
+ @app_b.function()
1343
1339
  def bar():
1344
1340
  ...
1345
1341