modal 1.5.6.dev31__tar.gz → 1.6.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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (242) hide show
  1. {modal-1.5.6.dev31 → modal-1.6.1}/PKG-INFO +3 -3
  2. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_environments.py +2 -1
  3. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_functions.py +4 -4
  4. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_object.py +17 -9
  5. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/status.py +1 -3
  6. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_partial_function.py +1 -1
  7. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_server.py +44 -17
  8. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_server.pyi +40 -17
  9. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/grpc_utils.py +11 -12
  10. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/task_command_router_client.py +29 -8
  11. modal-1.6.1/modal/cli/endpoint.py +779 -0
  12. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/function.py +5 -3
  13. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/server.py +175 -111
  14. {modal-1.5.6.dev31 → modal-1.6.1}/modal/client.pyi +2 -2
  15. {modal-1.5.6.dev31 → modal-1.6.1}/modal/config.py +28 -6
  16. {modal-1.5.6.dev31 → modal-1.6.1}/modal/dict.py +4 -4
  17. {modal-1.5.6.dev31 → modal-1.6.1}/modal/dict.pyi +9 -9
  18. {modal-1.5.6.dev31 → modal-1.6.1}/modal/functions.pyi +12 -10
  19. {modal-1.5.6.dev31 → modal-1.6.1}/modal/network_file_system.py +3 -3
  20. {modal-1.5.6.dev31 → modal-1.6.1}/modal/network_file_system.pyi +6 -6
  21. {modal-1.5.6.dev31 → modal-1.6.1}/modal/object.pyi +1 -3
  22. {modal-1.5.6.dev31 → modal-1.6.1}/modal/parallel_map.py +4 -4
  23. {modal-1.5.6.dev31 → modal-1.6.1}/modal/parallel_map.pyi +3 -3
  24. {modal-1.5.6.dev31 → modal-1.6.1}/modal/partial_function.pyi +1 -1
  25. {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox.py +62 -46
  26. {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox.pyi +28 -0
  27. {modal-1.5.6.dev31 → modal-1.6.1}/modal/server.pyi +1 -1
  28. {modal-1.5.6.dev31 → modal-1.6.1}/modal/skills/modal/SKILL.md +19 -24
  29. {modal-1.5.6.dev31 → modal-1.6.1}/modal/token_flow.py +2 -1
  30. {modal-1.5.6.dev31 → modal-1.6.1}/modal/types.py +8 -1
  31. {modal-1.5.6.dev31 → modal-1.6.1}/modal/volume.py +6 -2
  32. {modal-1.5.6.dev31 → modal-1.6.1}/modal/volume.pyi +21 -3
  33. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/PKG-INFO +3 -3
  34. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/requires.txt +2 -2
  35. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2.py +1383 -1373
  36. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2.pyi +64 -8
  37. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2.py +96 -96
  38. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2.pyi +12 -2
  39. {modal-1.5.6.dev31 → modal-1.6.1}/modal_version/__init__.py +1 -1
  40. {modal-1.5.6.dev31 → modal-1.6.1}/pyproject.toml +3 -3
  41. modal-1.5.6.dev31/modal/cli/endpoint.py +0 -372
  42. {modal-1.5.6.dev31 → modal-1.6.1}/LICENSE +0 -0
  43. {modal-1.5.6.dev31 → modal-1.6.1}/README.md +0 -0
  44. {modal-1.5.6.dev31 → modal-1.6.1}/modal/__init__.py +0 -0
  45. {modal-1.5.6.dev31 → modal-1.6.1}/modal/__main__.py +0 -0
  46. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_billing.py +0 -0
  47. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_cluster.py +0 -0
  48. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_cluster.pyi +0 -0
  49. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_container_entrypoint.py +0 -0
  50. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_function_variants.py +0 -0
  51. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_grpc_client.py +0 -0
  52. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_image.py +0 -0
  53. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_ipython.py +0 -0
  54. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_load_context.py +0 -0
  55. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_location.py +0 -0
  56. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs.py +0 -0
  57. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs_manager.py +0 -0
  58. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_logs_manager.pyi +0 -0
  59. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_outbound_policy.py +0 -0
  60. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_outbound_policy.pyi +0 -0
  61. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/__init__.py +0 -0
  62. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/manager.py +0 -0
  63. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/pty.py +0 -0
  64. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_output/rich.py +0 -0
  65. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_resolver.py +0 -0
  66. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_resources.py +0 -0
  67. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/__init__.py +0 -0
  68. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/asgi.py +0 -0
  69. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/container_io_manager.py +0 -0
  70. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/container_io_manager.pyi +0 -0
  71. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/execution_context.py +0 -0
  72. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/execution_context.pyi +0 -0
  73. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  74. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/task_lifecycle_manager.py +0 -0
  75. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  76. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/telemetry.py +0 -0
  77. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/user_code_event_loop.py +0 -0
  78. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_runtime/user_code_imports.py +0 -0
  79. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_serialization.py +0 -0
  80. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_supports_logs.py +0 -0
  81. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_traceback.py +0 -0
  82. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_tunnel.py +0 -0
  83. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_tunnel.pyi +0 -0
  84. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_type_manager.py +0 -0
  85. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/__init__.py +0 -0
  86. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/app_utils.py +0 -0
  87. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/async_utils.py +0 -0
  88. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/auth_token_manager.py +0 -0
  89. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/blob_utils.py +0 -0
  90. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/browser_utils.py +0 -0
  91. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
  92. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/curl_utils.py +0 -0
  93. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/deprecation.py +0 -0
  94. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/display_utils.py +0 -0
  95. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/docker_utils.py +0 -0
  96. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/function_utils.py +0 -0
  97. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/git_utils.py +0 -0
  98. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/grpc_testing.py +0 -0
  99. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/hash_utils.py +0 -0
  100. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/http_utils.py +0 -0
  101. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/idle_countdown.py +0 -0
  102. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/jwt_utils.py +0 -0
  103. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/logger.py +0 -0
  104. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/mount_utils.py +0 -0
  105. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/name_utils.py +0 -0
  106. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/package_utils.py +0 -0
  107. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/pattern_utils.py +0 -0
  108. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/proxy_support.py +0 -0
  109. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/rand_pb_testing.py +0 -0
  110. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/sandbox_fs_utils.py +0 -0
  111. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/shell_utils.py +0 -0
  112. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_utils/time_utils.py +0 -0
  113. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/__init__.py +0 -0
  114. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  115. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/cloudpickle.py +0 -0
  116. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/tblib.py +0 -0
  117. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_vendor/version.py +0 -0
  118. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_watcher.py +0 -0
  119. {modal-1.5.6.dev31 → modal-1.6.1}/modal/_workspace.py +0 -0
  120. {modal-1.5.6.dev31 → modal-1.6.1}/modal/app.py +0 -0
  121. {modal-1.5.6.dev31 → modal-1.6.1}/modal/app.pyi +0 -0
  122. {modal-1.5.6.dev31 → modal-1.6.1}/modal/billing.py +0 -0
  123. {modal-1.5.6.dev31 → modal-1.6.1}/modal/billing.pyi +0 -0
  124. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2023.12.312.txt +0 -0
  125. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2023.12.txt +0 -0
  126. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2024.04.txt +0 -0
  127. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2024.10.txt +0 -0
  128. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/2025.06.txt +0 -0
  129. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/PREVIEW.txt +0 -0
  130. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/README.md +0 -0
  131. {modal-1.5.6.dev31 → modal-1.6.1}/modal/builder/base-images.json +0 -0
  132. {modal-1.5.6.dev31 → modal-1.6.1}/modal/call_graph.py +0 -0
  133. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/__init__.py +0 -0
  134. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_download.py +0 -0
  135. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_help.py +0 -0
  136. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_logs.py +0 -0
  137. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_stats.py +0 -0
  138. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/_traceback.py +0 -0
  139. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/app.py +0 -0
  140. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/billing.py +0 -0
  141. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/changelog.py +0 -0
  142. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/config.py +0 -0
  143. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/container.py +0 -0
  144. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/curl.py +0 -0
  145. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/dashboard.py +0 -0
  146. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/dict.py +0 -0
  147. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/entry_point.py +0 -0
  148. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/environment.py +0 -0
  149. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/image.py +0 -0
  150. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/import_refs.py +0 -0
  151. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/logo.py +0 -0
  152. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/network_file_system.py +0 -0
  153. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/profile.py +0 -0
  154. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/queues.py +0 -0
  155. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/run.py +0 -0
  156. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/secret.py +0 -0
  157. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/selector.py +0 -0
  158. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/shell.py +0 -0
  159. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/skills.py +0 -0
  160. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/token.py +0 -0
  161. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/utils.py +0 -0
  162. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/volume.py +0 -0
  163. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cli/workspace.py +0 -0
  164. {modal-1.5.6.dev31 → modal-1.6.1}/modal/client.py +0 -0
  165. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cloud_bucket_mount.py +0 -0
  166. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cloud_bucket_mount.pyi +0 -0
  167. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cls.py +0 -0
  168. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cls.pyi +0 -0
  169. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cluster.py +0 -0
  170. {modal-1.5.6.dev31 → modal-1.6.1}/modal/cluster.pyi +0 -0
  171. {modal-1.5.6.dev31 → modal-1.6.1}/modal/container_process.py +0 -0
  172. {modal-1.5.6.dev31 → modal-1.6.1}/modal/container_process.pyi +0 -0
  173. {modal-1.5.6.dev31 → modal-1.6.1}/modal/environments.py +0 -0
  174. {modal-1.5.6.dev31 → modal-1.6.1}/modal/environments.pyi +0 -0
  175. {modal-1.5.6.dev31 → modal-1.6.1}/modal/exception.py +0 -0
  176. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/__init__.py +0 -0
  177. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/_flash_container_entry.py +0 -0
  178. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash.py +0 -0
  179. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash.pyi +0 -0
  180. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash_server.py +0 -0
  181. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/flash_server.pyi +0 -0
  182. {modal-1.5.6.dev31 → modal-1.6.1}/modal/experimental/ipython.py +0 -0
  183. {modal-1.5.6.dev31 → modal-1.6.1}/modal/file_pattern_matcher.py +0 -0
  184. {modal-1.5.6.dev31 → modal-1.6.1}/modal/functions.py +0 -0
  185. {modal-1.5.6.dev31 → modal-1.6.1}/modal/image.py +0 -0
  186. {modal-1.5.6.dev31 → modal-1.6.1}/modal/image.pyi +0 -0
  187. {modal-1.5.6.dev31 → modal-1.6.1}/modal/io_streams.py +0 -0
  188. {modal-1.5.6.dev31 → modal-1.6.1}/modal/io_streams.pyi +0 -0
  189. {modal-1.5.6.dev31 → modal-1.6.1}/modal/mount.py +0 -0
  190. {modal-1.5.6.dev31 → modal-1.6.1}/modal/mount.pyi +0 -0
  191. {modal-1.5.6.dev31 → modal-1.6.1}/modal/object.py +0 -0
  192. {modal-1.5.6.dev31 → modal-1.6.1}/modal/output.py +0 -0
  193. {modal-1.5.6.dev31 → modal-1.6.1}/modal/partial_function.py +0 -0
  194. {modal-1.5.6.dev31 → modal-1.6.1}/modal/proxy.py +0 -0
  195. {modal-1.5.6.dev31 → modal-1.6.1}/modal/proxy.pyi +0 -0
  196. {modal-1.5.6.dev31 → modal-1.6.1}/modal/py.typed +0 -0
  197. {modal-1.5.6.dev31 → modal-1.6.1}/modal/queue.py +0 -0
  198. {modal-1.5.6.dev31 → modal-1.6.1}/modal/queue.pyi +0 -0
  199. {modal-1.5.6.dev31 → modal-1.6.1}/modal/retries.py +0 -0
  200. {modal-1.5.6.dev31 → modal-1.6.1}/modal/runner.py +0 -0
  201. {modal-1.5.6.dev31 → modal-1.6.1}/modal/runner.pyi +0 -0
  202. {modal-1.5.6.dev31 → modal-1.6.1}/modal/running_app.py +0 -0
  203. {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox_fs.py +0 -0
  204. {modal-1.5.6.dev31 → modal-1.6.1}/modal/sandbox_fs.pyi +0 -0
  205. {modal-1.5.6.dev31 → modal-1.6.1}/modal/schedule.py +0 -0
  206. {modal-1.5.6.dev31 → modal-1.6.1}/modal/scheduler_placement.py +0 -0
  207. {modal-1.5.6.dev31 → modal-1.6.1}/modal/secret.py +0 -0
  208. {modal-1.5.6.dev31 → modal-1.6.1}/modal/secret.pyi +0 -0
  209. {modal-1.5.6.dev31 → modal-1.6.1}/modal/server.py +0 -0
  210. {modal-1.5.6.dev31 → modal-1.6.1}/modal/serving.py +0 -0
  211. {modal-1.5.6.dev31 → modal-1.6.1}/modal/serving.pyi +0 -0
  212. {modal-1.5.6.dev31 → modal-1.6.1}/modal/snapshot.py +0 -0
  213. {modal-1.5.6.dev31 → modal-1.6.1}/modal/snapshot.pyi +0 -0
  214. {modal-1.5.6.dev31 → modal-1.6.1}/modal/stream_type.py +0 -0
  215. {modal-1.5.6.dev31 → modal-1.6.1}/modal/token_flow.pyi +0 -0
  216. {modal-1.5.6.dev31 → modal-1.6.1}/modal/workspace.py +0 -0
  217. {modal-1.5.6.dev31 → modal-1.6.1}/modal/workspace.pyi +0 -0
  218. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/SOURCES.txt +0 -0
  219. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/dependency_links.txt +0 -0
  220. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/entry_points.txt +0 -0
  221. {modal-1.5.6.dev31 → modal-1.6.1}/modal.egg-info/top_level.txt +0 -0
  222. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/__init__.py +0 -0
  223. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_cli_docs.py +0 -0
  224. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_cli_docs_main.py +0 -0
  225. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_reference_docs.py +0 -0
  226. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/gen_reference_docs_main.py +0 -0
  227. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/__init__.py +0 -0
  228. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/mdmd.py +0 -0
  229. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/signatures.py +0 -0
  230. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/mdmd/types.py +0 -0
  231. {modal-1.5.6.dev31 → modal-1.6.1}/modal_docs/reference_intro.md +0 -0
  232. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/__init__.py +0 -0
  233. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_grpc.py +0 -0
  234. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2_grpc.py +0 -0
  235. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/api_pb2_grpc.pyi +0 -0
  236. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/modal_api_grpc.py +0 -0
  237. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/py.typed +0 -0
  238. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_grpc.py +0 -0
  239. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  240. {modal-1.5.6.dev31 → modal-1.6.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  241. {modal-1.5.6.dev31 → modal-1.6.1}/modal_version/__main__.py +0 -0
  242. {modal-1.5.6.dev31 → modal-1.6.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev31
3
+ Version: 1.6.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
@@ -21,9 +21,9 @@ Requires-Dist: certifi
21
21
  Requires-Dist: click~=8.1
22
22
  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
- Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
24
+ Requires-Dist: protobuf!=4.24.0,<8.0,>=3.19
25
25
  Requires-Dist: rich>=12.0.0
26
- Requires-Dist: synchronicity~=0.12.5
26
+ Requires-Dist: synchronicity~=0.12.6
27
27
  Requires-Dist: toml
28
28
  Requires-Dist: types-certifi
29
29
  Requires-Dist: types-toml
@@ -22,7 +22,7 @@ from ._utils.time_utils import is_utc_month_aligned, parse_billing_cycle
22
22
  from .client import _Client
23
23
  from .config import config, logger
24
24
  from .exception import InvalidError, WorkspaceManagementError
25
- from .types import BillingReportItem, EnvironmentBillingSummary
25
+ from .types import AppInfo, BillingReportItem, EnvironmentBillingSummary
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  import modal
@@ -346,6 +346,7 @@ class _EnvironmentAppsManager:
346
346
  self._environment.name,
347
347
  self._environment.client,
348
348
  description=item.description or None,
349
+ info=AppInfo._from_proto(item.metadata, item.app_id),
349
350
  )
350
351
  for item in response.apps
351
352
  if item.state not in stopped_app_states
@@ -7,7 +7,7 @@ import time
7
7
  import typing
8
8
  import warnings
9
9
  from collections import OrderedDict
10
- from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection, Sequence
10
+ from collections.abc import AsyncGenerator, Callable, Collection, Sequence
11
11
  from dataclasses import dataclass
12
12
  from datetime import datetime, timedelta, timezone
13
13
  from pathlib import PurePosixPath
@@ -1749,14 +1749,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1749
1749
  return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
1750
1750
 
1751
1751
  async def _fetch_flash_auth_token(self, retry: Retry) -> str:
1752
- resp = await self.client.stub.FunctionGetFlashAuthToken(
1752
+ resp = await self.client._stub.FunctionGetFlashAuthToken(
1753
1753
  api_pb2.FunctionGetFlashAuthTokenRequest(function_id=self.object_id), retry=retry
1754
1754
  )
1755
1755
  return resp.token
1756
1756
 
1757
1757
  async def _get_flash_auth_token(self) -> str:
1758
1758
  if self._flash_token_manager is None:
1759
- self._flash_token_manager = _AuthTokenManager(self.client.stub, fetch=self._fetch_flash_auth_token)
1759
+ self._flash_token_manager = _AuthTokenManager(self.client._stub, fetch=self._fetch_flash_auth_token)
1760
1760
  return await self._flash_token_manager.get_token()
1761
1761
 
1762
1762
  def _apply_dynamic_config(
@@ -2405,7 +2405,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2405
2405
  return await self._invocation().poll_function(timeout=timeout, index=index)
2406
2406
 
2407
2407
  @live_method_gen
2408
- async def iter(self, *, start: int = 0, end: int | None = None) -> AsyncIterator[ReturnType]:
2408
+ async def iter(self, *, start: int = 0, end: int | None = None) -> AsyncGenerator[ReturnType, None]:
2409
2409
  """Iterate in-order over the results of the function call.
2410
2410
 
2411
2411
  Optionally, specify a range [start, end) to iterate over.
@@ -4,10 +4,10 @@ import typing
4
4
  import uuid
5
5
  from collections.abc import Awaitable, Callable, Hashable, Sequence
6
6
  from functools import wraps
7
- from typing import ClassVar
7
+ from typing import Any, AsyncContextManager, AsyncGenerator, AsyncIterator, ClassVar, ParamSpec, TypeVar
8
8
 
9
9
  from google.protobuf.message import Message
10
- from typing_extensions import Self
10
+ from typing_extensions import Concatenate, Self
11
11
 
12
12
  from ._load_context import LoadContext
13
13
  from ._resolver import Resolver
@@ -38,18 +38,24 @@ def _get_environment_name(
38
38
  return config.get("environment") or ""
39
39
 
40
40
 
41
- def live_method(method):
41
+ P = ParamSpec("P")
42
+ T = TypeVar("T")
43
+
44
+
45
+ def live_method(method: Callable[Concatenate[Any, P], Awaitable[T]]) -> Callable[Concatenate[Any, P], Awaitable[T]]:
42
46
  @wraps(method)
43
- async def wrapped(self, *args, **kwargs):
47
+ async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> T:
44
48
  await self.hydrate()
45
49
  return await method(self, *args, **kwargs)
46
50
 
47
51
  return wrapped
48
52
 
49
53
 
50
- def live_method_gen(method):
54
+ def live_method_gen(
55
+ method: Callable[Concatenate[Any, P], AsyncGenerator[T, None]],
56
+ ) -> Callable[Concatenate[Any, P], AsyncIterator[T]]:
51
57
  @wraps(method)
52
- async def wrapped(self, *args, **kwargs):
58
+ async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncIterator[T]:
53
59
  await self.hydrate()
54
60
  async with aclosing(method(self, *args, **kwargs)) as stream:
55
61
  async for item in stream:
@@ -58,14 +64,16 @@ def live_method_gen(method):
58
64
  return wrapped
59
65
 
60
66
 
61
- def live_method_contextmanager(method):
67
+ def live_method_contextmanager(
68
+ method: Callable[Concatenate[Any, P], AsyncContextManager[T]],
69
+ ) -> Callable[Concatenate[Any, P], AsyncContextManager[T]]:
62
70
  # make sure a wrapped function returning an async context manager
63
71
  # will not require both an `await func.aio()` and `async with`
64
72
  # which would have been the case if it was wrapped in live_method
65
73
 
66
74
  @wraps(method)
67
75
  @contextlib.asynccontextmanager
68
- async def wrapped(self, *args, **kwargs):
76
+ async def wrapped(self, *args: P.args, **kwargs: P.kwargs):
69
77
  await self.hydrate()
70
78
  async with method(self, *args, **kwargs) as ctx:
71
79
  yield ctx
@@ -75,7 +83,7 @@ def live_method_contextmanager(method):
75
83
 
76
84
  class _Object:
77
85
  _type_prefix: ClassVar[str | None] = None
78
- _prefix_to_type: ClassVar[dict[str, type]] = {}
86
+ _prefix_to_type: ClassVar[dict[str, type["_Object"]]] = {}
79
87
 
80
88
  # For constructors
81
89
  _load: Callable[[Self, Resolver, LoadContext, str | None], Awaitable[None]] | None = None
@@ -13,9 +13,7 @@ from .manager import OutputManager
13
13
 
14
14
 
15
15
  def _get_annotation_for_web_url(url_info: api_pb2.WebUrlInfo) -> str:
16
- if url_info.truncated:
17
- suffix = " [grey70](label truncated)[/grey70]"
18
- elif url_info.label_stolen:
16
+ if url_info.label_stolen:
19
17
  suffix = " [grey70](label stolen)[/grey70]"
20
18
  else:
21
19
  suffix = ""
@@ -732,7 +732,7 @@ def _sessioned() -> Callable[
732
732
  [Callable[P, ReturnType] | _PartialFunction[P, ReturnType, ReturnType]],
733
733
  _PartialFunction[P, ReturnType, ReturnType],
734
734
  ]:
735
- """Decorator that enables sessions on a Server.
735
+ """Decorator that enables sticky sessions on a Server.
736
736
 
737
737
  Every request must carry a session token obtained from a session start request; requests with the same token are
738
738
  routed to the same container until the session is idle for `idle_timeout` seconds or explicitly terminated. A
@@ -1,7 +1,10 @@
1
1
  # Copyright Modal Labs 2025
2
+ import asyncio
2
3
  import inspect
3
4
  import json
5
+ import time
4
6
  import typing
7
+ import uuid
5
8
  from datetime import datetime, timedelta, timezone
6
9
 
7
10
  from typing_extensions import Self
@@ -113,7 +116,7 @@ class _Server:
113
116
 
114
117
  @property
115
118
  def sessions(self) -> "_ServerSessionsManager":
116
- """Start and terminate sessions on a Server decorated with `@modal.sessioned()`."""
119
+ """Start and terminate sticky sessions on a Server decorated with `@modal.sessioned()`."""
117
120
  return _ServerSessionsManager(self)
118
121
 
119
122
  async def info(self, *, refresh: bool = False) -> ServerInfo:
@@ -160,7 +163,7 @@ class _Server:
160
163
  This interface is experimental and may change or be removed without warning.
161
164
  """
162
165
  service_function = self._get_service_function()
163
- response = await service_function.client.stub.FlashContainerList(
166
+ response = await service_function.client._stub.FlashContainerList(
164
167
  api_pb2.FlashContainerListRequest(function_id=service_function.object_id)
165
168
  )
166
169
  return [
@@ -473,13 +476,12 @@ class _Server:
473
476
  request.until.FromDatetime(until)
474
477
  if container:
475
478
  request.container_id = container
476
- stats = await self._get_service_function().client.stub.ServerGetTimeRangeStats(request)
479
+ stats = await self._get_service_function().client._stub.ServerGetTimeRangeStats(request)
477
480
  return ServerStats._from_proto(stats)
478
481
 
479
482
 
480
- @retry(n_attempts=5, base_delay=0.5, attempt_timeout=65, total_timeout=200)
481
483
  async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int, str, str]:
482
- """POST to a session control endpoint, retrying connection errors and 5xx. Returns (status, reason, body)."""
484
+ """POST to a sticky session control endpoint, raising on 5xx."""
483
485
  async with ClientSessionRegistry.get_session().post(url, headers=headers) as resp:
484
486
  body = await resp.text()
485
487
  if resp.status >= 400:
@@ -489,6 +491,28 @@ async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int,
489
491
  return resp.status, resp.reason or "", body
490
492
 
491
493
 
494
+ async def _post_session_start(base_url: str, fn: _Function, idle_timeout: int) -> tuple[int, str, str]:
495
+ idempotency_key = str(uuid.uuid4())
496
+ start_timestamp = str(time.time())
497
+
498
+ @retry(n_attempts=5, base_delay=0.5, attempt_timeout=None, total_timeout=1515)
499
+ async def attempt() -> tuple[int, str, str]:
500
+ headers = {
501
+ "Modal-Authorization": f"Bearer {await fn._get_flash_auth_token()}",
502
+ "x-idempotency-key": idempotency_key,
503
+ "x-modal-timestamp": start_timestamp,
504
+ "x-modal-server-session-idle-timeout": str(idle_timeout),
505
+ }
506
+ return await _post_session_control(f"{base_url}/_modal/sessions/start", headers)
507
+
508
+ return await attempt()
509
+
510
+
511
+ @retry(n_attempts=3, base_delay=0.5, attempt_timeout=None, total_timeout=60)
512
+ async def _post_session_terminate(base_url: str, headers: dict[str, str]) -> tuple[int, str, str]:
513
+ return await _post_session_control(f"{base_url}/_modal/sessions/terminate", headers)
514
+
515
+
492
516
  class _ServerSessionsManager:
493
517
  """mdmd:namespace"""
494
518
 
@@ -501,11 +525,15 @@ class _ServerSessionsManager:
501
525
  raise InvalidError("`sessions` requires `@modal.sessioned()` on the Server.")
502
526
 
503
527
  async def start(self, idle_timeout: int = 600) -> ServerSessionCredentials:
504
- """Start a session and return its ID and token.
528
+ """Start a sticky session and return its ID and token.
529
+
530
+ Requests to the server URL that carry the returned token are routed to the same container until the
531
+ session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
532
+ for as long as it holds a live session.
505
533
 
506
- Requests to the server URL that carry the returned token are routed to the same container until the session
507
- has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down for as long
508
- as it holds a live session.
534
+ If no container has room for the session, the call waits for additional capacity. It will block for
535
+ up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
536
+ policy.
509
537
 
510
538
  Args:
511
539
  idle_timeout: Seconds without an in-flight request before the session ends.
@@ -531,13 +559,10 @@ class _ServerSessionsManager:
531
559
  self._validate()
532
560
  assert url is not None, "Server has no URL."
533
561
 
534
- headers = {
535
- "Modal-Authorization": f"Bearer {await fn._get_flash_auth_token()}",
536
- "x-modal-server-session-idle-timeout": str(idle_timeout),
537
- }
538
-
539
562
  try:
540
- status, reason, body = await _post_session_control(f"{url}/_modal/sessions/start", headers)
563
+ status, reason, body = await _post_session_start(url, fn, idle_timeout)
564
+ except asyncio.TimeoutError:
565
+ raise asyncio.TimeoutError("Failed to start session: timed out") from None
541
566
  except ServiceError as exc:
542
567
  raise ExecutionError(f"Failed to start session: {exc}") from None
543
568
  if status >= 400:
@@ -550,7 +575,9 @@ class _ServerSessionsManager:
550
575
  raise ExecutionError("Failed to start session: unexpected response from server") from None
551
576
 
552
577
  async def terminate(self, token: str) -> None:
553
- """Terminate a session. New requests to it will be rejected. Container will continue serving other sessions.
578
+ """Terminate a sticky session.
579
+
580
+ New requests to it will be rejected. The container continues serving other sessions.
554
581
 
555
582
  Args:
556
583
  token: The `token` of the `ServerSessionCredentials` to terminate.
@@ -574,7 +601,7 @@ class _ServerSessionsManager:
574
601
  "x-modal-server-session-token": token,
575
602
  }
576
603
  try:
577
- status, reason, _body = await _post_session_control(f"{url}/_modal/sessions/terminate", headers)
604
+ status, reason, _body = await _post_session_terminate(url, headers)
578
605
  except ServiceError as exc:
579
606
  raise ExecutionError(f"Failed to terminate session: {exc}") from None
580
607
  if status == 404:
@@ -67,7 +67,7 @@ class _Server:
67
67
 
68
68
  @property
69
69
  def sessions(self) -> _ServerSessionsManager:
70
- """Start and terminate sessions on a Server decorated with `@modal.sessioned()`."""
70
+ """Start and terminate sticky sessions on a Server decorated with `@modal.sessioned()`."""
71
71
  ...
72
72
 
73
73
  async def info(self, *, refresh: bool = False) -> modal.types.ServerInfo:
@@ -272,9 +272,14 @@ class _Server:
272
272
  ...
273
273
 
274
274
  async def _post_session_control(url: str, headers: dict[str, str]) -> tuple[int, str, str]:
275
- """POST to a session control endpoint, retrying connection errors and 5xx. Returns (status, reason, body)."""
275
+ """POST to a sticky session control endpoint, raising on 5xx."""
276
276
  ...
277
277
 
278
+ async def _post_session_start(
279
+ base_url: str, fn: modal._functions._Function, idle_timeout: int
280
+ ) -> tuple[int, str, str]: ...
281
+ async def _post_session_terminate(base_url: str, headers: dict[str, str]) -> tuple[int, str, str]: ...
282
+
278
283
  class _ServerSessionsManager:
279
284
  """mdmd:namespace"""
280
285
  def __init__(self, server: _Server):
@@ -283,11 +288,15 @@ class _ServerSessionsManager:
283
288
 
284
289
  def _validate(self) -> None: ...
285
290
  async def start(self, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
286
- """Start a session and return its ID and token.
291
+ """Start a sticky session and return its ID and token.
292
+
293
+ Requests to the server URL that carry the returned token are routed to the same container until the
294
+ session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
295
+ for as long as it holds a live session.
287
296
 
288
- Requests to the server URL that carry the returned token are routed to the same container until the session
289
- has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down for as long
290
- as it holds a live session.
297
+ If no container has room for the session, the call waits for additional capacity. It will block for
298
+ up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
299
+ policy.
291
300
 
292
301
  Args:
293
302
  idle_timeout: Seconds without an in-flight request before the session ends.
@@ -308,7 +317,9 @@ class _ServerSessionsManager:
308
317
  ...
309
318
 
310
319
  async def terminate(self, token: str) -> None:
311
- """Terminate a session. New requests to it will be rejected. Container will continue serving other sessions.
320
+ """Terminate a sticky session.
321
+
322
+ New requests to it will be rejected. The container continues serving other sessions.
312
323
 
313
324
  Args:
314
325
  token: The `token` of the `ServerSessionCredentials` to terminate.
@@ -334,11 +345,15 @@ class ServerSessionsManager:
334
345
 
335
346
  class __start_spec(typing_extensions.Protocol):
336
347
  def __call__(self, /, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
337
- """Start a session and return its ID and token.
348
+ """Start a sticky session and return its ID and token.
349
+
350
+ Requests to the server URL that carry the returned token are routed to the same container until the
351
+ session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
352
+ for as long as it holds a live session.
338
353
 
339
- Requests to the server URL that carry the returned token are routed to the same container until the session
340
- has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down for as long
341
- as it holds a live session.
354
+ If no container has room for the session, the call waits for additional capacity. It will block for
355
+ up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
356
+ policy.
342
357
 
343
358
  Args:
344
359
  idle_timeout: Seconds without an in-flight request before the session ends.
@@ -359,11 +374,15 @@ class ServerSessionsManager:
359
374
  ...
360
375
 
361
376
  async def aio(self, /, idle_timeout: int = 600) -> modal.types.ServerSessionCredentials:
362
- """Start a session and return its ID and token.
377
+ """Start a sticky session and return its ID and token.
363
378
 
364
- Requests to the server URL that carry the returned token are routed to the same container until the session
365
- has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down for as long
366
- as it holds a live session.
379
+ Requests to the server URL that carry the returned token are routed to the same container until the
380
+ session has had no connections for `idle_timeout` seconds or is terminated. A container won't be scaled down
381
+ for as long as it holds a live session.
382
+
383
+ If no container has room for the session, the call waits for additional capacity. It will block for
384
+ up to 25 minutes before giving up. To control the wait per call, use the HTTP API and apply your own retry
385
+ policy.
367
386
 
368
387
  Args:
369
388
  idle_timeout: Seconds without an in-flight request before the session ends.
@@ -387,7 +406,9 @@ class ServerSessionsManager:
387
406
 
388
407
  class __terminate_spec(typing_extensions.Protocol):
389
408
  def __call__(self, /, token: str) -> None:
390
- """Terminate a session. New requests to it will be rejected. Container will continue serving other sessions.
409
+ """Terminate a sticky session.
410
+
411
+ New requests to it will be rejected. The container continues serving other sessions.
391
412
 
392
413
  Args:
393
414
  token: The `token` of the `ServerSessionCredentials` to terminate.
@@ -404,7 +425,9 @@ class ServerSessionsManager:
404
425
  ...
405
426
 
406
427
  async def aio(self, /, token: str) -> None:
407
- """Terminate a session. New requests to it will be rejected. Container will continue serving other sessions.
428
+ """Terminate a sticky session.
429
+
430
+ New requests to it will be rejected. The container continues serving other sessions.
408
431
 
409
432
  Args:
410
433
  token: The `token` of the `ServerSessionCredentials` to terminate.
@@ -307,20 +307,19 @@ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.confi
307
307
  HTTP/2 keepalive probes keep idle transports warm through stateful
308
308
  middleboxes and surface dead connections before retries reuse them.
309
309
 
310
- `sustained_keepalive` clears the two grpclib defaults such that a probe
311
- actually goes out every `_keepalive_time`, for as long as the connection is
312
- open.
310
+ A probe goes out every `_keepalive_time` while the connection keeps sending
311
+ requests. `sustained_keepalive` also keeps probing a connection that only
312
+ awaits responses, for as long as it is open.
313
313
  """
314
- ping_throttle_overrides: dict[str, Any] = {}
314
+ ping_throttle_overrides: dict[str, Any] = {
315
+ # Default 300s floor between probes, applied regardless of data
316
+ # despite the name, so it has to stay under `_keepalive_time`.
317
+ "_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
318
+ }
315
319
  if sustained_keepalive:
316
- ping_throttle_overrides = {
317
- # Default 2: probing stops after two probes with no request sent in
318
- # between. Requests reset the count, acks don't. 0 = no cap.
319
- "_http2_max_pings_without_data": 0,
320
- # Default 300s floor between probes, applied regardless of data
321
- # despite the name, so it has to stay under `_keepalive_time`.
322
- "_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
323
- }
320
+ # Default 2: probing stops after two probes with no request sent in
321
+ # between. Requests reset the count, acks don't. 0 = no cap.
322
+ ping_throttle_overrides["_http2_max_pings_without_data"] = 0
324
323
 
325
324
  return grpclib.config.Configuration(
326
325
  _keepalive_time=GRPC_KEEPALIVE_TIME_SECS,
@@ -226,6 +226,7 @@ async def fetch_command_router_access_v2(
226
226
  *,
227
227
  sandbox_id: str | None = None,
228
228
  task_id: str | None = None,
229
+ sandbox_token: str | None = None,
229
230
  ) -> api_pb2.SandboxGetCommandRouterAccessResponse:
230
231
  """Fetch direct command router access info from Modal server for a V2 sandbox."""
231
232
  if sandbox_id is not None:
@@ -236,10 +237,10 @@ async def fetch_command_router_access_v2(
236
237
  raise ValueError("Either sandbox_id or task_id must be provided")
237
238
  assert server_client._auth_token_manager
238
239
  auth_token = await server_client._auth_token_manager.get_token()
239
- return await server_client._stub.SandboxGetCommandRouterAccess(
240
- request,
241
- metadata=[("x-modal-auth-token", auth_token)],
242
- )
240
+ metadata = [("x-modal-auth-token", auth_token)]
241
+ if sandbox_token:
242
+ metadata.append(("x-modal-sandbox-token", sandbox_token))
243
+ return await server_client._stub.SandboxGetCommandRouterAccess(request, metadata=metadata)
243
244
 
244
245
 
245
246
  def _finalize_channel(loop, channel):
@@ -267,6 +268,7 @@ class TaskCommandRouterClient:
267
268
  jwt: str,
268
269
  *,
269
270
  sandbox_id: str | None = None,
271
+ sandbox_token: str | None = None,
270
272
  ) -> "TaskCommandRouterClient":
271
273
  """Build a connected client from a jwt and url."""
272
274
  o = urllib.parse.urlparse(url)
@@ -315,7 +317,17 @@ class TaskCommandRouterClient:
315
317
  loop = asyncio.get_running_loop()
316
318
  jwt_refresh_lock = asyncio.Lock()
317
319
 
318
- client = cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
320
+ client = cls(
321
+ server_client,
322
+ task_id,
323
+ url,
324
+ jwt,
325
+ channel,
326
+ loop,
327
+ jwt_refresh_lock,
328
+ sandbox_id=sandbox_id,
329
+ sandbox_token=sandbox_token,
330
+ )
319
331
  # An unused client releases its connection too.
320
332
  client._arm_idle_release()
321
333
  return client
@@ -337,6 +349,8 @@ class TaskCommandRouterClient:
337
349
  sandbox_id: str,
338
350
  task_id: str,
339
351
  access: api_pb2.CommandRouterAccess | None = None,
352
+ *,
353
+ sandbox_token: str | None = None,
340
354
  ) -> "TaskCommandRouterClient":
341
355
  """Initialize a TaskCommandRouterClient for a V2 sandbox.
342
356
 
@@ -348,10 +362,12 @@ class TaskCommandRouterClient:
348
362
  if access is not None:
349
363
  url, jwt = access.url, access.jwt
350
364
  else:
351
- resp = await fetch_command_router_access_v2(server_client, sandbox_id=sandbox_id)
365
+ resp = await fetch_command_router_access_v2(
366
+ server_client, sandbox_id=sandbox_id, sandbox_token=sandbox_token
367
+ )
352
368
  url, jwt = resp.url, resp.jwt
353
369
  logger.debug(f"Using command router access for sandbox {sandbox_id}")
354
- return await cls._connect(server_client, task_id, url, jwt, sandbox_id=sandbox_id)
370
+ return await cls._connect(server_client, task_id, url, jwt, sandbox_id=sandbox_id, sandbox_token=sandbox_token)
355
371
 
356
372
  @classmethod
357
373
  async def init_v2_by_task_id(
@@ -375,6 +391,7 @@ class TaskCommandRouterClient:
375
391
  jwt_refresh_lock: asyncio.Lock,
376
392
  *,
377
393
  sandbox_id: str | None = None,
394
+ sandbox_token: str | None = None,
378
395
  stream_stdio_retry_delay_secs: float = 0.01,
379
396
  stream_stdio_retry_delay_factor: float = 2,
380
397
  stream_stdio_max_retries: int = 10,
@@ -388,6 +405,7 @@ class TaskCommandRouterClient:
388
405
  self._server_client = server_client
389
406
  self._task_id = task_id
390
407
  self._sandbox_id = sandbox_id
408
+ self._sandbox_token = sandbox_token
391
409
  self._server_url = server_url
392
410
  self._jwt = jwt
393
411
  self._channel = channel
@@ -902,7 +920,10 @@ class TaskCommandRouterClient:
902
920
  if self._is_v2_sandbox:
903
921
  logger.debug(f"Refreshing JWT for V2 exec with task ID {self._task_id}")
904
922
  v2_resp = await fetch_command_router_access_v2(
905
- self._server_client, sandbox_id=self._sandbox_id, task_id=self._task_id
923
+ self._server_client,
924
+ sandbox_id=self._sandbox_id,
925
+ task_id=self._task_id,
926
+ sandbox_token=self._sandbox_token,
906
927
  )
907
928
  logger.debug(f"Finished refreshing JWT for V2 exec with task ID {self._task_id}")
908
929
  jwt, url = v2_resp.jwt, v2_resp.url