modal 1.6.3.dev11__tar.gz → 1.6.3.dev12__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 (249) hide show
  1. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/PKG-INFO +4 -2
  2. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_image.py +74 -19
  3. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/grpc_utils.py +23 -4
  4. modal-1.6.3.dev12/modal/_utils/oauth.py +66 -0
  5. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/config.py +2 -0
  6. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/profile.py +1 -0
  7. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/token.py +1 -0
  8. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/client.py +39 -9
  9. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/client.pyi +42 -8
  10. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/config.py +3 -1
  11. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/image.pyi +15 -6
  12. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/network_file_system.py +4 -8
  13. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/network_file_system.pyi +10 -16
  14. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/token_flow.py +1 -0
  15. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/PKG-INFO +4 -2
  16. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/SOURCES.txt +1 -0
  17. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/requires.txt +4 -1
  18. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_grpc.py +32 -0
  19. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2.py +1520 -1411
  20. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2.pyi +234 -0
  21. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2_grpc.py +66 -0
  22. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/api_pb2_grpc.pyi +20 -0
  23. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/modal_api_grpc.py +2 -0
  24. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_version/__init__.py +1 -1
  25. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/pyproject.toml +5 -3
  26. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/LICENSE +0 -0
  27. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/README.md +0 -0
  28. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/__init__.py +0 -0
  29. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/__main__.py +0 -0
  30. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_billing.py +0 -0
  31. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_cluster.py +0 -0
  32. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_cluster.pyi +0 -0
  33. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_container_entrypoint.py +0 -0
  34. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_environments.py +0 -0
  35. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_function_variants.py +0 -0
  36. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_functions.py +0 -0
  37. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_grpc_client.py +0 -0
  38. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_ipython.py +0 -0
  39. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_load_context.py +0 -0
  40. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_location.py +0 -0
  41. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs.py +0 -0
  42. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs_manager.py +0 -0
  43. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_logs_manager.pyi +0 -0
  44. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_metrics.py +0 -0
  45. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_metrics.pyi +0 -0
  46. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_object.py +0 -0
  47. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_outbound_policy.py +0 -0
  48. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_outbound_policy.pyi +0 -0
  49. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/__init__.py +0 -0
  50. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/manager.py +0 -0
  51. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/pty.py +0 -0
  52. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/rich.py +0 -0
  53. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_output/status.py +0 -0
  54. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_partial_function.py +0 -0
  55. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_resolver.py +0 -0
  56. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_resources.py +0 -0
  57. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/__init__.py +0 -0
  58. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/asgi.py +0 -0
  59. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/container_io_manager.py +0 -0
  60. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
  61. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/execution_context.py +0 -0
  62. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/execution_context.pyi +0 -0
  63. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  64. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/task_lifecycle_manager.py +0 -0
  65. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  66. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/telemetry.py +0 -0
  67. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
  68. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_runtime/user_code_imports.py +0 -0
  69. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_serialization.py +0 -0
  70. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_server.py +0 -0
  71. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_server.pyi +0 -0
  72. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_supports_logs.py +0 -0
  73. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_traceback.py +0 -0
  74. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_tunnel.py +0 -0
  75. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_tunnel.pyi +0 -0
  76. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_type_manager.py +0 -0
  77. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/__init__.py +0 -0
  78. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/app_utils.py +0 -0
  79. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/async_utils.py +0 -0
  80. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/auth_token_manager.py +0 -0
  81. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/blob_utils.py +0 -0
  82. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/browser_utils.py +0 -0
  83. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
  84. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/curl_utils.py +0 -0
  85. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/deprecation.py +0 -0
  86. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/display_utils.py +0 -0
  87. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/docker_utils.py +0 -0
  88. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/function_utils.py +0 -0
  89. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/git_utils.py +0 -0
  90. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/grpc_testing.py +0 -0
  91. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/hash_utils.py +0 -0
  92. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/http_utils.py +0 -0
  93. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/idle_countdown.py +0 -0
  94. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/jwt_utils.py +0 -0
  95. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/logger.py +0 -0
  96. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/mount_utils.py +0 -0
  97. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/name_utils.py +0 -0
  98. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/package_utils.py +0 -0
  99. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/pattern_utils.py +0 -0
  100. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/proxy_support.py +0 -0
  101. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/rand_pb_testing.py +0 -0
  102. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/sandbox_fs_utils.py +0 -0
  103. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/shell_utils.py +0 -0
  104. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_utils/time_utils.py +0 -0
  105. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/__init__.py +0 -0
  106. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  107. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/cloudpickle.py +0 -0
  108. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/tblib.py +0 -0
  109. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_vendor/version.py +0 -0
  110. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_watcher.py +0 -0
  111. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/_workspace.py +0 -0
  112. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/app.py +0 -0
  113. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/app.pyi +0 -0
  114. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/billing.py +0 -0
  115. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/billing.pyi +0 -0
  116. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2023.12.312.txt +0 -0
  117. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2023.12.txt +0 -0
  118. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2024.04.txt +0 -0
  119. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2024.10.txt +0 -0
  120. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/2025.06.txt +0 -0
  121. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/PREVIEW.txt +0 -0
  122. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/README.md +0 -0
  123. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/builder/base-images.json +0 -0
  124. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/call_graph.py +0 -0
  125. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/__init__.py +0 -0
  126. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_download.py +0 -0
  127. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_help.py +0 -0
  128. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_logs.py +0 -0
  129. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_stats.py +0 -0
  130. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/_traceback.py +0 -0
  131. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/app.py +0 -0
  132. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/billing.py +0 -0
  133. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/changelog.py +0 -0
  134. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/container.py +0 -0
  135. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/curl.py +0 -0
  136. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/dashboard.py +0 -0
  137. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/dict.py +0 -0
  138. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/endpoint.py +0 -0
  139. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/entry_point.py +0 -0
  140. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/environment.py +0 -0
  141. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/function.py +0 -0
  142. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/image.py +0 -0
  143. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/import_refs.py +0 -0
  144. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/logo.py +0 -0
  145. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/network_file_system.py +0 -0
  146. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/queues.py +0 -0
  147. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/run.py +0 -0
  148. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/secret.py +0 -0
  149. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/selector.py +0 -0
  150. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/server.py +0 -0
  151. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/shell.py +0 -0
  152. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/skills.py +0 -0
  153. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/utils.py +0 -0
  154. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/volume.py +0 -0
  155. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cli/workspace.py +0 -0
  156. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cloud_bucket_mount.py +0 -0
  157. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cloud_bucket_mount.pyi +0 -0
  158. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cls.py +0 -0
  159. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cls.pyi +0 -0
  160. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cluster.py +0 -0
  161. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/cluster.pyi +0 -0
  162. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/container_process.py +0 -0
  163. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/container_process.pyi +0 -0
  164. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/dict.py +0 -0
  165. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/dict.pyi +0 -0
  166. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/environments.py +0 -0
  167. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/environments.pyi +0 -0
  168. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/exception.py +0 -0
  169. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/__init__.py +0 -0
  170. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/_flash_container_entry.py +0 -0
  171. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash.py +0 -0
  172. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash.pyi +0 -0
  173. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash_server.py +0 -0
  174. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/flash_server.pyi +0 -0
  175. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/experimental/ipython.py +0 -0
  176. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/file_pattern_matcher.py +0 -0
  177. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/functions.py +0 -0
  178. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/functions.pyi +0 -0
  179. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/image.py +0 -0
  180. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/io_streams.py +0 -0
  181. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/io_streams.pyi +0 -0
  182. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/mount.py +0 -0
  183. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/mount.pyi +0 -0
  184. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/object.py +0 -0
  185. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/object.pyi +0 -0
  186. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/output.py +0 -0
  187. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/parallel_map.py +0 -0
  188. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/parallel_map.pyi +0 -0
  189. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/partial_function.py +0 -0
  190. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/partial_function.pyi +0 -0
  191. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/proxy.py +0 -0
  192. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/proxy.pyi +0 -0
  193. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/py.typed +0 -0
  194. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/queue.py +0 -0
  195. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/queue.pyi +0 -0
  196. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/retries.py +0 -0
  197. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/runner.py +0 -0
  198. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/runner.pyi +0 -0
  199. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/running_app.py +0 -0
  200. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/__init__.py +0 -0
  201. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_common.py +0 -0
  202. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_filesystem.py +0 -0
  203. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_persistent.py +0 -0
  204. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_v1.py +0 -0
  205. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_impl_v2.py +0 -0
  206. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_sandbox.py +0 -0
  207. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_sidecar.py +0 -0
  208. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_snapshot.py +0 -0
  209. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/_task_command_router_client.py +0 -0
  210. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/sandbox.py +0 -0
  211. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/sandbox/sandbox.pyi +0 -0
  212. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/schedule.py +0 -0
  213. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/scheduler_placement.py +0 -0
  214. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/secret.py +0 -0
  215. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/secret.pyi +0 -0
  216. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/server.py +0 -0
  217. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/server.pyi +0 -0
  218. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/serving.py +0 -0
  219. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/serving.pyi +0 -0
  220. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/skills/modal/SKILL.md +0 -0
  221. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/stream_type.py +0 -0
  222. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/token_flow.pyi +0 -0
  223. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/types.py +0 -0
  224. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/volume.py +0 -0
  225. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/volume.pyi +0 -0
  226. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/workspace.py +0 -0
  227. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal/workspace.pyi +0 -0
  228. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/dependency_links.txt +0 -0
  229. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/entry_points.txt +0 -0
  230. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal.egg-info/top_level.txt +0 -0
  231. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/__init__.py +0 -0
  232. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_cli_docs.py +0 -0
  233. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
  234. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_reference_docs.py +0 -0
  235. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
  236. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/__init__.py +0 -0
  237. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/mdmd.py +0 -0
  238. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/signatures.py +0 -0
  239. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/mdmd/types.py +0 -0
  240. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_docs/reference_intro.md +0 -0
  241. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/__init__.py +0 -0
  242. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/py.typed +0 -0
  243. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_grpc.py +0 -0
  244. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2.py +0 -0
  245. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2.pyi +0 -0
  246. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  247. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  248. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/modal_version/__main__.py +0 -0
  249. {modal-1.6.3.dev11 → modal-1.6.3.dev12}/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.dev12
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
@@ -445,6 +445,40 @@ def _create_context_mount_function(
445
445
  return auto_created_context_mount_fn
446
446
 
447
447
 
448
+ _REGISTRY_AUTH_KEY_SETS: dict["api_pb2.RegistryAuthType.ValueType", list[set[str]]] = {
449
+ api_pb2.REGISTRY_AUTH_TYPE_AWS: [
450
+ {"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"},
451
+ {"AWS_ROLE_ARN", "AWS_REGION"},
452
+ ],
453
+ api_pb2.REGISTRY_AUTH_TYPE_GCP: [{"SERVICE_ACCOUNT_JSON"}],
454
+ api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: [{"REGISTRY_USERNAME", "REGISTRY_PASSWORD"}],
455
+ }
456
+
457
+ _REGISTRY_AUTH_NAMES = {
458
+ api_pb2.REGISTRY_AUTH_TYPE_AWS: "AWS",
459
+ api_pb2.REGISTRY_AUTH_TYPE_GCP: "GCP",
460
+ api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS: "username and password",
461
+ }
462
+
463
+ _REGISTRY_SECRET_KEYS_HELP = (
464
+ "Registry Secret must include exactly one of: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION "
465
+ "(or AWS_ROLE_ARN and AWS_REGION) for AWS ECR; SERVICE_ACCOUNT_JSON for GCP Artifact Registry; "
466
+ "or REGISTRY_USERNAME and REGISTRY_PASSWORD for other registries."
467
+ )
468
+
469
+
470
+ def _infer_registry_auth_type(keys: set[str]) -> "api_pb2.RegistryAuthType.ValueType":
471
+ matches = [auth for auth, key_sets in _REGISTRY_AUTH_KEY_SETS.items() if any(ks <= keys for ks in key_sets)]
472
+ if not matches:
473
+ raise InvalidError(f"{_REGISTRY_SECRET_KEYS_HELP} Found keys: {', '.join(sorted(keys)) or 'none'}.")
474
+ if len(matches) > 1:
475
+ names = ", ".join(_REGISTRY_AUTH_NAMES[auth] for auth in matches)
476
+ raise InvalidError(
477
+ f"Registry Secret has keys for more than one kind of authentication ({names}). {_REGISTRY_SECRET_KEYS_HELP}"
478
+ )
479
+ return matches[0]
480
+
481
+
448
482
  class _ImageRegistryConfig:
449
483
  """mdmd:hidden"""
450
484
 
@@ -453,13 +487,21 @@ class _ImageRegistryConfig:
453
487
  # TODO: change to _PUBLIC after worker starts handling it.
454
488
  registry_auth_type: "api_pb2.RegistryAuthType.ValueType" = api_pb2.REGISTRY_AUTH_TYPE_UNSPECIFIED,
455
489
  secret: _Secret | None = None,
490
+ *,
491
+ infer_auth_type: bool = False,
456
492
  ):
457
493
  self.registry_auth_type = registry_auth_type
458
494
  self.secret = secret
459
-
460
- def get_proto(self) -> api_pb2.ImageRegistryConfig:
495
+ self.infer_auth_type = infer_auth_type
496
+
497
+ async def get_proto(self) -> api_pb2.ImageRegistryConfig:
498
+ registry_auth_type = self.registry_auth_type
499
+ if self.infer_auth_type and self.secret:
500
+ # The Secret is an Image dependency, so it's hydrated by now and its keys are cached
501
+ keys = await self.secret._get_keys()
502
+ registry_auth_type = _infer_registry_auth_type(keys)
461
503
  return api_pb2.ImageRegistryConfig(
462
- registry_auth_type=self.registry_auth_type,
504
+ registry_auth_type=registry_auth_type,
463
505
  secret_id=(self.secret.object_id if self.secret else ""),
464
506
  )
465
507
 
@@ -762,7 +804,7 @@ class _Image(_Object, type_prefix="im"):
762
804
  secret_ids=[secret.object_id for secret in secrets],
763
805
  context_mount_id=(context_mount.object_id if context_mount else ""),
764
806
  gpu_config=gpu_config,
765
- image_registry_config=image_registry_config.get_proto(),
807
+ image_registry_config=await image_registry_config.get_proto(),
766
808
  runtime=config.get("function_runtime"),
767
809
  runtime_debug=config.get("function_runtime_debug"),
768
810
  build_function=_build_function,
@@ -2255,16 +2297,21 @@ class _Image(_Object, type_prefix="im"):
2255
2297
  remaining commands run. This might be useful if you want a custom Python installation or to
2256
2298
  set a `SHELL`. Prefer `run_commands()` when possible though.
2257
2299
 
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`).
2300
+ To pull from a private registry, set the `secret` parameter to a `modal.Secret` holding
2301
+ registry credentials. The kind of authentication follows from the keys in the Secret:
2302
+
2303
+ - AWS ECR: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` (optionally `AWS_SESSION_TOKEN`),
2304
+ or `AWS_ROLE_ARN` and `AWS_REGION` to assume a role with [OIDC](https://modal.com/docs/guide/oidc-integration).
2305
+ See `Image.from_aws_ecr()` for details.
2306
+ - GCP Artifact Registry: `SERVICE_ACCOUNT_JSON`, holding the service account key's JSON.
2307
+ See `Image.from_gcp_artifact_registry()` for details.
2308
+ - Other registries: a username (`REGISTRY_USERNAME`) and an access token or password (`REGISTRY_PASSWORD`).
2261
2309
 
2262
- To authenticate against private registries with credentials from a cloud provider,
2263
- use `Image.from_gcp_artifact_registry()` or `Image.from_aws_ecr()`.
2310
+ The Secret must have the keys for exactly one of these; otherwise an error is raised.
2264
2311
 
2265
2312
  Args:
2266
2313
  tag: Registry image reference (e.g. ``python:3.11-slim``).
2267
- secret: Optional secret for static registry credentials.
2314
+ secret: Optional Secret with registry credentials; its keys determine the kind of authentication.
2268
2315
  setup_dockerfile_commands: Extra Dockerfile lines run after ``FROM`` during base setup.
2269
2316
  force_build: If True, skip cached image builds.
2270
2317
  add_python: Optional standalone Python series to inject when the base image lacks Python.
@@ -2278,6 +2325,10 @@ class _Image(_Object, type_prefix="im"):
2278
2325
  modal.Image.from_registry("python:3.11-slim-bookworm")
2279
2326
  modal.Image.from_registry("ubuntu:22.04", add_python="3.11")
2280
2327
  modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
2328
+ modal.Image.from_registry(
2329
+ "ghcr.io/my-org/my-image:latest",
2330
+ secret=modal.Secret.from_name("my-registry-secret"),
2331
+ )
2281
2332
  ```
2282
2333
  """
2283
2334
 
@@ -2292,7 +2343,7 @@ class _Image(_Object, type_prefix="im"):
2292
2343
  )
2293
2344
 
2294
2345
  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)
2346
+ kwargs["image_registry_config"] = _ImageRegistryConfig(secret=secret, infer_auth_type=True)
2296
2347
 
2297
2348
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
2298
2349
  commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
@@ -2562,10 +2613,6 @@ class _Image(_Object, type_prefix="im"):
2562
2613
  force_build=force_build,
2563
2614
  )
2564
2615
 
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
2616
  def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
2570
2617
  return (
2571
2618
  _Mount.from_name(
@@ -2606,9 +2653,12 @@ class _Image(_Object, type_prefix="im"):
2606
2653
  # Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
2607
2654
  from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
2608
2655
  if from_index is None:
2609
- image = python_image(dockerfile_image(dockerfile_lines))
2656
+ image = dockerfile_image(dockerfile_lines)
2610
2657
  else:
2611
- from_lines = dockerfile_lines[: from_index + 1]
2658
+ # Drop the lines above FROM so Dockerfiles that differ only in header comments or blank lines
2659
+ # share the cached image. Those lines can only be comments or blank lines, since
2660
+ # splittable_from_line_index rejects parser directives.
2661
+ from_lines = [dockerfile_lines[from_index]]
2612
2662
  from_image = _Image._from_args(
2613
2663
  dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
2614
2664
  force_build=force_build,
@@ -2616,9 +2666,14 @@ class _Image(_Object, type_prefix="im"):
2616
2666
  rest = dockerfile_lines[from_index + 1 :]
2617
2667
  rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
2618
2668
  if rest_is_empty:
2619
- image = python_image(from_image)
2669
+ image = from_image
2620
2670
  else:
2621
- image = python_image(dockerfile_image(["FROM base", *rest], base_images={"base": from_image}))
2671
+ image = dockerfile_image(["FROM base", *rest], base_images={"base": from_image})
2672
+ if add_python or builder_version <= "2025.06":
2673
+ # Before 2025.06 we always created a python_image() layer because it's where we installed the
2674
+ # client dependencies. From 2025.06, we no longer install client dependencies in base layers,
2675
+ # so this was a no-op layer when add_python= is not used. Skip it on later image builders.
2676
+ image = python_image(image)
2622
2677
  await resolver.load(image, load_context)
2623
2678
  self._hydrate_from_other(image)
2624
2679
  self._serve_mounts = image._serve_mounts
@@ -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}
@@ -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))
@@ -67,6 +67,7 @@ async def list_(json: bool | None = False):
67
67
  "MODAL_OAUTH_REFRESH_TOKEN",
68
68
  "MODAL_OAUTH_CLIENT_ID",
69
69
  "MODAL_OAUTH_CLIENT_SECRET",
70
+ "MODAL_OAUTH_JWT_KEY",
70
71
  )
71
72
  if any(env_var in os.environ for env_var in credential_env_vars):
72
73
  try:
@@ -36,6 +36,7 @@ async def info():
36
36
  "MODAL_OAUTH_REFRESH_TOKEN",
37
37
  "MODAL_OAUTH_CLIENT_ID",
38
38
  "MODAL_OAUTH_CLIENT_SECRET",
39
+ "MODAL_OAUTH_JWT_KEY",
39
40
  ):
40
41
  if os.environ.get(env_var):
41
42
  env_vars.append(env_var)
@@ -35,7 +35,8 @@ HEARTBEAT_TIMEOUT: float = HEARTBEAT_INTERVAL + 0.1
35
35
  class _OAuthCredentials:
36
36
  refresh_token: str = field(repr=False)
37
37
  client_id: str
38
- client_secret: str = field(repr=False)
38
+ client_secret: str | None = field(default=None, repr=False)
39
+ jwt_key: str | None = field(default=None, repr=False)
39
40
 
40
41
 
41
42
  def _get_metadata(
@@ -75,9 +76,10 @@ def _get_metadata(
75
76
  {
76
77
  "x-modal-refresh-token": oauth_credentials.refresh_token,
77
78
  "x-modal-oauth-client-id": oauth_credentials.client_id,
78
- "x-modal-oauth-client-secret": oauth_credentials.client_secret,
79
79
  }
80
80
  )
81
+ if oauth_credentials.client_secret:
82
+ metadata["x-modal-oauth-client-secret"] = oauth_credentials.client_secret
81
83
  if agent_env := _agent_environment():
82
84
  metadata["x-modal-agent-harness"] = urllib.parse.quote(agent_env)
83
85
 
@@ -209,7 +211,19 @@ class _Client:
209
211
  self._cancellation_context = TaskContext(grace=0.5) # allow running rpcs to finish in 0.5s when closing client
210
212
  self._cancellation_context_event_loop = asyncio.get_running_loop()
211
213
  await self._cancellation_context.__aenter__()
212
- self._connection_manager = ConnectionManager(client=self, metadata=metadata)
214
+ metadata_provider = None
215
+ if self._oauth_credentials and self._oauth_credentials.jwt_key:
216
+ from ._utils.oauth import OAuthAssertionProvider
217
+
218
+ metadata_provider = OAuthAssertionProvider(
219
+ self._oauth_credentials.client_id,
220
+ self._oauth_credentials.jwt_key,
221
+ )
222
+ self._connection_manager = ConnectionManager(
223
+ client=self,
224
+ metadata=metadata,
225
+ metadata_provider=metadata_provider,
226
+ )
213
227
  self._control_plane_stub = await self._get_stub(self.server_url)
214
228
  self._auth_token_manager = _AuthTokenManager(self._stub)
215
229
  self._owner_pid = os.getpid()
@@ -289,6 +303,7 @@ class _Client:
289
303
  oauth_refresh_token = c.get("oauth_refresh_token")
290
304
  oauth_client_id = c.get("oauth_client_id")
291
305
  oauth_client_secret = c.get("oauth_client_secret")
306
+ oauth_jwt_key = c.get("oauth_jwt_key")
292
307
  if _is_remote():
293
308
  ignored_credential_settings = [
294
309
  name
@@ -298,6 +313,7 @@ class _Client:
298
313
  ("MODAL_OAUTH_REFRESH_TOKEN", oauth_refresh_token),
299
314
  ("MODAL_OAUTH_CLIENT_ID", oauth_client_id),
300
315
  ("MODAL_OAUTH_CLIENT_SECRET", oauth_client_secret),
316
+ ("MODAL_OAUTH_JWT_KEY", oauth_jwt_key),
301
317
  )
302
318
  if value
303
319
  ]
@@ -311,18 +327,26 @@ class _Client:
311
327
  oauth_credentials = None
312
328
  else:
313
329
  has_token_config = bool(token_id or token_secret)
314
- has_oauth_config = bool(oauth_refresh_token or oauth_client_id or oauth_client_secret)
330
+ has_oauth_config = bool(oauth_refresh_token or oauth_client_id or oauth_client_secret or oauth_jwt_key)
315
331
  if has_token_config and has_oauth_config:
316
332
  raise InvalidError("Modal token credentials and OAuth credentials cannot both be configured.")
317
333
  if has_oauth_config:
318
- if not oauth_refresh_token or not oauth_client_id or not oauth_client_secret:
319
- raise InvalidError("OAuth refresh token, client ID, and client secret must all be configured.")
334
+ if (
335
+ not oauth_refresh_token
336
+ or not oauth_client_id
337
+ or bool(oauth_client_secret) == bool(oauth_jwt_key)
338
+ ):
339
+ raise InvalidError(
340
+ "OAuth refresh token, client ID, and exactly one of client secret or JWT key "
341
+ "must be configured."
342
+ )
320
343
  client_type = api_pb2.CLIENT_TYPE_CLIENT
321
344
  credentials = None
322
345
  oauth_credentials = _OAuthCredentials(
323
346
  refresh_token=oauth_refresh_token,
324
347
  client_id=oauth_client_id,
325
348
  client_secret=oauth_client_secret,
349
+ jwt_key=oauth_jwt_key,
326
350
  )
327
351
  elif token_id and token_secret:
328
352
  client_type = api_pb2.CLIENT_TYPE_CLIENT
@@ -392,7 +416,8 @@ class _Client:
392
416
  refresh_token: str,
393
417
  *,
394
418
  oauth_client_id: str,
395
- oauth_client_secret: str,
419
+ oauth_client_secret: str | None = None,
420
+ oauth_jwt_key: str | None = None,
396
421
  ) -> "_Client":
397
422
  """
398
423
  Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
@@ -401,6 +426,8 @@ class _Client:
401
426
  refresh_token: OAuth refresh token returned by Modal's token endpoint.
402
427
  oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
403
428
  oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
429
+ oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
430
+ Requires `modal[oauth]`.
404
431
 
405
432
  Returns:
406
433
  An authenticated `Client` with its connection opened.
@@ -417,8 +444,10 @@ class _Client:
417
444
  ```
418
445
  """
419
446
  _check_config()
420
- if not refresh_token or not oauth_client_id or not oauth_client_secret:
421
- raise AuthError("OAuth refresh token, client ID, and client secret must all be provided.")
447
+ if not refresh_token or not oauth_client_id or bool(oauth_client_secret) == bool(oauth_jwt_key):
448
+ raise AuthError(
449
+ "OAuth refresh token, client ID, and exactly one of client secret or JWT key must be provided."
450
+ )
422
451
 
423
452
  server_url = config["server_url"]
424
453
  client_type = api_pb2.CLIENT_TYPE_CLIENT
@@ -426,6 +455,7 @@ class _Client:
426
455
  refresh_token=refresh_token,
427
456
  client_id=oauth_client_id,
428
457
  client_secret=oauth_client_secret,
458
+ jwt_key=oauth_jwt_key,
429
459
  )
430
460
  client = _Client(
431
461
  server_url,
@@ -12,13 +12,20 @@ import typing
12
12
  import typing_extensions
13
13
 
14
14
  class _OAuthCredentials:
15
- """_OAuthCredentials(refresh_token: str, client_id: str, client_secret: str)"""
15
+ """_OAuthCredentials(refresh_token: str, client_id: str, client_secret: str | None = None, jwt_key: str | None = None)"""
16
16
 
17
17
  refresh_token: str
18
18
  client_id: str
19
- client_secret: str
19
+ client_secret: typing.Optional[str]
20
+ jwt_key: typing.Optional[str]
20
21
 
21
- def __init__(self, refresh_token: str, client_id: str, client_secret: str) -> None:
22
+ def __init__(
23
+ self,
24
+ refresh_token: str,
25
+ client_id: str,
26
+ client_secret: typing.Optional[str] = None,
27
+ jwt_key: typing.Optional[str] = None,
28
+ ) -> None:
22
29
  """Initialize self. See help(type(self)) for accurate signature."""
23
30
  ...
24
31
 
@@ -80,7 +87,7 @@ class _Client:
80
87
  server_url: str,
81
88
  client_type: int,
82
89
  credentials: typing.Optional[tuple[str, str]],
83
- version: str = "1.6.3.dev11",
90
+ version: str = "1.6.3.dev12",
84
91
  *,
85
92
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
86
93
  ):
@@ -190,7 +197,12 @@ class _Client:
190
197
 
191
198
  @classmethod
192
199
  async def from_oauth_credentials(
193
- cls, refresh_token: str, *, oauth_client_id: str, oauth_client_secret: str
200
+ cls,
201
+ refresh_token: str,
202
+ *,
203
+ oauth_client_id: str,
204
+ oauth_client_secret: typing.Optional[str] = None,
205
+ oauth_jwt_key: typing.Optional[str] = None,
194
206
  ) -> _Client:
195
207
  """Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
196
208
 
@@ -198,6 +210,8 @@ class _Client:
198
210
  refresh_token: OAuth refresh token returned by Modal's token endpoint.
199
211
  oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
200
212
  oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
213
+ oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
214
+ Requires `modal[oauth]`.
201
215
 
202
216
  Returns:
203
217
  An authenticated `Client` with its connection opened.
@@ -290,7 +304,7 @@ class Client:
290
304
  server_url: str,
291
305
  client_type: int,
292
306
  credentials: typing.Optional[tuple[str, str]],
293
- version: str = "1.6.3.dev11",
307
+ version: str = "1.6.3.dev12",
294
308
  *,
295
309
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
296
310
  ):
@@ -490,13 +504,23 @@ class Client:
490
504
  from_credentials: typing.ClassVar[__from_credentials_spec]
491
505
 
492
506
  class __from_oauth_credentials_spec(typing_extensions.Protocol):
493
- def __call__(self, /, refresh_token: str, *, oauth_client_id: str, oauth_client_secret: str) -> Client:
507
+ def __call__(
508
+ self,
509
+ /,
510
+ refresh_token: str,
511
+ *,
512
+ oauth_client_id: str,
513
+ oauth_client_secret: typing.Optional[str] = None,
514
+ oauth_jwt_key: typing.Optional[str] = None,
515
+ ) -> Client:
494
516
  """Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
495
517
 
496
518
  Args:
497
519
  refresh_token: OAuth refresh token returned by Modal's token endpoint.
498
520
  oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
499
521
  oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
522
+ oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
523
+ Requires `modal[oauth]`.
500
524
 
501
525
  Returns:
502
526
  An authenticated `Client` with its connection opened.
@@ -514,13 +538,23 @@ class Client:
514
538
  """
515
539
  ...
516
540
 
517
- async def aio(self, /, refresh_token: str, *, oauth_client_id: str, oauth_client_secret: str) -> Client:
541
+ async def aio(
542
+ self,
543
+ /,
544
+ refresh_token: str,
545
+ *,
546
+ oauth_client_id: str,
547
+ oauth_client_secret: typing.Optional[str] = None,
548
+ oauth_jwt_key: typing.Optional[str] = None,
549
+ ) -> Client:
518
550
  """Constructor based on OAuth credentials; useful for managing Modal on behalf of third-party users.
519
551
 
520
552
  Args:
521
553
  refresh_token: OAuth refresh token returned by Modal's token endpoint.
522
554
  oauth_client_id: Modal-issued OAuth client ID, with an `oc-` prefix.
523
555
  oauth_client_secret: Modal-issued OAuth client secret, with an `ov-` prefix.
556
+ oauth_jwt_key: Unencrypted RSA private key encoded as PEM, with literal or escaped newlines.
557
+ Requires `modal[oauth]`.
524
558
 
525
559
  Returns:
526
560
  An authenticated `Client` with its connection opened.
@@ -9,7 +9,8 @@ Client processes can authenticate in two ways:
9
9
  1. Modal API tokens use `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET`. The
10
10
  `modal token set` command stores these values in `.modal.toml`.
11
11
  2. Third-party OAuth integrations typically inject `MODAL_OAUTH_REFRESH_TOKEN`,
12
- `MODAL_OAUTH_CLIENT_ID`, and `MODAL_OAUTH_CLIENT_SECRET` into the process.
12
+ `MODAL_OAUTH_CLIENT_ID`, and either `MODAL_OAUTH_CLIENT_SECRET` or
13
+ `MODAL_OAUTH_JWT_KEY` into the process.
13
14
 
14
15
  The two authentication methods cannot be combined, and all values for the
15
16
  selected method must be provided.
@@ -370,6 +371,7 @@ _SETTINGS = {
370
371
  "oauth_refresh_token": _Setting(),
371
372
  "oauth_client_id": _Setting(),
372
373
  "oauth_client_secret": _Setting(),
374
+ "oauth_jwt_key": _Setting(),
373
375
  # --- Logging ----------------------------------------------------------------
374
376
  "loglevel": _Setting("WARNING", lambda s: s.upper()),
375
377
  "log_format": _Setting("STRING", lambda s: s.upper()),