modal 1.6.3.dev11__tar.gz → 1.6.3.dev13__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 (250) hide show
  1. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/PKG-INFO +4 -2
  2. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_image.py +110 -29
  3. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/docker_utils.py +10 -1
  4. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/grpc_utils.py +23 -4
  5. modal-1.6.3.dev13/modal/_utils/oauth.py +66 -0
  6. modal-1.6.3.dev13/modal/builder/PREVIEW.txt +17 -0
  7. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/base-images.json +3 -0
  8. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/config.py +2 -0
  9. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/endpoint.py +49 -7
  10. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/profile.py +2 -1
  11. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/token.py +1 -0
  12. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/workspace.py +1 -1
  13. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/client.py +39 -9
  14. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/client.pyi +42 -8
  15. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/config.py +3 -1
  16. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/image.pyi +39 -18
  17. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/io_streams.py +26 -14
  18. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/io_streams.pyi +2 -0
  19. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/mount.py +9 -0
  20. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/network_file_system.py +4 -8
  21. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/network_file_system.pyi +10 -16
  22. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/token_flow.py +1 -0
  23. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/PKG-INFO +4 -2
  24. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/SOURCES.txt +2 -0
  25. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/requires.txt +4 -1
  26. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_grpc.py +48 -0
  27. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2.py +1550 -1411
  28. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2.pyi +286 -1
  29. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2_grpc.py +99 -0
  30. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/api_pb2_grpc.pyi +30 -0
  31. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/modal_api_grpc.py +3 -0
  32. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_version/__init__.py +1 -1
  33. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/pyproject.toml +5 -3
  34. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/LICENSE +0 -0
  35. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/README.md +0 -0
  36. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/__init__.py +0 -0
  37. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/__main__.py +0 -0
  38. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_billing.py +0 -0
  39. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_cluster.py +0 -0
  40. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_cluster.pyi +0 -0
  41. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_container_entrypoint.py +0 -0
  42. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_environments.py +0 -0
  43. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_function_variants.py +0 -0
  44. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_functions.py +0 -0
  45. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_grpc_client.py +0 -0
  46. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_ipython.py +0 -0
  47. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_load_context.py +0 -0
  48. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_location.py +0 -0
  49. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs.py +0 -0
  50. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs_manager.py +0 -0
  51. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_logs_manager.pyi +0 -0
  52. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_metrics.py +0 -0
  53. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_metrics.pyi +0 -0
  54. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_object.py +0 -0
  55. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_outbound_policy.py +0 -0
  56. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_outbound_policy.pyi +0 -0
  57. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/__init__.py +0 -0
  58. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/manager.py +0 -0
  59. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/pty.py +0 -0
  60. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/rich.py +0 -0
  61. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_output/status.py +0 -0
  62. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_partial_function.py +0 -0
  63. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_resolver.py +0 -0
  64. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_resources.py +0 -0
  65. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/__init__.py +0 -0
  66. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/asgi.py +0 -0
  67. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/container_io_manager.py +0 -0
  68. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/container_io_manager.pyi +0 -0
  69. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/execution_context.py +0 -0
  70. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/execution_context.pyi +0 -0
  71. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  72. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/task_lifecycle_manager.py +0 -0
  73. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  74. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/telemetry.py +0 -0
  75. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/user_code_event_loop.py +0 -0
  76. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_runtime/user_code_imports.py +0 -0
  77. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_serialization.py +0 -0
  78. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_server.py +0 -0
  79. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_server.pyi +0 -0
  80. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_supports_logs.py +0 -0
  81. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_traceback.py +0 -0
  82. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_tunnel.py +0 -0
  83. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_tunnel.pyi +0 -0
  84. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_type_manager.py +0 -0
  85. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/__init__.py +0 -0
  86. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/app_utils.py +0 -0
  87. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/async_utils.py +0 -0
  88. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/auth_token_manager.py +0 -0
  89. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/blob_utils.py +0 -0
  90. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/browser_utils.py +0 -0
  91. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/bytes_io_segment_payload.py +0 -0
  92. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/curl_utils.py +0 -0
  93. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/deprecation.py +0 -0
  94. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/display_utils.py +0 -0
  95. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/function_utils.py +0 -0
  96. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/git_utils.py +0 -0
  97. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/grpc_testing.py +0 -0
  98. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/hash_utils.py +0 -0
  99. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/http_utils.py +0 -0
  100. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/idle_countdown.py +0 -0
  101. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/jwt_utils.py +0 -0
  102. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/logger.py +0 -0
  103. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/mount_utils.py +0 -0
  104. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/name_utils.py +0 -0
  105. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/package_utils.py +0 -0
  106. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/pattern_utils.py +0 -0
  107. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/proxy_support.py +0 -0
  108. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/rand_pb_testing.py +0 -0
  109. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/sandbox_fs_utils.py +0 -0
  110. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/shell_utils.py +0 -0
  111. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_utils/time_utils.py +0 -0
  112. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/__init__.py +0 -0
  113. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  114. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/cloudpickle.py +0 -0
  115. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/tblib.py +0 -0
  116. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_vendor/version.py +0 -0
  117. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_watcher.py +0 -0
  118. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/_workspace.py +0 -0
  119. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/app.py +0 -0
  120. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/app.pyi +0 -0
  121. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/billing.py +0 -0
  122. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/billing.pyi +0 -0
  123. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2023.12.312.txt +0 -0
  124. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2023.12.txt +0 -0
  125. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2024.04.txt +0 -0
  126. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2024.10.txt +0 -0
  127. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/2025.06.txt +0 -0
  128. /modal-1.6.3.dev11/modal/builder/PREVIEW.txt → /modal-1.6.3.dev13/modal/builder/2026.10.txt +0 -0
  129. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/builder/README.md +0 -0
  130. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/call_graph.py +0 -0
  131. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/__init__.py +0 -0
  132. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_download.py +0 -0
  133. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_help.py +0 -0
  134. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_logs.py +0 -0
  135. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_stats.py +0 -0
  136. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/_traceback.py +0 -0
  137. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/app.py +0 -0
  138. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/billing.py +0 -0
  139. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/changelog.py +0 -0
  140. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/container.py +0 -0
  141. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/curl.py +0 -0
  142. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/dashboard.py +0 -0
  143. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/dict.py +0 -0
  144. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/entry_point.py +0 -0
  145. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/environment.py +0 -0
  146. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/function.py +0 -0
  147. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/image.py +0 -0
  148. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/import_refs.py +0 -0
  149. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/logo.py +0 -0
  150. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/network_file_system.py +0 -0
  151. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/queues.py +0 -0
  152. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/run.py +0 -0
  153. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/secret.py +0 -0
  154. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/selector.py +0 -0
  155. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/server.py +0 -0
  156. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/shell.py +0 -0
  157. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/skills.py +0 -0
  158. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/utils.py +0 -0
  159. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cli/volume.py +0 -0
  160. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cloud_bucket_mount.py +0 -0
  161. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cloud_bucket_mount.pyi +0 -0
  162. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cls.py +0 -0
  163. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cls.pyi +0 -0
  164. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cluster.py +0 -0
  165. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/cluster.pyi +0 -0
  166. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/container_process.py +0 -0
  167. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/container_process.pyi +0 -0
  168. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/dict.py +0 -0
  169. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/dict.pyi +0 -0
  170. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/environments.py +0 -0
  171. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/environments.pyi +0 -0
  172. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/exception.py +0 -0
  173. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/__init__.py +0 -0
  174. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/_flash_container_entry.py +0 -0
  175. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash.py +0 -0
  176. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash.pyi +0 -0
  177. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash_server.py +0 -0
  178. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/flash_server.pyi +0 -0
  179. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/experimental/ipython.py +0 -0
  180. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/file_pattern_matcher.py +0 -0
  181. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/functions.py +0 -0
  182. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/functions.pyi +0 -0
  183. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/image.py +0 -0
  184. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/mount.pyi +0 -0
  185. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/object.py +0 -0
  186. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/object.pyi +0 -0
  187. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/output.py +0 -0
  188. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/parallel_map.py +0 -0
  189. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/parallel_map.pyi +0 -0
  190. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/partial_function.py +0 -0
  191. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/partial_function.pyi +0 -0
  192. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/proxy.py +0 -0
  193. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/proxy.pyi +0 -0
  194. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/py.typed +0 -0
  195. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/queue.py +0 -0
  196. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/queue.pyi +0 -0
  197. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/retries.py +0 -0
  198. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/runner.py +0 -0
  199. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/runner.pyi +0 -0
  200. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/running_app.py +0 -0
  201. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/__init__.py +0 -0
  202. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_common.py +0 -0
  203. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_filesystem.py +0 -0
  204. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_persistent.py +0 -0
  205. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_v1.py +0 -0
  206. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_impl_v2.py +0 -0
  207. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_sandbox.py +0 -0
  208. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_sidecar.py +0 -0
  209. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_snapshot.py +0 -0
  210. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/_task_command_router_client.py +0 -0
  211. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/sandbox.py +0 -0
  212. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/sandbox/sandbox.pyi +0 -0
  213. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/schedule.py +0 -0
  214. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/scheduler_placement.py +0 -0
  215. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/secret.py +0 -0
  216. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/secret.pyi +0 -0
  217. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/server.py +0 -0
  218. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/server.pyi +0 -0
  219. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/serving.py +0 -0
  220. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/serving.pyi +0 -0
  221. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/skills/modal/SKILL.md +0 -0
  222. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/stream_type.py +0 -0
  223. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/token_flow.pyi +0 -0
  224. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/types.py +0 -0
  225. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/volume.py +0 -0
  226. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/volume.pyi +0 -0
  227. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/workspace.py +0 -0
  228. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal/workspace.pyi +0 -0
  229. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/dependency_links.txt +0 -0
  230. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/entry_points.txt +0 -0
  231. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal.egg-info/top_level.txt +0 -0
  232. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/__init__.py +0 -0
  233. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_cli_docs.py +0 -0
  234. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_cli_docs_main.py +0 -0
  235. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_reference_docs.py +0 -0
  236. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/gen_reference_docs_main.py +0 -0
  237. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/__init__.py +0 -0
  238. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/mdmd.py +0 -0
  239. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/signatures.py +0 -0
  240. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/mdmd/types.py +0 -0
  241. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_docs/reference_intro.md +0 -0
  242. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/__init__.py +0 -0
  243. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/py.typed +0 -0
  244. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_grpc.py +0 -0
  245. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2.py +0 -0
  246. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2.pyi +0 -0
  247. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  248. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  249. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/modal_version/__main__.py +0 -0
  250. {modal-1.6.3.dev11 → modal-1.6.3.dev13}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.3.dev11
3
+ Version: 1.6.3.dev13
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
@@ -40,6 +40,7 @@ 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,
43
44
  splittable_from_line_index,
44
45
  )
45
46
  from ._utils.function_utils import FunctionSourceInfo, parse_gpu_config
@@ -70,8 +71,8 @@ if typing.TYPE_CHECKING:
70
71
  import modal.client
71
72
 
72
73
  # This is used for both type checking and runtime validation
73
- ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
74
- LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2025.06"
74
+ ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "2026.10", "PREVIEW"]
75
+ LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2026.10"
75
76
 
76
77
  # Note: we also define supported Python versions via logic at the top of the package __init__.py
77
78
  # so that we fail fast / clearly in unsupported containers. Additionally, we enumerate the supported
@@ -79,6 +80,7 @@ LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2025.06"
79
80
  # Consider consolidating these multiple sources of truth?
80
81
  SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
81
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"],
82
84
  "2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
83
85
  "2024.10": ["3.10", "3.11", "3.12", "3.13"],
84
86
  "2024.04": ["3.10", "3.11", "3.12"],
@@ -445,6 +447,40 @@ def _create_context_mount_function(
445
447
  return auto_created_context_mount_fn
446
448
 
447
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
+
448
484
  class _ImageRegistryConfig:
449
485
  """mdmd:hidden"""
450
486
 
@@ -453,13 +489,21 @@ class _ImageRegistryConfig:
453
489
  # TODO: change to _PUBLIC after worker starts handling it.
454
490
  registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
455
491
  secret: _Secret | None = None,
492
+ *,
493
+ infer_auth_type: bool = False,
456
494
  ):
457
495
  self.registry_auth_type = registry_auth_type
458
496
  self.secret = secret
459
-
460
- 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)
461
505
  return api_pb2.ImageRegistryConfig(
462
- registry_auth_type=self.registry_auth_type,
506
+ registry_auth_type=registry_auth_type,
463
507
  secret_id=(self.secret.object_id if self.secret else ""),
464
508
  )
465
509
 
@@ -762,7 +806,7 @@ class _Image(_Object, type_prefix="im"):
762
806
  secret_ids=[secret.object_id for secret in secrets],
763
807
  context_mount_id=(context_mount.object_id if context_mount else ""),
764
808
  gpu_config=gpu_config,
765
- image_registry_config=image_registry_config.get_proto(),
809
+ image_registry_config=await image_registry_config.get_proto(),
766
810
  runtime=config.get("function_runtime"),
767
811
  runtime_debug=config.get("function_runtime_debug"),
768
812
  build_function=_build_function,
@@ -2255,16 +2299,21 @@ class _Image(_Object, type_prefix="im"):
2255
2299
  remaining commands run. This might be useful if you want a custom Python installation or to
2256
2300
  set a `SHELL`. Prefer `run_commands()` when possible though.
2257
2301
 
2258
- To authenticate against a private registry with static credentials, you must set the `secret` parameter to
2259
- a `modal.Secret` containing a username (`REGISTRY_USERNAME`) and
2260
- 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`).
2261
2311
 
2262
- To authenticate against private registries with credentials from a cloud provider,
2263
- 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.
2264
2313
 
2265
2314
  Args:
2266
2315
  tag: Registry image reference (e.g. ``python:3.11-slim``).
2267
- secret: Optional secret for static registry credentials.
2316
+ secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
2268
2317
  setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
2269
2318
  force_build: If True, skip cached image builds.
2270
2319
  add_python: Optional standalone Python series to inject when the base image lacks Python.
@@ -2278,6 +2327,10 @@ class _Image(_Object, type_prefix="im"):
2278
2327
  modal.Image.from_registry("python:3.11-slim-bookworm")
2279
2328
  modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
2280
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
+ )
2281
2334
  ```
2282
2335
  """
2283
2336
 
@@ -2292,7 +2345,7 @@ class _Image(_Object, type_prefix="im"):
2292
2345
  )
2293
2346
 
2294
2347
  if "image_registry_config" not in kwargs and secret is not None:
2295
- 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)
2296
2349
 
2297
2350
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
2298
2351
  commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
@@ -2472,28 +2525,33 @@ class _Image(_Object, type_prefix="im"):
2472
2525
  *,
2473
2526
  force_build: bool = False,
2474
2527
  context_dir: Path | str | None = None,
2475
- env: dict[str, str | None] | None = None,
2528
+ ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
2529
+ build_args: dict[str, str] = {},
2530
+ registry_secret: _Secret | None = None,
2476
2531
  secrets: Collection[_Secret] | None = None,
2532
+ env: dict[str, str | None] | None = None,
2477
2533
  gpu: str | None = None,
2478
2534
  add_python: str | None = None,
2479
- build_args: dict[str, str] = {},
2480
- ignore: Sequence[str] | Callable[[Path], bool] = AUTO_DOCKERIGNORE,
2481
2535
  ) -> "_Image":
2482
2536
  """Build a Modal image from a local Dockerfile.
2483
2537
 
2484
2538
  If your Dockerfile does not have Python installed, you can use the `add_python` parameter
2485
2539
  to specify a version of Python to add to the image.
2486
2540
 
2541
+ A `registry_secret` can be provided to pull the base image from a private registry.
2542
+ Currently this is supported only for single-stage Dockerfiles.
2543
+
2487
2544
  Args:
2488
2545
  path: Path to the Dockerfile on the local machine.
2489
2546
  force_build: If True, skip cached image builds.
2490
2547
  context_dir: Build context directory for resolving relative COPY paths.
2548
+ ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
2549
+ build_args: Dockerfile ``ARG`` values forwarded to the build.
2550
+ registry_secret: Optional Secret with credentials for pulling the base image from a private registry.
2551
+ secrets: Secrets injected as environment variables in the build container.
2491
2552
  env: Environment variables set in the build container.
2492
- secrets: Secrets injected as environment variables during the build.
2493
- gpu: GPU type to attach to the builder container.
2553
+ gpu: GPU type to attach to the build container.
2494
2554
  add_python: Standalone Python version to add when the Dockerfile does not install Python.
2495
- build_args: Dockerfile ``ARG`` values forwarded to the build.
2496
- ignore: Ignore rules for the implicit context mount (defaults to auto ``.dockerignore`` behavior).
2497
2555
 
2498
2556
  Returns:
2499
2557
  An `Image` built from the Dockerfile plus Modal runtime dependencies.
@@ -2539,6 +2597,12 @@ class _Image(_Object, type_prefix="im"):
2539
2597
  add_python="3.12",
2540
2598
  ignore=FilePatternMatcher.from_file("/path/to/dockerignore"),
2541
2599
  )
2600
+
2601
+ # Pull the Dockerfile's base image from a private registry.
2602
+ image = modal.Image.from_dockerfile(
2603
+ "./Dockerfile",
2604
+ registry_secret=modal.Secret.from_name("my-registry-secret"),
2605
+ )
2542
2606
  ```
2543
2607
  """
2544
2608
 
@@ -2551,7 +2615,15 @@ class _Image(_Object, type_prefix="im"):
2551
2615
  ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
2552
2616
  )
2553
2617
 
2554
- def dockerfile_image(commands: list[str], base_images: dict[str, _Image] | None = None) -> _Image:
2618
+ registry_config = (
2619
+ _ImageRegistryConfig(secret=registry_secret, infer_auth_type=True) if registry_secret is not None else None
2620
+ )
2621
+
2622
+ def dockerfile_image(
2623
+ commands: list[str],
2624
+ base_images: dict[str, _Image] | None = None,
2625
+ image_registry_config: _ImageRegistryConfig | None = None,
2626
+ ) -> _Image:
2555
2627
  return _Image._from_args(
2556
2628
  base_images=base_images,
2557
2629
  dockerfile_function=lambda version: DockerfileSpec(commands=commands, context_files={}),
@@ -2559,13 +2631,10 @@ class _Image(_Object, type_prefix="im"):
2559
2631
  gpu_config=gpu_config,
2560
2632
  secrets=secrets,
2561
2633
  build_args=build_args,
2634
+ image_registry_config=image_registry_config,
2562
2635
  force_build=force_build,
2563
2636
  )
2564
2637
 
2565
- # --- Now add in the modal dependencies, and, optionally a Python distribution
2566
- # This happening in two steps is probably a vestigial consequence of previous limitations,
2567
- # but it will be difficult to merge them without forcing rebuilds of images.
2568
-
2569
2638
  def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
2570
2639
  return (
2571
2640
  _Mount.from_name(
@@ -2601,24 +2670,36 @@ class _Image(_Object, type_prefix="im"):
2601
2670
  )
2602
2671
  with open(os.path.expanduser(path)) as f:
2603
2672
  dockerfile_lines = f.read().split("\n")
2673
+ if registry_secret is not None and is_multi_stage_dockerfile(dockerfile_lines):
2674
+ raise InvalidError("`registry_secret` is not supported with multi-stage Dockerfiles.")
2604
2675
 
2605
2676
  # Builder versions after 2025.06 put the FROM line of a single-stage Dockerfile in its own image.
2606
2677
  # Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
2607
2678
  from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
2608
2679
  if from_index is None:
2609
- image = python_image(dockerfile_image(dockerfile_lines))
2680
+ image = dockerfile_image(dockerfile_lines, image_registry_config=registry_config)
2610
2681
  else:
2611
- from_lines = dockerfile_lines[: from_index + 1]
2682
+ # Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
2683
+ # share the cached image. Those lines can only be comments or blank lines, since
2684
+ # splittable_from_line_index rejects parser directives.
2685
+ from_lines = [dockerfile_lines[from_index]]
2686
+ # Only this layer pulls from the registry; the layers above it build on top of it.
2612
2687
  from_image = _Image._from_args(
2613
2688
  dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
2689
+ image_registry_config=registry_config,
2614
2690
  force_build=force_build,
2615
2691
  )
2616
2692
  rest = dockerfile_lines[from_index + 1 :]
2617
2693
  rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
2618
2694
  if rest_is_empty:
2619
- image = python_image(from_image)
2695
+ image = from_image
2620
2696
  else:
2621
- image = python_image(dockerfile_image(["FROM base", *rest], base_images={"base": from_image}))
2697
+ image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
2698
+ if add_python or builder_version <= "2025.06":
2699
+ # Before 2025.06 we always created a python_image() layer because it's where we installed the
2700
+ # client dependencies. From 2025.06, we no longer install client dependencies in base layers,
2701
+ # so this was a no-op layer when add_python= is not used. Skip it on later image builders.
2702
+ image = python_image(image)
2622
2703
  await resolver.load(image, load_context)
2623
2704
  self._hydrate_from_other(image)
2624
2705
  self._serve_mounts = image._serve_mounts
@@ -79,6 +79,15 @@ _FROM_ALIAS_PATTERN = re.compile(r"\sAS\s+\S+\s*$", re.IGNORECASE)
79
79
  _PARSER_DIRECTIVE_PATTERN = re.compile(r"^#\s*[a-zA-Z][a-zA-Z0-9]*\s*=")
80
80
 
81
81
 
82
+ def is_multi_stage_dockerfile(dockerfile_lines: Sequence[str]) -> bool:
83
+ """Return whether more than one line starts with a FROM instruction.
84
+
85
+ Can have false-positives for Dockerfiles with a Heredoc bodies of continuation lines
86
+ that contain "from" tokens.
87
+ """
88
+ return sum(1 for line in dockerfile_lines if _FROM_PATTERN.match(line)) > 1
89
+
90
+
82
91
  def splittable_from_line_index(dockerfile_lines: Sequence[str]) -> int | None:
83
92
  """Return the line index of the FROM instruction if it can be built as its own image.
84
93
 
@@ -96,7 +105,7 @@ def splittable_from_line_index(dockerfile_lines: Sequence[str]) -> int | None:
96
105
  return None
97
106
  if any(_PARSER_DIRECTIVE_PATTERN.match(line.strip()) for line in dockerfile_lines[:start]):
98
107
  return None
99
- if any(_FROM_PATTERN.match(line) for line in dockerfile_lines[start + 1 :]):
108
+ if is_multi_stage_dockerfile(dockerfile_lines):
100
109
  return None
101
110
  return start
102
111
 
@@ -9,7 +9,7 @@ import time
9
9
  import typing
10
10
  import urllib.parse
11
11
  import uuid
12
- from collections.abc import AsyncIterator, Sequence
12
+ from collections.abc import AsyncIterator, Callable, Mapping, Sequence
13
13
  from dataclasses import dataclass, field
14
14
  from functools import cache
15
15
  from typing import Any, Optional, TypeVar, Union
@@ -42,6 +42,7 @@ from .logger import logger
42
42
 
43
43
  RequestType = TypeVar("RequestType", bound=Message)
44
44
  ResponseType = TypeVar("ResponseType", bound=Message)
45
+ MetadataProvider = Callable[[], Mapping[str, str]]
45
46
 
46
47
  if typing.TYPE_CHECKING:
47
48
  import modal._grpc_client
@@ -224,17 +225,29 @@ class ConnectionManager:
224
225
  should share the same connection.
225
226
  """
226
227
 
227
- def __init__(self, client: "modal.client._Client", metadata: dict[str, str] = {}):
228
+ def __init__(
229
+ self,
230
+ client: "modal.client._Client",
231
+ metadata: dict[str, str] = {},
232
+ metadata_provider: MetadataProvider | None = None,
233
+ ):
228
234
  self._client = client
229
235
  # This metadata is injected into all requests on all channels created by this manager.
230
236
  self._metadata = metadata
237
+ self._metadata_provider = metadata_provider
231
238
  self._channels: dict[str, asyncio.Future[grpclib.client.Channel]] = {}
232
239
 
233
240
  async def get_or_create_channel(self, server_url: str) -> grpclib.client.Channel:
234
241
  fut = self._channels.get(server_url)
235
242
  if fut is None or (fut.done() and (fut.cancelled() or fut.exception() is not None)):
236
243
  # Cache the attempt rather than the channel, so callers racing to connect share one.
237
- fut = asyncio.ensure_future(create_channel_with_fallbacks(server_url, self._metadata))
244
+ fut = asyncio.ensure_future(
245
+ create_channel_with_fallbacks(
246
+ server_url,
247
+ self._metadata,
248
+ metadata_provider=self._metadata_provider,
249
+ )
250
+ )
238
251
  self._channels[server_url] = fut
239
252
  # Shielded so one caller giving up doesn't cancel the connection for the others.
240
253
  return await asyncio.shield(fut)
@@ -357,6 +370,7 @@ def listen_for_server_warnings(channel: grpclib.client.Channel) -> None:
357
370
  def create_channel(
358
371
  server_url: str,
359
372
  metadata: dict[str, str] = {},
373
+ metadata_provider: MetadataProvider | None = None,
360
374
  ) -> grpclib.client.Channel:
361
375
  """Creates a grpclib.Channel to be used by a GRPC stub.
362
376
 
@@ -387,6 +401,10 @@ def create_channel(
387
401
  async def send_request(event: grpclib.events.SendRequest) -> None:
388
402
  for k, v in metadata.items():
389
403
  event.metadata[k] = v
404
+ if metadata_provider is not None:
405
+ provided_metadata = await asyncio.to_thread(metadata_provider)
406
+ for k, v in provided_metadata.items():
407
+ event.metadata[k] = v
390
408
 
391
409
  if o.hostname is not None:
392
410
  # GRPC actually injects a :authority pseudo-header containing the hostname.
@@ -412,6 +430,7 @@ async def create_channel_with_fallbacks(
412
430
  server_url: str,
413
431
  metadata: dict[str, str] = {},
414
432
  stagger_delay: float = 3.0,
433
+ metadata_provider: MetadataProvider | None = None,
415
434
  ) -> grpclib.client.Channel:
416
435
  """Create a connected channel, supporting comma-separated fallback URLs.
417
436
 
@@ -440,7 +459,7 @@ async def create_channel_with_fallbacks(
440
459
  await asyncio.wait_for(failed[index - 1].wait(), timeout=index * stagger_delay)
441
460
  except asyncio.TimeoutError:
442
461
  pass
443
- channel = create_channel(url, metadata)
462
+ channel = create_channel(url, metadata, metadata_provider)
444
463
  try:
445
464
  await channel.__connect__()
446
465
  except BaseException:
@@ -0,0 +1,66 @@
1
+ # Copyright Modal Labs 2026
2
+ import base64
3
+ import hashlib
4
+ import json
5
+ import time
6
+ import uuid
7
+ from collections.abc import Mapping
8
+ from typing import Any
9
+
10
+ from modal.exception import InvalidError
11
+
12
+ _OAUTH_CLIENT_ASSERTION_AUDIENCE = "modal-server"
13
+ _OAUTH_CLIENT_ASSERTION_LIFETIME_SECONDS = 5 * 60
14
+ _MIN_RSA_KEY_SIZE = 2048
15
+
16
+
17
+ class OAuthAssertionProvider:
18
+ """Mint short-lived private_key_jwt assertions for OAuth client authentication."""
19
+
20
+ def __init__(self, client_id: str, private_key_pem: str):
21
+ try:
22
+ import jwt
23
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
24
+ from jwt.algorithms import RSAAlgorithm
25
+ from jwt.exceptions import InvalidKeyError
26
+ except ImportError:
27
+ raise ImportError(
28
+ "Private-key OAuth authentication requires the 'oauth' extra. "
29
+ "Install it with `pip install 'modal[oauth]'`."
30
+ ) from None
31
+
32
+ private_key_pem = private_key_pem.replace("\\n", "\n")
33
+ try:
34
+ private_key = RSAAlgorithm(RSAAlgorithm.SHA256).prepare_key(private_key_pem)
35
+ except (TypeError, InvalidKeyError):
36
+ raise InvalidError("oauth_jwt_key must be an unencrypted RSA private key encoded as PEM.") from None
37
+ if not isinstance(private_key, RSAPrivateKey):
38
+ raise InvalidError("oauth_jwt_key must be an RSA private key.")
39
+ if private_key.key_size < _MIN_RSA_KEY_SIZE:
40
+ raise InvalidError("oauth_jwt_key must be at least 2048 bits.")
41
+
42
+ public_jwk: dict[str, Any] = RSAAlgorithm.to_jwk(private_key.public_key(), as_dict=True)
43
+ thumbprint_jwk = {field: public_jwk[field] for field in ("e", "kty", "n")}
44
+ canonical_jwk = json.dumps(thumbprint_jwk, sort_keys=True, separators=(",", ":")).encode()
45
+
46
+ self._client_id = client_id
47
+ self._private_key = private_key
48
+ self._kid = base64.urlsafe_b64encode(hashlib.sha256(canonical_jwk).digest()).rstrip(b"=").decode()
49
+ self._jwt = jwt
50
+
51
+ def __call__(self) -> Mapping[str, str]:
52
+ issued_at = int(time.time())
53
+ assertion = self._jwt.encode(
54
+ {
55
+ "iss": self._client_id,
56
+ "sub": self._client_id,
57
+ "aud": _OAUTH_CLIENT_ASSERTION_AUDIENCE,
58
+ "iat": issued_at,
59
+ "exp": issued_at + _OAUTH_CLIENT_ASSERTION_LIFETIME_SECONDS,
60
+ "jti": str(uuid.uuid4()),
61
+ },
62
+ self._private_key,
63
+ algorithm="RS256",
64
+ headers={"kid": self._kid},
65
+ )
66
+ return {"x-modal-oauth-client-assertion": assertion}
@@ -0,0 +1,17 @@
1
+ aiohappyeyeballs==2.7.1
2
+ aiohttp==3.14.5
3
+ aiosignal==1.4.0
4
+ attrs==26.1.0
5
+ cbor2==5.9.0
6
+ certifi==2026.10.19
7
+ frozenlist==1.8.0
8
+ grpclib==0.4.9
9
+ h2==4.4.1
10
+ hpack==4.2.0
11
+ hyperframe==6.1.0
12
+ idna==3.20
13
+ multidict==7.1.0
14
+ propcache==0.5.4
15
+ protobuf==7.36.2
16
+ typing_extensions==4.16.0
17
+ yarl==1.25.1
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "debian": {
3
3
  "PREVIEW": "trixie",
4
+ "2026.10": "trixie",
4
5
  "2025.06": "bookworm",
5
6
  "2024.10": "bookworm",
6
7
  "2024.04": "bookworm",
@@ -38,6 +39,7 @@
38
39
  },
39
40
  "micromamba": {
40
41
  "PREVIEW": "2.9.0-debian13-slim",
42
+ "2026.10": "2.9.0-debian13-slim",
41
43
  "2025.06": "2.1.1-debian12-slim",
42
44
  "2024.10": "1.5.10-bookworm-slim",
43
45
  "2024.04": "1.5.8-bookworm-slim",
@@ -45,6 +47,7 @@
45
47
  },
46
48
  "package_tools": {
47
49
  "PREVIEW": "pip wheel uv",
50
+ "2026.10": "pip wheel uv",
48
51
  "2025.06": "pip wheel uv",
49
52
  "2024.10": "pip wheel uv",
50
53
  "2024.04": "pip wheel uv",
@@ -33,6 +33,8 @@ def show(redact: bool, include_internal: bool, json: bool):
33
33
  config_dict["oauth_refresh_token"] = "***"
34
34
  if redact and config_dict.get("oauth_client_secret"):
35
35
  config_dict["oauth_client_secret"] = "***"
36
+ if redact and config_dict.get("oauth_jwt_key"):
37
+ config_dict["oauth_jwt_key"] = "***"
36
38
 
37
39
  # Accept --json as a no-op for consistency; we may change the default formatting later
38
40
  OutputManager.get().print_json(dumps(config_dict))
@@ -260,26 +260,35 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
260
260
  @click.option(
261
261
  "--name",
262
262
  default=None,
263
- help="Endpoint name. Defaults to inference-{number} if not provided.",
263
+ help="Endpoint name. If omitted, Modal chooses a name.",
264
264
  )
265
265
  @click.option(
266
266
  "--model",
267
267
  required=True,
268
268
  help="Hugging Face repo ID for the base model architecture (e.g., 'Qwen/Qwen3.6-27B-FP8').",
269
269
  )
270
+ @click.option(
271
+ "--shared",
272
+ is_flag=True,
273
+ default=False,
274
+ help="Create a Shared Endpoint. Defaults to a Dedicated Endpoint.",
275
+ )
270
276
  @click.option(
271
277
  "--routing-region",
272
278
  "routing_region",
273
279
  multiple=True,
274
280
  callback=_single_routing_region_callback,
275
- help=f"Region to route inference requests through. Defaults to {_DEFAULT_ROUTING_REGION}.",
281
+ help=(
282
+ f"Region to route inference requests through. Defaults to {_DEFAULT_ROUTING_REGION}. "
283
+ "Shared Endpoint routing cannot currently be customized."
284
+ ),
276
285
  )
277
286
  @click.option(
278
287
  "--compute-region",
279
288
  "compute_regions",
280
289
  multiple=True,
281
290
  help=(
282
- "Region to run Endpoint containers in. May be specified multiple times. "
291
+ "Region to run Dedicated Endpoint containers in. May be specified multiple times. "
283
292
  "This incurs a region selection price multiplier."
284
293
  ),
285
294
  )
@@ -287,13 +296,16 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
287
296
  "--colocate-compute",
288
297
  is_flag=True,
289
298
  default=False,
290
- help="Run all containers within the routing region. This incurs a region selection price multiplier.",
299
+ help=(
300
+ "Run all Dedicated Endpoint containers within the routing region. "
301
+ "This incurs a region selection price multiplier."
302
+ ),
291
303
  )
292
304
  @click.option(
293
305
  "--unauthenticated",
294
306
  is_flag=True,
295
307
  default=False,
296
- help="Allow unauthenticated HTTP requests to the endpoint.",
308
+ help="Allow unauthenticated HTTP requests to a Dedicated Endpoint.",
297
309
  )
298
310
  @click.option("--custom-hf-repo", default=None, help="Hugging Face repo ID for fine-tuned model weights.")
299
311
  @click.option("--custom-hf-revision", default=None, help="Git revision for --custom-hf-repo.")
@@ -304,6 +316,7 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
304
316
  async def create(
305
317
  name: Optional[str],
306
318
  model: Optional[str] = None,
319
+ shared: bool = False,
307
320
  routing_region: str = _DEFAULT_ROUTING_REGION,
308
321
  compute_regions: tuple[str, ...] = (),
309
322
  colocate_compute: bool = False,
@@ -317,13 +330,21 @@ async def create(
317
330
  ):
318
331
  """Deploy a new Endpoint.
319
332
 
333
+ Creates a Dedicated Endpoint by default, with support for custom weights and compute placement.
334
+ Use --shared to serve a supported base model with Modal-managed capacity.
335
+
320
336
  Examples:
321
337
 
322
- Create an Endpoint from a base model:
338
+ Create a Dedicated Endpoint from a base model:
323
339
  ```bash
324
340
  modal endpoint create --model Qwen/Qwen3.6-27B-FP8
325
341
  ```
326
342
 
343
+ Create a Shared Endpoint from a supported base model:
344
+ ```bash
345
+ modal endpoint create --shared --model moonshotai/Kimi-K3
346
+ ```
347
+
327
348
  Create an Endpoint with an explicit name:
328
349
  ```bash
329
350
  modal endpoint create --name qwen-chat --model Qwen/Qwen3.6-27B-FP8
@@ -348,6 +369,22 @@ async def create(
348
369
  ```
349
370
 
350
371
  """
372
+ if shared:
373
+ for option, provided in (
374
+ ("--compute-region", bool(compute_regions)),
375
+ ("--colocate-compute", colocate_compute),
376
+ ("--unauthenticated", unauthenticated),
377
+ ("--custom-hf-repo", custom_hf_repo is not None),
378
+ ("--custom-hf-revision", custom_hf_revision is not None),
379
+ ("--custom-hf-token", custom_hf_token is not None),
380
+ ("--custom-volume-name", custom_volume_name is not None),
381
+ ("--custom-volume-path", custom_volume_path is not None),
382
+ ):
383
+ if provided:
384
+ raise click.UsageError(f"{option} cannot be used with --shared.")
385
+ if routing_region != _DEFAULT_ROUTING_REGION:
386
+ raise click.UsageError(f"Shared Endpoints require --routing-region {_DEFAULT_ROUTING_REGION}.")
387
+
351
388
  if compute_regions and colocate_compute:
352
389
  raise click.UsageError("--compute-region and --colocate-compute are mutually exclusive.")
353
390
  if custom_hf_repo and custom_volume_name:
@@ -403,13 +440,18 @@ async def create(
403
440
  model=model_source,
404
441
  environment_name=environment_name,
405
442
  unauthenticated=unauthenticated,
443
+ serving_mode=(api_pb2.ENDPOINT_SERVING_MODE_SHARED if shared else api_pb2.ENDPOINT_SERVING_MODE_DEDICATED),
406
444
  )
407
445
  if name:
408
446
  req.name = name
409
447
  resp = await client._stub.EndpointCreate(req)
410
448
 
411
449
  output = OutputManager.get()
412
- output.print(f"[green]✓[/green] Endpoint '{resp.name}' ({resp.endpoint_id}) was created and started provisioning.")
450
+ serving_mode = "Shared" if shared else "Dedicated"
451
+ output.print(
452
+ f"[green]✓[/green] {serving_mode} Endpoint '{resp.name}' ({resp.endpoint_id}) "
453
+ "was created and started provisioning."
454
+ )
413
455
  if resp.model_name:
414
456
  output.print(f' → Send [cyan]"model": "{resp.model_name}"[/cyan] in requests to the Endpoint.')
415
457
  if resp.endpoint_page_url: