modal 1.6.2.dev13__tar.gz → 1.6.3__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 (253) hide show
  1. {modal-1.6.2.dev13 → modal-1.6.3}/PKG-INFO +4 -2
  2. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_environments.py +5 -1
  3. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_image.py +251 -72
  4. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/auth_token_manager.py +8 -4
  5. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/docker_utils.py +36 -0
  6. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/grpc_testing.py +3 -1
  7. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/grpc_utils.py +23 -4
  8. modal-1.6.3/modal/_utils/oauth.py +66 -0
  9. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_workspace.py +65 -1
  10. {modal-1.6.2.dev13 → modal-1.6.3}/modal/app.py +39 -19
  11. {modal-1.6.2.dev13 → modal-1.6.3}/modal/app.pyi +59 -0
  12. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2025.06.txt +1 -1
  13. modal-1.6.3/modal/builder/2026.10.txt +17 -0
  14. modal-1.6.3/modal/builder/PREVIEW.txt +17 -0
  15. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/base-images.json +5 -11
  16. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/config.py +2 -0
  17. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/endpoint.py +49 -7
  18. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/environment.py +21 -5
  19. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/profile.py +2 -1
  20. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/token.py +1 -0
  21. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/workspace.py +101 -1
  22. {modal-1.6.2.dev13 → modal-1.6.3}/modal/client.py +39 -9
  23. {modal-1.6.2.dev13 → modal-1.6.3}/modal/client.pyi +42 -8
  24. {modal-1.6.2.dev13 → modal-1.6.3}/modal/config.py +3 -1
  25. {modal-1.6.2.dev13 → modal-1.6.3}/modal/environments.pyi +2 -0
  26. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/__init__.py +6 -5
  27. {modal-1.6.2.dev13 → modal-1.6.3}/modal/image.pyi +52 -20
  28. {modal-1.6.2.dev13 → modal-1.6.3}/modal/io_streams.py +26 -14
  29. {modal-1.6.2.dev13 → modal-1.6.3}/modal/io_streams.pyi +2 -0
  30. {modal-1.6.2.dev13 → modal-1.6.3}/modal/mount.py +43 -13
  31. {modal-1.6.2.dev13 → modal-1.6.3}/modal/mount.pyi +10 -8
  32. {modal-1.6.2.dev13 → modal-1.6.3}/modal/network_file_system.py +4 -8
  33. {modal-1.6.2.dev13 → modal-1.6.3}/modal/network_file_system.pyi +10 -16
  34. {modal-1.6.2.dev13 → modal-1.6.3}/modal/runner.py +10 -25
  35. modal-1.6.3/modal/sandbox/_common.py +1134 -0
  36. modal-1.6.3/modal/sandbox/_impl_persistent.py +470 -0
  37. modal-1.6.3/modal/sandbox/_impl_v1.py +523 -0
  38. modal-1.6.3/modal/sandbox/_impl_v2.py +623 -0
  39. modal-1.6.3/modal/sandbox/_sandbox.py +1492 -0
  40. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_sidecar.py +67 -214
  41. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_task_command_router_client.py +88 -128
  42. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/sandbox.pyi +114 -318
  43. {modal-1.6.2.dev13 → modal-1.6.3}/modal/token_flow.py +1 -0
  44. {modal-1.6.2.dev13 → modal-1.6.3}/modal/types.py +23 -0
  45. {modal-1.6.2.dev13 → modal-1.6.3}/modal/workspace.py +3 -0
  46. {modal-1.6.2.dev13 → modal-1.6.3}/modal/workspace.pyi +93 -0
  47. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/PKG-INFO +4 -2
  48. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/SOURCES.txt +5 -0
  49. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/requires.txt +4 -1
  50. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_grpc.py +160 -16
  51. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2.py +1711 -1436
  52. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2.pyi +794 -115
  53. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2_grpc.py +332 -35
  54. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/api_pb2_grpc.pyi +105 -15
  55. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/modal_api_grpc.py +10 -1
  56. {modal-1.6.2.dev13 → modal-1.6.3}/modal_version/__init__.py +1 -1
  57. {modal-1.6.2.dev13 → modal-1.6.3}/pyproject.toml +8 -4
  58. modal-1.6.2.dev13/modal/builder/PREVIEW.txt +0 -21
  59. modal-1.6.2.dev13/modal/sandbox/_common.py +0 -248
  60. modal-1.6.2.dev13/modal/sandbox/_sandbox.py +0 -2590
  61. {modal-1.6.2.dev13 → modal-1.6.3}/LICENSE +0 -0
  62. {modal-1.6.2.dev13 → modal-1.6.3}/README.md +0 -0
  63. {modal-1.6.2.dev13 → modal-1.6.3}/modal/__init__.py +0 -0
  64. {modal-1.6.2.dev13 → modal-1.6.3}/modal/__main__.py +0 -0
  65. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_billing.py +0 -0
  66. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_cluster.py +0 -0
  67. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_cluster.pyi +0 -0
  68. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_container_entrypoint.py +0 -0
  69. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_function_variants.py +0 -0
  70. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_functions.py +0 -0
  71. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_grpc_client.py +0 -0
  72. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_ipython.py +0 -0
  73. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_load_context.py +0 -0
  74. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_location.py +0 -0
  75. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs.py +0 -0
  76. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs_manager.py +0 -0
  77. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_logs_manager.pyi +0 -0
  78. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_metrics.py +0 -0
  79. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_metrics.pyi +0 -0
  80. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_object.py +0 -0
  81. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_outbound_policy.py +0 -0
  82. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_outbound_policy.pyi +0 -0
  83. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/__init__.py +0 -0
  84. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/manager.py +0 -0
  85. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/pty.py +0 -0
  86. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/rich.py +0 -0
  87. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_output/status.py +0 -0
  88. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_partial_function.py +0 -0
  89. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_resolver.py +0 -0
  90. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_resources.py +0 -0
  91. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/__init__.py +0 -0
  92. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/asgi.py +0 -0
  93. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/container_io_manager.py +0 -0
  94. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/container_io_manager.pyi +0 -0
  95. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/execution_context.py +0 -0
  96. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/execution_context.pyi +0 -0
  97. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  98. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/task_lifecycle_manager.py +0 -0
  99. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  100. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/telemetry.py +0 -0
  101. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/user_code_event_loop.py +0 -0
  102. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_runtime/user_code_imports.py +0 -0
  103. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_serialization.py +0 -0
  104. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_server.py +0 -0
  105. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_server.pyi +0 -0
  106. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_supports_logs.py +0 -0
  107. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_traceback.py +0 -0
  108. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_tunnel.py +0 -0
  109. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_tunnel.pyi +0 -0
  110. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_type_manager.py +0 -0
  111. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/__init__.py +0 -0
  112. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/app_utils.py +0 -0
  113. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/async_utils.py +0 -0
  114. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/blob_utils.py +0 -0
  115. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/browser_utils.py +0 -0
  116. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/bytes_io_segment_payload.py +0 -0
  117. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/curl_utils.py +0 -0
  118. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/deprecation.py +0 -0
  119. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/display_utils.py +0 -0
  120. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/function_utils.py +0 -0
  121. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/git_utils.py +0 -0
  122. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/hash_utils.py +0 -0
  123. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/http_utils.py +0 -0
  124. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/idle_countdown.py +0 -0
  125. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/jwt_utils.py +0 -0
  126. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/logger.py +0 -0
  127. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/mount_utils.py +0 -0
  128. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/name_utils.py +0 -0
  129. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/package_utils.py +0 -0
  130. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/pattern_utils.py +0 -0
  131. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/proxy_support.py +0 -0
  132. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/rand_pb_testing.py +0 -0
  133. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/sandbox_fs_utils.py +0 -0
  134. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/shell_utils.py +0 -0
  135. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_utils/time_utils.py +0 -0
  136. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/__init__.py +0 -0
  137. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  138. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/cloudpickle.py +0 -0
  139. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/tblib.py +0 -0
  140. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_vendor/version.py +0 -0
  141. {modal-1.6.2.dev13 → modal-1.6.3}/modal/_watcher.py +0 -0
  142. {modal-1.6.2.dev13 → modal-1.6.3}/modal/billing.py +0 -0
  143. {modal-1.6.2.dev13 → modal-1.6.3}/modal/billing.pyi +0 -0
  144. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2023.12.312.txt +0 -0
  145. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2023.12.txt +0 -0
  146. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2024.04.txt +0 -0
  147. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/2024.10.txt +0 -0
  148. {modal-1.6.2.dev13 → modal-1.6.3}/modal/builder/README.md +0 -0
  149. {modal-1.6.2.dev13 → modal-1.6.3}/modal/call_graph.py +0 -0
  150. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/__init__.py +0 -0
  151. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_download.py +0 -0
  152. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_help.py +0 -0
  153. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_logs.py +0 -0
  154. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_stats.py +0 -0
  155. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/_traceback.py +0 -0
  156. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/app.py +0 -0
  157. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/billing.py +0 -0
  158. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/changelog.py +0 -0
  159. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/container.py +0 -0
  160. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/curl.py +0 -0
  161. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/dashboard.py +0 -0
  162. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/dict.py +0 -0
  163. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/entry_point.py +0 -0
  164. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/function.py +0 -0
  165. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/image.py +0 -0
  166. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/import_refs.py +0 -0
  167. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/logo.py +0 -0
  168. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/network_file_system.py +0 -0
  169. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/queues.py +0 -0
  170. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/run.py +0 -0
  171. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/secret.py +0 -0
  172. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/selector.py +0 -0
  173. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/server.py +0 -0
  174. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/shell.py +0 -0
  175. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/skills.py +0 -0
  176. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/utils.py +0 -0
  177. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cli/volume.py +0 -0
  178. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cloud_bucket_mount.py +0 -0
  179. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cloud_bucket_mount.pyi +0 -0
  180. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cls.py +0 -0
  181. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cls.pyi +0 -0
  182. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cluster.py +0 -0
  183. {modal-1.6.2.dev13 → modal-1.6.3}/modal/cluster.pyi +0 -0
  184. {modal-1.6.2.dev13 → modal-1.6.3}/modal/container_process.py +0 -0
  185. {modal-1.6.2.dev13 → modal-1.6.3}/modal/container_process.pyi +0 -0
  186. {modal-1.6.2.dev13 → modal-1.6.3}/modal/dict.py +0 -0
  187. {modal-1.6.2.dev13 → modal-1.6.3}/modal/dict.pyi +0 -0
  188. {modal-1.6.2.dev13 → modal-1.6.3}/modal/environments.py +0 -0
  189. {modal-1.6.2.dev13 → modal-1.6.3}/modal/exception.py +0 -0
  190. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/_flash_container_entry.py +0 -0
  191. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash.py +0 -0
  192. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash.pyi +0 -0
  193. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash_server.py +0 -0
  194. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/flash_server.pyi +0 -0
  195. {modal-1.6.2.dev13 → modal-1.6.3}/modal/experimental/ipython.py +0 -0
  196. {modal-1.6.2.dev13 → modal-1.6.3}/modal/file_pattern_matcher.py +0 -0
  197. {modal-1.6.2.dev13 → modal-1.6.3}/modal/functions.py +0 -0
  198. {modal-1.6.2.dev13 → modal-1.6.3}/modal/functions.pyi +0 -0
  199. {modal-1.6.2.dev13 → modal-1.6.3}/modal/image.py +0 -0
  200. {modal-1.6.2.dev13 → modal-1.6.3}/modal/object.py +0 -0
  201. {modal-1.6.2.dev13 → modal-1.6.3}/modal/object.pyi +0 -0
  202. {modal-1.6.2.dev13 → modal-1.6.3}/modal/output.py +0 -0
  203. {modal-1.6.2.dev13 → modal-1.6.3}/modal/parallel_map.py +0 -0
  204. {modal-1.6.2.dev13 → modal-1.6.3}/modal/parallel_map.pyi +0 -0
  205. {modal-1.6.2.dev13 → modal-1.6.3}/modal/partial_function.py +0 -0
  206. {modal-1.6.2.dev13 → modal-1.6.3}/modal/partial_function.pyi +0 -0
  207. {modal-1.6.2.dev13 → modal-1.6.3}/modal/proxy.py +0 -0
  208. {modal-1.6.2.dev13 → modal-1.6.3}/modal/proxy.pyi +0 -0
  209. {modal-1.6.2.dev13 → modal-1.6.3}/modal/py.typed +0 -0
  210. {modal-1.6.2.dev13 → modal-1.6.3}/modal/queue.py +0 -0
  211. {modal-1.6.2.dev13 → modal-1.6.3}/modal/queue.pyi +0 -0
  212. {modal-1.6.2.dev13 → modal-1.6.3}/modal/retries.py +0 -0
  213. {modal-1.6.2.dev13 → modal-1.6.3}/modal/runner.pyi +0 -0
  214. {modal-1.6.2.dev13 → modal-1.6.3}/modal/running_app.py +0 -0
  215. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/__init__.py +0 -0
  216. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_filesystem.py +0 -0
  217. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/_snapshot.py +0 -0
  218. {modal-1.6.2.dev13 → modal-1.6.3}/modal/sandbox/sandbox.py +0 -0
  219. {modal-1.6.2.dev13 → modal-1.6.3}/modal/schedule.py +0 -0
  220. {modal-1.6.2.dev13 → modal-1.6.3}/modal/scheduler_placement.py +0 -0
  221. {modal-1.6.2.dev13 → modal-1.6.3}/modal/secret.py +0 -0
  222. {modal-1.6.2.dev13 → modal-1.6.3}/modal/secret.pyi +0 -0
  223. {modal-1.6.2.dev13 → modal-1.6.3}/modal/server.py +0 -0
  224. {modal-1.6.2.dev13 → modal-1.6.3}/modal/server.pyi +0 -0
  225. {modal-1.6.2.dev13 → modal-1.6.3}/modal/serving.py +0 -0
  226. {modal-1.6.2.dev13 → modal-1.6.3}/modal/serving.pyi +0 -0
  227. {modal-1.6.2.dev13 → modal-1.6.3}/modal/skills/modal/SKILL.md +0 -0
  228. {modal-1.6.2.dev13 → modal-1.6.3}/modal/stream_type.py +0 -0
  229. {modal-1.6.2.dev13 → modal-1.6.3}/modal/token_flow.pyi +0 -0
  230. {modal-1.6.2.dev13 → modal-1.6.3}/modal/volume.py +0 -0
  231. {modal-1.6.2.dev13 → modal-1.6.3}/modal/volume.pyi +0 -0
  232. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/dependency_links.txt +0 -0
  233. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/entry_points.txt +0 -0
  234. {modal-1.6.2.dev13 → modal-1.6.3}/modal.egg-info/top_level.txt +0 -0
  235. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/__init__.py +0 -0
  236. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_cli_docs.py +0 -0
  237. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_cli_docs_main.py +0 -0
  238. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_reference_docs.py +0 -0
  239. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/gen_reference_docs_main.py +0 -0
  240. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/__init__.py +0 -0
  241. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/mdmd.py +0 -0
  242. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/signatures.py +0 -0
  243. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/mdmd/types.py +0 -0
  244. {modal-1.6.2.dev13 → modal-1.6.3}/modal_docs/reference_intro.md +0 -0
  245. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/__init__.py +0 -0
  246. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/py.typed +0 -0
  247. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_grpc.py +0 -0
  248. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2.py +0 -0
  249. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2.pyi +0 -0
  250. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  251. {modal-1.6.2.dev13 → modal-1.6.3}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  252. {modal-1.6.2.dev13 → modal-1.6.3}/modal_version/__main__.py +0 -0
  253. {modal-1.6.2.dev13 → modal-1.6.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.2.dev13
3
+ Version: 1.6.3
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,<8.0,>=3.19
25
25
  Requires-Dist: rich>=12.0.0
26
- Requires-Dist: synchronicity~=0.12.6
26
+ Requires-Dist: synchronicity~=0.12.7
27
27
  Requires-Dist: toml
28
28
  Requires-Dist: types-certifi
29
29
  Requires-Dist: types-toml
@@ -33,6 +33,8 @@ Requires-Dist: typing_extensions>=4.14; python_version >= "3.15"
33
33
  Provides-Extra: api-proxy-support
34
34
  Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
35
35
  Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
36
+ Provides-Extra: oauth
37
+ Requires-Dist: pyjwt[crypto]>=2.7; extra == "oauth"
36
38
  Dynamic: license-file
37
39
 
38
40
  # Modal Python SDK
@@ -648,6 +648,7 @@ async def _update_environment(
648
648
  *,
649
649
  new_name: str | None = None,
650
650
  new_web_suffix: str | None = None,
651
+ new_image_builder_version: str | None = None,
651
652
  client: _Client | None = None,
652
653
  ):
653
654
  new_name_pb2 = None
@@ -662,7 +663,10 @@ async def _update_environment(
662
663
  new_web_suffix_pb2 = StringValue(value=new_web_suffix)
663
664
 
664
665
  update_payload = api_pb2.EnvironmentUpdateRequest(
665
- current_name=current_name, name=new_name_pb2, web_suffix=new_web_suffix_pb2
666
+ current_name=current_name,
667
+ name=new_name_pb2,
668
+ web_suffix=new_web_suffix_pb2,
669
+ image_builder_version=new_image_builder_version,
666
670
  )
667
671
  if client is None:
668
672
  client = await _Client.from_env()
@@ -40,6 +40,8 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
40
40
  from ._utils.docker_utils import (
41
41
  extract_copy_command_patterns,
42
42
  find_dockerignore_file,
43
+ is_multi_stage_dockerfile,
44
+ splittable_from_line_index,
43
45
  )
44
46
  from ._utils.function_utils import FunctionSourceInfo, parse_gpu_config
45
47
  from ._utils.mount_utils import validate_only_modal_volumes, validate_volumes_by_object_id
@@ -57,7 +59,7 @@ from .exception import (
57
59
  VersionError,
58
60
  )
59
61
  from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
60
- from .mount import _Mount, python_standalone_mount_name
62
+ from .mount import PYTHON_STANDALONE_VERSIONS, _Mount, python_standalone_mount_name
61
63
  from .network_file_system import _NetworkFileSystem
62
64
  from .output import OutputManager
63
65
  from .secret import _Secret
@@ -69,14 +71,16 @@ if typing.TYPE_CHECKING:
69
71
  import modal.client
70
72
 
71
73
  # This is used for both type checking and runtime validation
72
- ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
74
+ ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "2026.10", "PREVIEW"]
75
+ LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2026.10"
73
76
 
74
77
  # Note: we also define supported Python versions via logic at the top of the package __init__.py
75
78
  # so that we fail fast / clearly in unsupported containers. Additionally, we enumerate the supported
76
79
  # Python versions in mount.py where we specify the "standalone Python versions" we create mounts for.
77
80
  # Consider consolidating these multiple sources of truth?
78
81
  SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
79
- "PREVIEW": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
82
+ "PREVIEW": ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
83
+ "2026.10": ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
80
84
  "2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
81
85
  "2024.10": ["3.10", "3.11", "3.12", "3.13"],
82
86
  "2024.04": ["3.10", "3.11", "3.12"],
@@ -173,6 +177,7 @@ def _validate_python_version(
173
177
  allow_free_threading: bool = False,
174
178
  caller_name: str = "",
175
179
  ) -> str:
180
+ python_version_unspecified = python_version is None
176
181
  if python_version is None:
177
182
  # If Python version is unspecified, match the local version, up to the minor component
178
183
  python_version = series_version = "{}.{}".format(*sys.version_info)
@@ -195,6 +200,12 @@ def _validate_python_version(
195
200
 
196
201
  supported_series = SUPPORTED_PYTHON_SERIES[builder_version]
197
202
  if series_version not in supported_series:
203
+ if python_version_unspecified:
204
+ raise InvalidError(
205
+ f"Python {series_version} is not supported by the {builder_version!r} Image builder."
206
+ f" Since `python_version` was not set, Modal tried to match your local Python ({series_version})."
207
+ f" Set `python_version` to one of the supported series: {supported_series!r}."
208
+ )
198
209
  raise InvalidError(
199
210
  f"Unsupported Python version: {python_version!r}."
200
211
  f" When using the {builder_version!r} Image builder, Modal supports the following series:"
@@ -212,23 +223,32 @@ def _dockerhub_python_version(
212
223
  python_version = _validate_python_version(
213
224
  python_version, builder_version, allow_free_threading=allow_free_threading, caller_name=caller_name
214
225
  )
226
+
215
227
  version_components = python_version.split(".")
216
228
 
217
- # When user specifies a full Python version, use that
218
- if len(version_components) > 2:
229
+ # - Users that want a specific patch version for reproducibility guarantees can specify that up
230
+ # front
231
+ # - Users on the newest builder can force-rebuild to get the latest patch versions, allowing
232
+ # quick responses to upstream bug fixes / CVEs.
233
+ if builder_version > "2025.06" or len(version_components) > 2:
219
234
  return python_version
220
235
 
221
236
  # Otherwise, use the same series, but a specific micro version, corresponding to the latest
222
237
  # available from https://hub.docker.com/_/python at the time of each image builder release.
223
238
  # This allows us to publish one pre-built debian-slim image per Python series.
224
239
  python_versions = _base_image_config("python", builder_version)
225
- series_to_micro_version = dict(tuple(v.rsplit(".", 1)) for v in python_versions)
240
+ series_to_micro_version: dict[str, str] = {}
241
+ for v in python_versions:
242
+ series, micro = v.rsplit(".", 1)
243
+ series_to_micro_version[series] = micro
226
244
  python_series_requested = "{}.{}".format(*version_components)
227
245
  micro_version = series_to_micro_version[python_series_requested]
228
246
  return f"{python_series_requested}.{micro_version}"
229
247
 
230
248
 
231
249
  def _base_image_config(group: str, builder_version: ImageBuilderVersion) -> Any:
250
+ # data["python"] does not include image builder versions more recent than `2025.06`, as those do
251
+ # not rely on hardcoded patch versions.
232
252
  with open(LOCAL_REQUIREMENTS_DIR / "base-images.json") as f:
233
253
  data = json.load(f)
234
254
  return data[group][builder_version]
@@ -339,6 +359,13 @@ def _get_image_builder_version(server_version: ImageBuilderVersion) -> ImageBuil
339
359
  return version
340
360
 
341
361
 
362
+ async def _resolve_image_builder_version(load_context: LoadContext) -> ImageBuilderVersion:
363
+ environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
364
+ # A bit hacky,but assume that the environment provides a valid builder version
365
+ image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
366
+ return _get_image_builder_version(image_builder_version)
367
+
368
+
342
369
  def _create_context_mount(
343
370
  docker_commands: Sequence[str],
344
371
  ignore_fn: Callable[[Path], bool],
@@ -393,7 +420,7 @@ def _create_context_mount_function(
393
420
 
394
421
  if ignore is AUTO_DOCKERIGNORE:
395
422
 
396
- def auto_created_context_mount_fn() -> _Mount | None:
423
+ def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
397
424
  nonlocal context_dir
398
425
  context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
399
426
  dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
@@ -410,7 +437,7 @@ def _create_context_mount_function(
410
437
 
411
438
  else:
412
439
 
413
- def auto_created_context_mount_fn() -> _Mount | None:
440
+ def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
414
441
  # use COPY commands and ignore patterns to construct implicit context mount
415
442
  nonlocal context_dir
416
443
  context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
@@ -420,6 +447,40 @@ def _create_context_mount_function(
420
447
  return auto_created_context_mount_fn
421
448
 
422
449
 
450
+ _REGISTRY_AUTH_KEY_SETS: dict["api_pb2.RegistryAuthType.ValueType", list[set[str]]] = {
451
+ api_pb2.REGISTRY_AUTH_TYPE_AWS: [
452
+ {"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"},
453
+ {"AWS_ROLE_ARN", "AWS_REGION"},
454
+ ],
455
+ api_pb2.REGISTRY_AUTH_TYPE_GCP: [{"SERVICE_ACCOUNT_JSON"}],
456
+ api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: [{"REGISTRY_USERNAME", "REGISTRY_PASSWORD"}],
457
+ }
458
+
459
+ _REGISTRY_AUTH_NAMES = {
460
+ api_pb2.REGISTRY_AUTH_TYPE_AWS: "AWS",
461
+ api_pb2.REGISTRY_AUTH_TYPE_GCP: "GCP",
462
+ api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: "username and password",
463
+ }
464
+
465
+ _REGISTRY_SECRET_KEYS_HELP = (
466
+ "Registry Secret must include exactly one of: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION "
467
+ "(or AWS_ROLE_ARN and AWS_REGION) for AWS ECR; SERVICE_ACCOUNT_JSON for GCP Artifact Registry; "
468
+ "or REGISTRY_USERNAME and REGISTRY_PASSWORD for other registries."
469
+ )
470
+
471
+
472
+ def _infer_registry_auth_type(keys: set[str]) -> "api_pb2.RegistryAuthType.ValueType":
473
+ matches = [auth for auth, key_sets in _REGISTRY_AUTH_KEY_SETS.items() if any(ks <= keys for ks in key_sets)]
474
+ if not matches:
475
+ raise InvalidError(f"{_REGISTRY_SECRET_KEYS_HELP} Found keys: {', '.join(sorted(keys)) or 'none'}.")
476
+ if len(matches) > 1:
477
+ names = ", ".join(_REGISTRY_AUTH_NAMES[auth] for auth in matches)
478
+ raise InvalidError(
479
+ f"Registry Secret has keys for more than one kind of authentication ({names}). {_REGISTRY_SECRET_KEYS_HELP}"
480
+ )
481
+ return matches[0]
482
+
483
+
423
484
  class _ImageRegistryConfig:
424
485
  """mdmd:hidden"""
425
486
 
@@ -428,13 +489,21 @@ class _ImageRegistryConfig:
428
489
  # TODO: change to _PUBLIC after worker starts handling it.
429
490
  registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
430
491
  secret: _Secret | None = None,
492
+ *,
493
+ infer_auth_type: bool = False,
431
494
  ):
432
495
  self.registry_auth_type = registry_auth_type
433
496
  self.secret = secret
434
-
435
- def get_proto(self) -> api_pb2.ImageRegistryConfig:
497
+ self.infer_auth_type = infer_auth_type
498
+
499
+ async def get_proto(self) -> api_pb2.ImageRegistryConfig:
500
+ registry_auth_type = self.registry_auth_type
501
+ if self.infer_auth_type and self.secret:
502
+ # The Secret is an Image dependency, so it's hydrated by now and its keys are cached
503
+ keys = await self.secret._get_keys()
504
+ registry_auth_type = _infer_registry_auth_type(keys)
436
505
  return api_pb2.ImageRegistryConfig(
437
- registry_auth_type=self.registry_auth_type,
506
+ registry_auth_type=registry_auth_type,
438
507
  secret_id=(self.secret.object_id if self.secret else ""),
439
508
  )
440
509
 
@@ -613,7 +682,7 @@ class _Image(_Object, type_prefix="im"):
613
682
  build_function: "modal._functions._Function | None" = None,
614
683
  build_function_input: api_pb2.FunctionInput | None = None,
615
684
  image_registry_config: _ImageRegistryConfig | None = None,
616
- context_mount_function: Callable[[], _Mount | None] | None = None,
685
+ context_mount_function: Callable[[ImageBuilderVersion], _Mount | None] | None = None,
617
686
  force_build: bool = False,
618
687
  build_args: dict[str, str] = {},
619
688
  validated_volumes: Sequence[tuple[str, _Volume]] | None = None,
@@ -652,7 +721,10 @@ class _Image(_Object, type_prefix="im"):
652
721
  return deps
653
722
 
654
723
  async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
655
- context_mount = context_mount_function() if context_mount_function else None
724
+ assert load_context.app_id # type narrowing
725
+ builder_version = await _resolve_image_builder_version(load_context)
726
+
727
+ context_mount = context_mount_function(builder_version) if context_mount_function else None
656
728
  if context_mount:
657
729
  await resolver.load(context_mount, load_context)
658
730
 
@@ -661,12 +733,6 @@ class _Image(_Object, type_prefix="im"):
661
733
  # base images can't have
662
734
  image._assert_no_mount_layers()
663
735
 
664
- assert load_context.app_id # type narrowing
665
- environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
666
- # A bit hacky,but assume that the environment provides a valid builder version
667
- image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
668
- builder_version = _get_image_builder_version(image_builder_version)
669
-
670
736
  if dockerfile_function is None:
671
737
  dockerfile = DockerfileSpec(commands=[], context_files={})
672
738
  else:
@@ -740,7 +806,7 @@ class _Image(_Object, type_prefix="im"):
740
806
  secret_ids=[secret.object_id for secret in secrets],
741
807
  context_mount_id=(context_mount.object_id if context_mount else ""),
742
808
  gpu_config=gpu_config,
743
- image_registry_config=image_registry_config.get_proto(),
809
+ image_registry_config=await image_registry_config.get_proto(),
744
810
  runtime=config.get("function_runtime"),
745
811
  runtime_debug=config.get("function_runtime_debug"),
746
812
  build_function=_build_function,
@@ -838,7 +904,7 @@ class _Image(_Object, type_prefix="im"):
838
904
  return _Image._from_args(
839
905
  base_images={"base": self},
840
906
  dockerfile_function=build_dockerfile,
841
- context_mount_function=lambda: mount,
907
+ context_mount_function=lambda builder_version: mount,
842
908
  )
843
909
 
844
910
  @_requires_image_instance
@@ -2182,12 +2248,9 @@ class _Image(_Object, type_prefix="im"):
2182
2248
  "COPY /python/. /usr/local",
2183
2249
  "ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
2184
2250
  ]
2185
- python_minor = add_python.split(".")[1]
2186
- if python_minor.endswith("t"):
2187
- python_minor = python_minor[:-1]
2188
- if int(python_minor) < 13:
2189
- # Previous versions did not include the `python` binary, but later ones do.
2190
- # (The important factor is not the Python version itself, but the standalone dist version.)
2251
+ release, _ = PYTHON_STANDALONE_VERSIONS[builder_version][add_python]
2252
+ if release < "20241008":
2253
+ # Older standalone releases do not include the `python` binary.
2191
2254
  # We insert the command in the list at the position it was previously always added
2192
2255
  # for backwards compatibility with existing images.
2193
2256
  add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
@@ -2236,16 +2299,21 @@ class _Image(_Object, type_prefix="im"):
2236
2299
  remaining commands run. This might be useful if you want a custom Python installation or to
2237
2300
  set a `SHELL`. Prefer `run_commands()` when possible though.
2238
2301
 
2239
- To authenticate against a private registry with static credentials, you must set the `secret` parameter to
2240
- a `modal.Secret` containing a username (`REGISTRY_USERNAME`) and
2241
- an access token or password (`REGISTRY_PASSWORD`).
2302
+ To pull from a private registry, set the `secret` parameter to a `modal.Secret` holding
2303
+ registry credentials. The kind of authentication follows from the keys in the Secret:
2304
+
2305
+ - AWS ECR: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` (optionally `AWS_SESSION_TOKEN`),
2306
+ or `AWS_ROLE_ARN` and `AWS_REGION` to assume a role with [OIDC](https://modal.com/docs/guide/oidc-integration).
2307
+ See `Image.from_aws_ecr()` for details.
2308
+ - GCP Artifact Registry: `SERVICE_ACCOUNT_JSON`, holding the service account key's JSON.
2309
+ See `Image.from_gcp_artifact_registry()` for details.
2310
+ - Other registries: a username (`REGISTRY_USERNAME`) and an access token or password (`REGISTRY_PASSWORD`).
2242
2311
 
2243
- To authenticate against private registries with credentials from a cloud provider,
2244
- use `Image.from_gcp_artifact_registry()` or `Image.from_aws_ecr()`.
2312
+ The Secret must have the keys for exactly one of these; otherwise an error is raised.
2245
2313
 
2246
2314
  Args:
2247
2315
  tag: Registry image reference (e.g. ``python:3.11-slim``).
2248
- secret: Optional secret for static registry credentials.
2316
+ secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
2249
2317
  setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
2250
2318
  force_build: If True, skip cached image builds.
2251
2319
  add_python: Optional standalone Python series to inject when the base image lacks Python.
@@ -2259,13 +2327,17 @@ class _Image(_Object, type_prefix="im"):
2259
2327
  modal.Image.from_registry("python:3.11-slim-bookworm")
2260
2328
  modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
2261
2329
  modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
2330
+ modal.Image.from_registry(
2331
+ "ghcr.io/my-org/my-image:latest",
2332
+ secret=modal.Secret.from_name("my-registry-secret"),
2333
+ )
2262
2334
  ```
2263
2335
  """
2264
2336
 
2265
- def context_mount_function() -> _Mount | None:
2337
+ def context_mount_function(builder_version: ImageBuilderVersion) -> _Mount | None:
2266
2338
  return (
2267
2339
  _Mount.from_name(
2268
- python_standalone_mount_name(add_python),
2340
+ python_standalone_mount_name(add_python, builder_version),
2269
2341
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
2270
2342
  )
2271
2343
  if add_python
@@ -2273,7 +2345,7 @@ class _Image(_Object, type_prefix="im"):
2273
2345
  )
2274
2346
 
2275
2347
  if "image_registry_config" not in kwargs and secret is not None:
2276
- kwargs["image_registry_config"] = _ImageRegistryConfig(api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS, secret)
2348
+ kwargs["image_registry_config"] = _ImageRegistryConfig(secret=secret, infer_auth_type=True)
2277
2349
 
2278
2350
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
2279
2351
  commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
@@ -2282,12 +2354,48 @@ class _Image(_Object, type_prefix="im"):
2282
2354
  context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, add_python)}
2283
2355
  return DockerfileSpec(commands=commands, context_files=context_files)
2284
2356
 
2285
- return _Image._from_args(
2357
+ single_layer_image = _Image._from_args(
2286
2358
  dockerfile_function=build_dockerfile,
2287
2359
  context_mount_function=context_mount_function,
2288
2360
  force_build=force_build,
2289
2361
  **kwargs,
2290
2362
  )
2363
+ if not add_python:
2364
+ return single_layer_image
2365
+
2366
+ # Builder versions after 2025.06 put the registry image and the standalone Python in separate layers.
2367
+ registry_image = _Image._from_args(
2368
+ dockerfile_function=lambda version: DockerfileSpec(commands=[f"FROM {tag}"], context_files={}),
2369
+ image_registry_config=kwargs.get("image_registry_config"),
2370
+ force_build=force_build,
2371
+ )
2372
+
2373
+ def build_python_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
2374
+ commands = _Image._registry_setup_commands("base", version, setup_dockerfile_commands, add_python)
2375
+ return DockerfileSpec(commands=commands, context_files={})
2376
+
2377
+ split_image = _Image._from_args(
2378
+ base_images={"base": registry_image},
2379
+ dockerfile_function=build_python_dockerfile,
2380
+ context_mount_function=context_mount_function,
2381
+ force_build=force_build,
2382
+ **kwargs,
2383
+ )
2384
+
2385
+ async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
2386
+ builder_version = await _resolve_image_builder_version(load_context)
2387
+ # Validate before the registry base image of split_image is built.
2388
+ _validate_python_version(
2389
+ add_python, builder_version, allow_micro_granularity=False, allow_free_threading=True
2390
+ )
2391
+ image = split_image if builder_version > "2025.06" else single_layer_image
2392
+ await resolver.load(image, load_context)
2393
+ self._hydrate_from_other(image)
2394
+ self._serve_mounts = image._serve_mounts
2395
+
2396
+ obj = _Image._from_loader(_load, f"Image.from_registry({tag!r})", load_context_overrides=LoadContext.empty())
2397
+ obj.force_build = force_build
2398
+ return obj
2291
2399
 
2292
2400
  @staticmethod
2293
2401
  def from_gcp_artifact_registry(
@@ -2299,7 +2407,11 @@ class _Image(_Object, type_prefix="im"):
2299
2407
  add_python: str | None = None,
2300
2408
  **kwargs,
2301
2409
  ) -> "_Image":
2302
- """Build a Modal image from a private image in Google Cloud Platform (GCP) Artifact Registry.
2410
+ """mdmd:hidden
2411
+ Build a Modal image from a private image in Google Cloud Platform (GCP) Artifact Registry.
2412
+
2413
+ **Note:** This constructor is superseded by `Image.from_registry()`, which can also authenticate
2414
+ to private repositories when provided with the appropriate keys in a `modal.Secret`.
2303
2415
 
2304
2416
  You will need to pass a `modal.Secret` containing [your GCP service account key data](https://cloud.google.com/iam/docs/keys-create-delete#creating)
2305
2417
  as `SERVICE_ACCOUNT_JSON`. This can be done from the [Secrets](https://modal.com/secrets) page.
@@ -2360,11 +2472,15 @@ class _Image(_Object, type_prefix="im"):
2360
2472
  add_python: str | None = None,
2361
2473
  **kwargs,
2362
2474
  ) -> "_Image":
2363
- """Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
2475
+ """mdmd:hidden
2476
+ Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
2364
2477
 
2365
2478
  You will need to pass a `modal.Secret` containing either IAM user credentials or OIDC
2366
2479
  configuration to access the target ECR registry.
2367
2480
 
2481
+ **Note:** This constructor is superseded by `Image.from_registry()`, which can also authenticate
2482
+ to private repositories when provided with the appropriate keys in a `modal.Secret`.
2483
+
2368
2484
  For IAM user authentication, set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`.
2369
2485
 
2370
2486
  For OIDC authentication, set `AWS_ROLE_ARN` and `AWS_REGION`.
@@ -2417,28 +2533,33 @@ class _Image(_Object, type_prefix="im"):
2417
2533
  *,
2418
2534
  force_build: bool = False,
2419
2535
  context_dir: Path | str | None = None,
2420
- env: dict[str, str | None] | None = None,
2536
+ ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
2537
+ build_args: dict[str, str] = {},
2538
+ registry_secret: _Secret | None = None,
2421
2539
  secrets: Collection[_Secret] | None = None,
2540
+ env: dict[str, str | None] | None = None,
2422
2541
  gpu: str | None = None,
2423
2542
  add_python: str | None = None,
2424
- build_args: dict[str, str] = {},
2425
- ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
2426
2543
  ) -> "_Image":
2427
2544
  """Build a Modal image from a local Dockerfile.
2428
2545
 
2429
2546
  If your Dockerfile does not have Python installed, you can use the `add_python` parameter
2430
2547
  to specify a version of Python to add to the image.
2431
2548
 
2549
+ A `registry_secret` can be provided to pull the base image from a private registry.
2550
+ Currently this is supported only for single-stage Dockerfiles.
2551
+
2432
2552
  Args:
2433
2553
  path: Path to the Dockerfile on the local machine.
2434
2554
  force_build: If True, skip cached image builds.
2435
2555
  context_dir: Build context directory for resolving relative COPY paths.
2556
+ ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
2557
+ build_args: Dockerfile ``ARG`` values forwarded to the build.
2558
+ registry_secret: Optional Secret with credentials for pulling the base image from a private registry.
2559
+ secrets: Secrets injected as environment variables in the build container.
2436
2560
  env: Environment variables set in the build container.
2437
- secrets: Secrets injected as environment variables during the build.
2438
- gpu: GPU type to attach to the builder container.
2561
+ gpu: GPU type to attach to the build container.
2439
2562
  add_python: Standalone Python version to add when the Dockerfile does not install Python.
2440
- build_args: Dockerfile ``ARG`` values forwarded to the build.
2441
- ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
2442
2563
 
2443
2564
  Returns:
2444
2565
  An `Image` built from the Dockerfile plus Modal runtime dependencies.
@@ -2484,6 +2605,12 @@ class _Image(_Object, type_prefix="im"):
2484
2605
  add_python="3.12",
2485
2606
  ignore=FilePatternMatcher.from_file("/path/to/dockerignore"),
2486
2607
  )
2608
+
2609
+ # Pull the Dockerfile's base image from a private registry.
2610
+ image = modal.Image.from_dockerfile(
2611
+ "./Dockerfile",
2612
+ registry_secret=modal.Secret.from_name("my-registry-secret"),
2613
+ )
2487
2614
  ```
2488
2615
  """
2489
2616
 
@@ -2491,32 +2618,35 @@ class _Image(_Object, type_prefix="im"):
2491
2618
  if env:
2492
2619
  secrets = [*secrets, _Secret.from_dict(env)]
2493
2620
 
2494
- # --- Build the base dockerfile
2495
-
2496
- def build_dockerfile_base(version: ImageBuilderVersion) -> DockerfileSpec:
2497
- with open(os.path.expanduser(path)) as f:
2498
- commands = f.read().split("\n")
2499
- return DockerfileSpec(commands=commands, context_files={})
2500
-
2501
2621
  gpu_config = parse_gpu_config(gpu)
2502
- base_image = _Image._from_args(
2503
- dockerfile_function=build_dockerfile_base,
2504
- context_mount_function=_create_context_mount_function(
2505
- ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
2506
- ),
2507
- gpu_config=gpu_config,
2508
- secrets=secrets,
2509
- build_args=build_args,
2622
+ context_mount_function = _create_context_mount_function(
2623
+ ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
2510
2624
  )
2511
2625
 
2512
- # --- Now add in the modal dependencies, and, optionally a Python distribution
2513
- # This happening in two steps is probably a vestigial consequence of previous limitations,
2514
- # but it will be difficult to merge them without forcing rebuilds of images.
2626
+ registry_config = (
2627
+ _ImageRegistryConfig(secret=registry_secret, infer_auth_type=True) if registry_secret is not None else None
2628
+ )
2515
2629
 
2516
- def add_python_mount():
2630
+ def dockerfile_image(
2631
+ commands: list[str],
2632
+ base_images: dict[str, _Image] | None = None,
2633
+ image_registry_config: _ImageRegistryConfig | None = None,
2634
+ ) -> _Image:
2635
+ return _Image._from_args(
2636
+ base_images=base_images,
2637
+ dockerfile_function=lambda version: DockerfileSpec(commands=commands, context_files={}),
2638
+ context_mount_function=context_mount_function,
2639
+ gpu_config=gpu_config,
2640
+ secrets=secrets,
2641
+ build_args=build_args,
2642
+ image_registry_config=image_registry_config,
2643
+ force_build=force_build,
2644
+ )
2645
+
2646
+ def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
2517
2647
  return (
2518
2648
  _Mount.from_name(
2519
- python_standalone_mount_name(add_python),
2649
+ python_standalone_mount_name(add_python, builder_version),
2520
2650
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
2521
2651
  )
2522
2652
  if add_python
@@ -2531,12 +2661,61 @@ class _Image(_Object, type_prefix="im"):
2531
2661
  context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
2532
2662
  return DockerfileSpec(commands=commands, context_files=context_files)
2533
2663
 
2534
- return _Image._from_args(
2535
- base_images={"base": base_image},
2536
- dockerfile_function=build_dockerfile_python,
2537
- context_mount_function=add_python_mount,
2538
- force_build=force_build,
2539
- )
2664
+ def python_image(base_image: _Image) -> _Image:
2665
+ return _Image._from_args(
2666
+ base_images={"base": base_image},
2667
+ dockerfile_function=build_dockerfile_python,
2668
+ context_mount_function=add_python_mount,
2669
+ force_build=force_build,
2670
+ )
2671
+
2672
+ async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
2673
+ builder_version = await _resolve_image_builder_version(load_context)
2674
+ if add_python:
2675
+ # Validate before any Dockerfile layer is built.
2676
+ _validate_python_version(
2677
+ add_python, builder_version, allow_micro_granularity=False, allow_free_threading=True
2678
+ )
2679
+ with open(os.path.expanduser(path)) as f:
2680
+ dockerfile_lines = f.read().split("\n")
2681
+ if registry_secret is not None and is_multi_stage_dockerfile(dockerfile_lines):
2682
+ raise InvalidError("`registry_secret` is not supported with multi-stage Dockerfiles.")
2683
+
2684
+ # Builder versions after 2025.06 put the FROM line of a single-stage Dockerfile in its own image.
2685
+ # Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
2686
+ from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
2687
+ if from_index is None:
2688
+ image = dockerfile_image(dockerfile_lines, image_registry_config=registry_config)
2689
+ else:
2690
+ # Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
2691
+ # share the cached image. Those lines can only be comments or blank lines, since
2692
+ # splittable_from_line_index rejects parser directives.
2693
+ from_lines = [dockerfile_lines[from_index]]
2694
+ # Only this layer pulls from the registry; the layers above it build on top of it.
2695
+ from_image = _Image._from_args(
2696
+ dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
2697
+ image_registry_config=registry_config,
2698
+ force_build=force_build,
2699
+ )
2700
+ rest = dockerfile_lines[from_index + 1 :]
2701
+ rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
2702
+ if rest_is_empty:
2703
+ image = from_image
2704
+ else:
2705
+ image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
2706
+ if add_python or builder_version <= "2025.06":
2707
+ # Before 2025.06 we always created a python_image() layer because it's where we installed the
2708
+ # client dependencies. From 2025.06, we no longer install client dependencies in base layers,
2709
+ # so this was a no-op layer when add_python= is not used. Skip it on later image builders.
2710
+ image = python_image(image)
2711
+ await resolver.load(image, load_context)
2712
+ self._hydrate_from_other(image)
2713
+ self._serve_mounts = image._serve_mounts
2714
+
2715
+ rep = f"Image.from_dockerfile({str(path)!r})"
2716
+ obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext.empty())
2717
+ obj.force_build = force_build
2718
+ return obj
2540
2719
 
2541
2720
  @staticmethod
2542
2721
  def from_scratch(force_build: bool = False) -> "_Image":
@@ -98,8 +98,9 @@ class _AuthTokenManager:
98
98
  caller goes away.
99
99
  """
100
100
  if self._refresh_task is None:
101
- # Maybe another coroutine refreshed already, or we're cooling down after a failure.
102
- if self._token and not self._should_refresh():
101
+ # Maybe another coroutine refreshed already, or we're cooling down after a failure. The cooldown
102
+ # only applies while the cached token is still usable.
103
+ if self._has_valid_token() and not self._should_refresh():
103
104
  return
104
105
  # Note: the task is created in async code so it binds to the synchronicity event loop.
105
106
  self._refresh_task = asyncio.create_task(self._fetch_token())
@@ -121,12 +122,12 @@ class _AuthTokenManager:
121
122
  async def _fetch_token(self):
122
123
  """Fetch a new token and cache it, or arm the failure backoff."""
123
124
  try:
124
- # No cached token to fall back on, so a failure is user-visible: retry transient errors.
125
+ # Retry transient errors if the cached token is missing or expired.
125
126
  # Otherwise one attempt, and _retry_after handles the cooldown.
126
127
  retry = Retry(
127
128
  attempt_timeout=self.AUTH_TOKEN_TIMEOUT,
128
129
  total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
129
- max_retries=0 if self._token else DEFAULT_MAX_RETRIES,
130
+ max_retries=0 if self._has_valid_token() else DEFAULT_MAX_RETRIES,
130
131
  )
131
132
  token = await self._fetch(retry)
132
133
  if not token:
@@ -166,6 +167,9 @@ class _AuthTokenManager:
166
167
  except Exception as e:
167
168
  raise ValueError("Internal error: Cannot parse auth token. Please contact Modal support.") from e
168
169
 
170
+ def _has_valid_token(self):
171
+ return bool(self._token) and not self._is_expired()
172
+
169
173
  def _in_backoff(self):
170
174
  return time.time() < self._retry_after
171
175