modal 1.5.6.dev18__tar.gz → 1.5.6.dev20__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 (241) hide show
  1. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/__init__.py +2 -0
  3. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_functions.py +0 -2
  4. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/user_code_imports.py +0 -1
  5. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/task_command_router_client.py +6 -0
  6. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/app.py +76 -19
  7. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/entry_point.py +1 -3
  8. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/utils.py +14 -5
  9. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/client.pyi +2 -2
  10. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/config.py +1 -0
  11. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/network_file_system.py +12 -0
  12. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/network_file_system.pyi +1 -0
  13. modal-1.5.6.dev20/modal/outbound_policy.py +167 -0
  14. modal-1.5.6.dev20/modal/outbound_policy.pyi +184 -0
  15. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox.py +111 -16
  16. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox.pyi +77 -0
  17. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/types.py +20 -11
  18. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/PKG-INFO +1 -1
  19. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/SOURCES.txt +2 -1
  20. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_reference_docs.py +4 -2
  21. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2.py +1046 -1041
  22. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2.pyi +37 -38
  23. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_version/__init__.py +1 -1
  24. modal-1.5.6.dev18/modal/cli/bootstrap.py +0 -139
  25. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/LICENSE +0 -0
  26. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/README.md +0 -0
  27. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/__main__.py +0 -0
  28. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_billing.py +0 -0
  29. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_clustered_functions.py +0 -0
  30. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_clustered_functions.pyi +0 -0
  31. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_container_entrypoint.py +0 -0
  32. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_environments.py +0 -0
  33. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_function_variants.py +0 -0
  34. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_grpc_client.py +0 -0
  35. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_image.py +0 -0
  36. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_ipython.py +0 -0
  37. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_load_context.py +0 -0
  38. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_location.py +0 -0
  39. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs.py +0 -0
  40. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs_manager.py +0 -0
  41. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_logs_manager.pyi +0 -0
  42. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_object.py +0 -0
  43. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/__init__.py +0 -0
  44. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/manager.py +0 -0
  45. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/pty.py +0 -0
  46. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/rich.py +0 -0
  47. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_output/status.py +0 -0
  48. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_partial_function.py +0 -0
  49. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_resolver.py +0 -0
  50. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_resources.py +0 -0
  51. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/__init__.py +0 -0
  52. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/asgi.py +0 -0
  53. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/container_io_manager.py +0 -0
  54. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/container_io_manager.pyi +0 -0
  55. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/execution_context.py +0 -0
  56. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/execution_context.pyi +0 -0
  57. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  58. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/task_lifecycle_manager.py +0 -0
  59. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  60. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/telemetry.py +0 -0
  61. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_runtime/user_code_event_loop.py +0 -0
  62. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_serialization.py +0 -0
  63. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_server.py +0 -0
  64. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_supports_logs.py +0 -0
  65. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_traceback.py +0 -0
  66. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_tunnel.py +0 -0
  67. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_tunnel.pyi +0 -0
  68. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_type_manager.py +0 -0
  69. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/__init__.py +0 -0
  70. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/app_utils.py +0 -0
  71. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/async_utils.py +0 -0
  72. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/auth_token_manager.py +0 -0
  73. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/blob_utils.py +0 -0
  74. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/browser_utils.py +0 -0
  75. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/bytes_io_segment_payload.py +0 -0
  76. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/curl_utils.py +0 -0
  77. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/deprecation.py +0 -0
  78. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/display_utils.py +0 -0
  79. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/docker_utils.py +0 -0
  80. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/function_utils.py +0 -0
  81. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/git_utils.py +0 -0
  82. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/grpc_testing.py +0 -0
  83. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/grpc_utils.py +0 -0
  84. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/idle_countdown.py +0 -0
  87. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/jwt_utils.py +0 -0
  88. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/logger.py +0 -0
  89. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/mount_utils.py +0 -0
  90. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/name_utils.py +0 -0
  91. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/package_utils.py +0 -0
  92. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/pattern_utils.py +0 -0
  93. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/proxy_support.py +0 -0
  94. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/rand_pb_testing.py +0 -0
  95. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/sandbox_fs_utils.py +0 -0
  96. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/shell_utils.py +0 -0
  97. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_utils/time_utils.py +0 -0
  98. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/__init__.py +0 -0
  99. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  100. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/cloudpickle.py +0 -0
  101. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/tblib.py +0 -0
  102. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_vendor/version.py +0 -0
  103. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_watcher.py +0 -0
  104. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/_workspace.py +0 -0
  105. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/app.py +0 -0
  106. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/app.pyi +0 -0
  107. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/billing.py +0 -0
  108. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/billing.pyi +0 -0
  109. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2023.12.312.txt +0 -0
  110. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2023.12.txt +0 -0
  111. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2024.04.txt +0 -0
  112. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2024.10.txt +0 -0
  113. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/2025.06.txt +0 -0
  114. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/PREVIEW.txt +0 -0
  115. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/README.md +0 -0
  116. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/builder/base-images.json +0 -0
  117. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/call_graph.py +0 -0
  118. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/__init__.py +0 -0
  119. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_download.py +0 -0
  120. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_help.py +0 -0
  121. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_logs.py +0 -0
  122. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_stats.py +0 -0
  123. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/_traceback.py +0 -0
  124. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/billing.py +0 -0
  125. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/changelog.py +0 -0
  126. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/cluster.py +0 -0
  127. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/config.py +0 -0
  128. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/container.py +0 -0
  129. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/curl.py +0 -0
  130. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/dashboard.py +0 -0
  131. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/dict.py +0 -0
  132. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/endpoint.py +0 -0
  133. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/environment.py +0 -0
  134. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/function.py +0 -0
  135. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/image.py +0 -0
  136. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/import_refs.py +0 -0
  137. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/launch.py +0 -0
  138. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/logo.py +0 -0
  139. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/network_file_system.py +0 -0
  140. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/profile.py +0 -0
  141. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/__init__.py +0 -0
  142. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/run_jupyter.py +0 -0
  143. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/programs/vscode.py +0 -0
  144. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/queues.py +0 -0
  145. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/run.py +0 -0
  146. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/secret.py +0 -0
  147. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/selector.py +0 -0
  148. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/server.py +0 -0
  149. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/shell.py +0 -0
  150. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/skills.py +0 -0
  151. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/token.py +0 -0
  152. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/volume.py +0 -0
  153. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cli/workspace.py +0 -0
  154. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/client.py +0 -0
  155. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cloud_bucket_mount.py +0 -0
  156. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cloud_bucket_mount.pyi +0 -0
  157. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cls.py +0 -0
  158. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/cls.pyi +0 -0
  159. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/container_process.py +0 -0
  160. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/container_process.pyi +0 -0
  161. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/dict.py +0 -0
  162. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/dict.pyi +0 -0
  163. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/environments.py +0 -0
  164. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/environments.pyi +0 -0
  165. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/exception.py +0 -0
  166. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/__init__.py +0 -0
  167. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/flash.py +0 -0
  168. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/flash.pyi +0 -0
  169. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/experimental/ipython.py +0 -0
  170. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/file_pattern_matcher.py +0 -0
  171. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/functions.py +0 -0
  172. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/functions.pyi +0 -0
  173. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/image.py +0 -0
  174. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/image.pyi +0 -0
  175. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/io_streams.py +0 -0
  176. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/io_streams.pyi +0 -0
  177. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/mount.py +0 -0
  178. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/mount.pyi +0 -0
  179. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/object.py +0 -0
  180. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/object.pyi +0 -0
  181. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/output.py +0 -0
  182. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/parallel_map.py +0 -0
  183. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/parallel_map.pyi +0 -0
  184. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/partial_function.py +0 -0
  185. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/partial_function.pyi +0 -0
  186. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/proxy.py +0 -0
  187. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/proxy.pyi +0 -0
  188. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/py.typed +0 -0
  189. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/queue.py +0 -0
  190. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/queue.pyi +0 -0
  191. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/retries.py +0 -0
  192. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/runner.py +0 -0
  193. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/runner.pyi +0 -0
  194. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/running_app.py +0 -0
  195. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox_fs.py +0 -0
  196. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/sandbox_fs.pyi +0 -0
  197. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/schedule.py +0 -0
  198. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/scheduler_placement.py +0 -0
  199. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/secret.py +0 -0
  200. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/secret.pyi +0 -0
  201. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/server.py +0 -0
  202. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/server.pyi +0 -0
  203. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/serving.py +0 -0
  204. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/serving.pyi +0 -0
  205. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/skills/modal/SKILL.md +0 -0
  206. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/snapshot.py +0 -0
  207. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/snapshot.pyi +0 -0
  208. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/stream_type.py +0 -0
  209. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/token_flow.py +0 -0
  210. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/token_flow.pyi +0 -0
  211. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/volume.py +0 -0
  212. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/volume.pyi +0 -0
  213. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/workspace.py +0 -0
  214. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal/workspace.pyi +0 -0
  215. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/dependency_links.txt +0 -0
  216. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/entry_points.txt +0 -0
  217. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/requires.txt +0 -0
  218. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal.egg-info/top_level.txt +0 -0
  219. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/__init__.py +0 -0
  220. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_cli_docs.py +0 -0
  221. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_cli_docs_main.py +0 -0
  222. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/gen_reference_docs_main.py +0 -0
  223. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/__init__.py +0 -0
  224. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/mdmd.py +0 -0
  225. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/signatures.py +0 -0
  226. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/mdmd/types.py +0 -0
  227. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_docs/reference_intro.md +0 -0
  228. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/__init__.py +0 -0
  229. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_grpc.py +0 -0
  230. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2_grpc.py +0 -0
  231. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/api_pb2_grpc.pyi +0 -0
  232. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/modal_api_grpc.py +0 -0
  233. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/py.typed +0 -0
  234. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_grpc.py +0 -0
  235. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2.py +0 -0
  236. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2.pyi +0 -0
  237. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  238. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  239. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/modal_version/__main__.py +0 -0
  240. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/pyproject.toml +0 -0
  241. {modal-1.5.6.dev18 → modal-1.5.6.dev20}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev18
3
+ Version: 1.5.6.dev20
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,6 +23,7 @@ try:
23
23
  from .functions import Function, FunctionCall
24
24
  from .image import Image
25
25
  from .network_file_system import NetworkFileSystem
26
+ from .outbound_policy import OutboundPolicy
26
27
  from .output import enable_output
27
28
  from .partial_function import (
28
29
  asgi_app,
@@ -71,6 +72,7 @@ __all__ = [
71
72
  "FunctionCall",
72
73
  "Image",
73
74
  "NetworkFileSystem",
75
+ "OutboundPolicy",
74
76
  "Period",
75
77
  "Probe",
76
78
  "Proxy",
@@ -1103,8 +1103,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1103
1103
  is_class=function_definition.is_class,
1104
1104
  class_parameter_info=function_definition.class_parameter_info,
1105
1105
  is_method=function_definition.is_method,
1106
- use_function_id=function_definition.use_function_id,
1107
- use_method_name=function_definition.use_method_name,
1108
1106
  method_definitions=function_definition.method_definitions,
1109
1107
  method_definitions_set=function_definition.method_definitions_set,
1110
1108
  experimental_options=experimental_options or {},
@@ -562,7 +562,6 @@ def _get_cls_or_user_cls(
562
562
  f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
563
563
  )
564
564
 
565
- assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
566
565
  cls_name = parts[0]
567
566
  cls_or_user_cls = getattr(module, cls_name)
568
567
  return cls_or_user_cls
@@ -1077,6 +1077,12 @@ class TaskCommandRouterClient:
1077
1077
  lambda: self._call_with_auth_retry(self._stub.TaskSetNetworkAccess, request)
1078
1078
  )
1079
1079
 
1080
+ async def set_outbound_policy(self, request: sr_pb2.TaskSetOutboundPolicyRequest):
1081
+ with grpc_error_converter():
1082
+ return await call_with_retries_on_transient_errors(
1083
+ lambda: self._call_with_auth_retry(self._stub.TaskSetOutboundPolicy, request)
1084
+ )
1085
+
1080
1086
  async def reload_volumes(
1081
1087
  self, task_id: str, timeout: float, container_id: str = ""
1082
1088
  ) -> sr_pb2.TaskReloadVolumesResponse:
@@ -3,13 +3,15 @@ import re
3
3
  import sys
4
4
  import time
5
5
  import warnings
6
+ from collections.abc import Mapping
6
7
  from json import dumps
7
8
  from typing import Literal, get_args
8
9
 
9
10
  import click
10
11
  import rich
11
12
  from click import UsageError
12
- from rich.table import Column
13
+ from google.protobuf.json_format import MessageToDict
14
+ from rich.table import Column, Table
13
15
  from rich.text import Text
14
16
 
15
17
  from modal._environments import ensure_env
@@ -620,8 +622,9 @@ async def dashboard(
620
622
  @click.argument("app_identifier")
621
623
  @env_option
622
624
  @click.option("--json", is_flag=True, default=False)
625
+ @click.option("--no-color", is_flag=True, default=False, help="Disable colors in the output.")
623
626
  @synchronizer.create_blocking
624
- async def info(app_identifier: str, *, env: str | None = None, json: bool = False):
627
+ async def info(app_identifier: str, *, env: str | None = None, json: bool = False, no_color: bool = False):
625
628
  """Show an App's lifecycle, Functions, and Servers.
626
629
 
627
630
  Examples:
@@ -650,6 +653,17 @@ async def info(app_identifier: str, *, env: str | None = None, json: bool = Fals
650
653
  state = APP_STATE_TO_MESSAGE.get(lifecycle.app_state, Text("unknown", style="gray"))
651
654
 
652
655
  if json:
656
+ summaries = {
657
+ function_id: MessageToDict(summary, preserving_proto_field_name=True)
658
+ for function_id, summary in resp.function_info_summaries.items()
659
+ }
660
+
661
+ def entries_with_info(entries: Mapping[str, str]) -> dict[str, dict]:
662
+ return {
663
+ name: {"id": function_id, "summary": summaries.get(function_id, {})}
664
+ for name, function_id in sorted(entries.items())
665
+ }
666
+
653
667
  output.print_json(
654
668
  dumps(
655
669
  {
@@ -665,28 +679,71 @@ async def info(app_identifier: str, *, env: str | None = None, json: bool = Fals
665
679
  "stopped_at": timestamp_to_localized_str(lifecycle.stopped_at, json),
666
680
  "stopped_by": lifecycle.stopped_by or None,
667
681
  },
668
- "functions": dict(sorted(app_info.functions.items())),
669
- "servers": dict(sorted(app_info.servers.items())),
682
+ "functions": entries_with_info(app_info.functions),
683
+ "servers": entries_with_info(app_info.servers),
670
684
  }
671
685
  )
672
686
  )
673
687
  return
674
688
 
675
- output.print(
676
- Text.assemble("App: ", (app_info.app_id, "bold"), " - ", (app_info.description, "bold"), " (", state, ")")
677
- )
689
+ header = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
690
+ header.add_row(Text("App:"), Text(app_info.description))
691
+ header.add_row(Text("App ID:"), Text(app_info.app_id))
692
+ header.add_row(Text("State:"), Text(state.plain))
678
693
 
679
- if app_info.functions:
680
- display_table(["Name", "Function ID"], sorted(app_info.functions.items()), title="Functions")
681
- if app_info.servers:
682
- display_table(["Name", "Function ID"], sorted(app_info.servers.items()), title="Servers")
694
+ def event(label: str, timestamp: float, actor: str, version: int = 0) -> None:
695
+ parts = [timestamp_to_localized_str(timestamp, isotz=False), actor]
696
+ if version:
697
+ parts.insert(0, f"v{version}")
698
+ header.add_row(Text(label + ":"), Text(" · ".join(p for p in parts if p)))
683
699
 
684
- events: list[list[Text | str | None]] = [
685
- ["Created", timestamp_to_localized_str(lifecycle.created_at, json), lifecycle.created_by]
686
- ]
687
- if lifecycle.deployed_at:
688
- label = f"Deployed (v{lifecycle.version})" if lifecycle.version else "Deployed"
689
- events.append([label, timestamp_to_localized_str(lifecycle.deployed_at, json), lifecycle.deployed_by])
690
700
  if lifecycle.stopped_at:
691
- events.append(["Stopped", timestamp_to_localized_str(lifecycle.stopped_at, json), lifecycle.stopped_by])
692
- display_table(["Event", "Time", "By"], events, title="Lifecycle")
701
+ event("Stopped", lifecycle.stopped_at, lifecycle.stopped_by)
702
+ if lifecycle.deployed_at:
703
+ event("Deployment", lifecycle.deployed_at, lifecycle.deployed_by, lifecycle.version)
704
+ event("Created", lifecycle.created_at, lifecycle.created_by)
705
+ output.print(header)
706
+
707
+ name_width = max((len(name) for name in [*app_info.functions, *app_info.servers]), default=0)
708
+ for title, entries in (("Functions", app_info.functions), ("Servers", app_info.servers)):
709
+ if not entries:
710
+ continue
711
+ output.print("")
712
+ output.print(Text(f"{title} ({len(entries)}):"))
713
+ for index, (name, function_id) in enumerate(sorted(entries.items())):
714
+ if index:
715
+ output.print("")
716
+ identity = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
717
+ identity.add_column(width=name_width + 2)
718
+ identity.add_column(no_wrap=True)
719
+ identity.add_row(Text(f" {name}"), Text(function_id))
720
+ output.print(identity)
721
+ if function_id not in resp.function_info_summaries:
722
+ continue
723
+ summary = resp.function_info_summaries[function_id]
724
+ hardware = [f"{gpu.count} × {gpu.gpu_type} GPU" for gpu in summary.gpu_config]
725
+ metadata = Text(f" {hardware[0] if hardware else 'CPU'}")
726
+ if len(hardware) > 1:
727
+ metadata.append(f" ({', '.join(hardware[1:])})", style=None if no_color else "bright_black")
728
+ if summary.web_function:
729
+ metadata.append(" · Web Function")
730
+ if schedule := _app_function_schedule(summary.schedule):
731
+ metadata.append(" · ")
732
+ metadata.append(schedule)
733
+ if summary.HasField("requires_proxy_auth") and not summary.requires_proxy_auth:
734
+ metadata.append(" · ")
735
+ metadata.append("Unauthenticated", style=None if no_color else "yellow")
736
+ output.print(metadata)
737
+
738
+
739
+ def _app_function_schedule(schedule: api_pb2.Schedule) -> str | None:
740
+ if schedule.HasField("cron"):
741
+ return f"{schedule.cron.cron_string} ({schedule.cron.timezone or 'UTC'})"
742
+ if schedule.HasField("period"):
743
+ parts = [
744
+ f"{value:g} {field.name.removesuffix('s') if value == 1 else field.name}"
745
+ for field, value in schedule.period.ListFields()
746
+ if value
747
+ ]
748
+ return "Every " + ", ".join(parts)
749
+ return None
@@ -11,7 +11,6 @@ from . import run, shell as shell_module
11
11
  from ._help import ModalCommand, ModalGroup, ModalProfileOption
12
12
  from .app import app_cli
13
13
  from .billing import billing_cli
14
- from .bootstrap import bootstrap
15
14
  from .changelog import changelog
16
15
  from .cluster import cluster_cli
17
16
  from .config import config_cli
@@ -131,8 +130,6 @@ entrypoint_cli.add_command(secret_cli, panel="Storage")
131
130
  entrypoint_cli.add_command(queue_cli, panel="Storage")
132
131
  entrypoint_cli.add_command(volume_cli, panel="Storage")
133
132
 
134
- entrypoint_cli.add_command(setup, panel="Onboarding")
135
- entrypoint_cli.add_command(bootstrap, panel="Onboarding")
136
133
 
137
134
  entrypoint_cli.add_command(workspace_cli, panel="Configuration")
138
135
  entrypoint_cli.add_command(environment_cli, panel="Configuration")
@@ -140,6 +137,7 @@ entrypoint_cli.add_command(profile_cli, panel="Configuration")
140
137
  entrypoint_cli.add_command(config_cli, panel="Configuration")
141
138
  entrypoint_cli.add_command(token_cli, panel="Configuration")
142
139
  entrypoint_cli.add_command(skills_cli, panel="Configuration")
140
+ entrypoint_cli.add_command(setup, panel="Configuration")
143
141
 
144
142
  entrypoint_cli.add_command(billing_cli, panel="Observability")
145
143
  entrypoint_cli.add_command(changelog, panel="Observability")
@@ -248,12 +248,21 @@ async def _resolve_function_id(
248
248
  raise
249
249
 
250
250
  print_server_warnings(response.server_warnings)
251
- return response.function_id, response.handle_metadata
252
-
253
- if _is_function_id(function_identifier):
251
+ function_id = response.function_id
252
+ function = response.function
253
+ metadata = response.handle_metadata
254
+ elif _is_function_id(function_identifier):
254
255
  get_by_id_response = await client.stub.FunctionGetById(
255
256
  api_pb2.FunctionGetByIdRequest(function_id=function_identifier)
256
257
  )
257
- return function_identifier, get_by_id_response.handle_metadata
258
+ function_id = function_identifier
259
+ function = get_by_id_response.function
260
+ metadata = get_by_id_response.handle_metadata
261
+ else:
262
+ raise UsageError(usage)
263
+
264
+ if function.is_server != (object_type == "Server"):
265
+ actual_type = "Server" if function.is_server else "Function"
266
+ raise UsageError(f"'{function_identifier}' is a {actual_type}. Use `modal {actual_type.lower()} {command}`.")
258
267
 
259
- raise UsageError(usage)
268
+ return function_id, metadata
@@ -77,7 +77,7 @@ class _Client:
77
77
  server_url: str,
78
78
  client_type: int,
79
79
  credentials: typing.Optional[tuple[str, str]],
80
- version: str = "1.5.6.dev18",
80
+ version: str = "1.5.6.dev20",
81
81
  *,
82
82
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
83
83
  ):
@@ -277,7 +277,7 @@ class Client:
277
277
  server_url: str,
278
278
  client_type: int,
279
279
  credentials: typing.Optional[tuple[str, str]],
280
- version: str = "1.5.6.dev18",
280
+ version: str = "1.5.6.dev20",
281
281
  *,
282
282
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
283
283
  ):
@@ -377,6 +377,7 @@ _SETTINGS = {
377
377
  "image_builder_version": _Setting(),
378
378
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
379
379
  "sandbox_v2": _Setting(None, transform=_to_boolean),
380
+ "use_control_plane_sidecar_create": _Setting(False, transform=_to_boolean), # For internal/experimental use
380
381
  "snapshot_debug": _Setting(False, transform=_to_boolean),
381
382
  "cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
382
383
  # Used to indicate gVisor cuda-checkpoint automation.
@@ -22,6 +22,7 @@ from ._object import (
22
22
  from ._resolver import Resolver
23
23
  from ._utils.async_utils import TaskContext, aclosing, async_map, sync_or_async_iter, synchronize_api
24
24
  from ._utils.blob_utils import LARGE_FILE_LIMIT, blob_iter, blob_upload_file
25
+ from ._utils.deprecation import deprecation_warning
25
26
  from ._utils.hash_utils import get_sha256_hex
26
27
  from ._utils.name_utils import check_object_name
27
28
  from .client import _Client
@@ -33,6 +34,14 @@ NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
33
34
  ) # 10 min max for transferring files (does not include upload time to s3)
34
35
 
35
36
 
37
+ def _warn_network_file_system_deprecated() -> None:
38
+ deprecation_warning(
39
+ (2026, 9, 16),
40
+ "`modal.NetworkFileSystem` is deprecated and will be removed in a future release. Use `modal.Volume` instead.",
41
+ show_source=False,
42
+ )
43
+
44
+
36
45
  def network_file_system_mount_protos(
37
46
  validated_network_file_systems: list[tuple[str, "_NetworkFileSystem"]],
38
47
  ) -> list[api_pb2.SharedVolumeMount]:
@@ -119,6 +128,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
119
128
  ```
120
129
  """
121
130
  check_object_name(name, "NetworkFileSystem")
131
+ _warn_network_file_system_deprecated()
122
132
 
123
133
  async def _load(
124
134
  self: _NetworkFileSystem, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None
@@ -175,6 +185,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
175
185
  assert await nfs.listdir("/") == []
176
186
  ```
177
187
  """
188
+ _warn_network_file_system_deprecated()
178
189
  if client is None:
179
190
  client = await _Client.from_env()
180
191
  request = api_pb2.SharedVolumeGetOrCreateRequest(
@@ -201,6 +212,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
201
212
  ) -> str:
202
213
  """mdmd:hidden"""
203
214
  check_object_name(deployment_name, "NetworkFileSystem")
215
+ _warn_network_file_system_deprecated()
204
216
  if client is None:
205
217
  client = await _Client.from_env()
206
218
  request = api_pb2.SharedVolumeGetOrCreateRequest(
@@ -9,6 +9,7 @@ import synchronicity.combined_types
9
9
  import typing
10
10
  import typing_extensions
11
11
 
12
+ def _warn_network_file_system_deprecated() -> None: ...
12
13
  def network_file_system_mount_protos(
13
14
  validated_network_file_systems: list[tuple[str, _NetworkFileSystem]],
14
15
  ) -> list[modal_proto.api_pb2.SharedVolumeMount]:
@@ -0,0 +1,167 @@
1
+ # Copyright Modal Labs 2026
2
+ """Immutable configuration for replacing headers in outbound HTTPS traffic."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Mapping, Sequence
9
+
10
+ from modal_proto import api_pb2
11
+
12
+ from ._utils.async_utils import synchronize_api
13
+ from .exception import InvalidError
14
+ from .secret import _Secret
15
+
16
+ _DOMAIN_RE = re.compile(
17
+ r"^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\Z"
18
+ )
19
+ _HEADER_NAME_RE = re.compile(r"^[A-Za-z0-9!#$%&'*+.^_`|~-]+\Z")
20
+
21
+ _MAX_HEADER_REPLACEMENTS = 25
22
+
23
+
24
+ def _header_value_has_invalid_chars(value: str) -> bool:
25
+ return any((ord(c) < 0x20 and c != "\t") or ord(c) == 0x7F for c in value)
26
+
27
+
28
+ def _template_references_key(template: str) -> bool:
29
+ """Whether a header value template references a `$KEY`."""
30
+ i = template.find("$")
31
+ while i != -1 and i + 1 < len(template):
32
+ c = template[i + 1]
33
+ if c == "$":
34
+ i = template.find("$", i + 2)
35
+ continue
36
+ if c.isascii() and (c.isalpha() or c == "_"):
37
+ return True
38
+ i = template.find("$", i + 1)
39
+ return False
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class _HeaderReplacement:
44
+ domain: str
45
+ headers: dict[str, str]
46
+ secret: _Secret | None
47
+
48
+
49
+ class _OutboundPolicy:
50
+ """Immutable configuration for replacing headers in outbound HTTPS requests
51
+ from a Sandbox.
52
+
53
+ Header values support templating with keys in a replacement's secret: a
54
+ `$`-prefixed key name in the secret is replaced with the secret value.
55
+ Literal `$` characters are written `$$`.
56
+
57
+ Secret values never enter the Sandbox: they are resolved and injected into
58
+ matching requests outside the container.
59
+
60
+ Examples:
61
+ ```python
62
+ import modal
63
+
64
+ secret = modal.Secret.from_name("api-token")
65
+
66
+ outbound_policy = (
67
+ modal.OutboundPolicy()
68
+ # Inject a secret-backed Authorization header.
69
+ .with_header_replacement(
70
+ domain="example.com",
71
+ secret=secret,
72
+ headers={"Authorization": "Bearer $API_TOKEN"},
73
+ )
74
+ # Inject a static header into requests to another domain.
75
+ .with_header_replacement(
76
+ domain="modal.com",
77
+ headers={"X-Trace-Token": "trace_abcd"},
78
+ )
79
+ )
80
+
81
+ sb = modal.Sandbox.create(outbound_policy=outbound_policy)
82
+ ```
83
+ """
84
+
85
+ _replacements: tuple[_HeaderReplacement, ...]
86
+
87
+ def __init__(self, _replacements: tuple[_HeaderReplacement, ...] = ()):
88
+ self._replacements = _replacements
89
+
90
+ def with_header_replacement(
91
+ self,
92
+ *,
93
+ domain: str,
94
+ headers: Mapping[str, str],
95
+ secret: _Secret | None = None,
96
+ ) -> _OutboundPolicy:
97
+ """Return a new `OutboundPolicy` with an added header replacement.
98
+
99
+ Args:
100
+ domain: Domain the replacements are scoped to. Supports `*.` wildcard
101
+ prefixes (matching the apex domain and subdomains) and a bare `"*"`.
102
+ headers: Header name -> header value. Values support `$KEY` templates
103
+ referencing keys in the replacement's `secret`.
104
+ secret: Named Secret (e.g. from `Secret.from_name`) whose keys may be
105
+ referenced in the header value templates. Static replacements pass no
106
+ secret.
107
+ """
108
+ replacement = _HeaderReplacement(domain=domain, headers=dict(headers), secret=secret)
109
+ return _OutboundPolicy(self._replacements + (replacement,))
110
+
111
+ def _validate(self) -> None:
112
+ """Check all replacements, raising `InvalidError` on the first violation found."""
113
+ total = 0
114
+ for replacement in self._replacements:
115
+ if replacement.domain != "*" and not _DOMAIN_RE.match(replacement.domain):
116
+ raise InvalidError(f"Invalid domain: {replacement.domain!r}")
117
+ if not replacement.headers:
118
+ raise InvalidError("`headers` must contain at least one header")
119
+ for name, value in replacement.headers.items():
120
+ if not _HEADER_NAME_RE.match(name):
121
+ raise InvalidError(f"Invalid header name: {name!r}")
122
+ if _header_value_has_invalid_chars(value):
123
+ raise InvalidError(f"Header value for {name!r} must not contain control characters (except tab)")
124
+ if replacement.secret is None and _template_references_key(value):
125
+ raise InvalidError(f"Header value for {name!r} references a secret key, but no `secret` was passed")
126
+ if replacement.secret is not None and replacement.secret._is_ephemeral:
127
+ raise InvalidError(
128
+ "Outbound policies only support named secrets (e.g. `Secret.from_name`); "
129
+ "ephemeral secrets (e.g. `Secret.from_dict`) are not yet supported"
130
+ )
131
+ total += len(replacement.headers)
132
+ if total > _MAX_HEADER_REPLACEMENTS:
133
+ raise InvalidError(f"Outbound policy cannot have more than {_MAX_HEADER_REPLACEMENTS} header replacements")
134
+
135
+ def _secrets(self) -> list[_Secret]:
136
+ """Deduplicated list of secrets referenced by the policy's replacements."""
137
+ return list(dict.fromkeys(r.secret for r in self._replacements if r.secret is not None))
138
+
139
+ def _to_proto(self) -> api_pb2.OutboundPolicy:
140
+ """Convert to the wire format. Referenced secrets must be hydrated first."""
141
+ return api_pb2.OutboundPolicy(
142
+ header_replacements=[
143
+ api_pb2.OutboundPolicy.HeaderReplacement(
144
+ domain=replacement.domain,
145
+ secret_id=(replacement.secret.object_id if replacement.secret is not None else ""),
146
+ headers=replacement.headers,
147
+ )
148
+ for replacement in self._replacements
149
+ ]
150
+ )
151
+
152
+
153
+ def _validate_compatible_network_access(
154
+ outbound_policy: _OutboundPolicy | None,
155
+ block_network: bool,
156
+ outbound_domain_allowlist: Sequence[str] | None,
157
+ ) -> None:
158
+ """Reject combinations of network configuration and outbound policy that the server does not support."""
159
+ if outbound_policy is None:
160
+ return
161
+ if block_network:
162
+ raise InvalidError("`outbound_policy` cannot be used when `block_network` is enabled")
163
+ if outbound_domain_allowlist:
164
+ raise InvalidError("`outbound_policy` cannot be used with `outbound_domain_allowlist`")
165
+
166
+
167
+ OutboundPolicy = synchronize_api(_OutboundPolicy)