modal 1.6.2.dev7__tar.gz → 1.6.2.dev8__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 (245) hide show
  1. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/PKG-INFO +1 -1
  2. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/async_utils.py +3 -1
  3. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/blob_utils.py +2 -0
  4. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/_logs.py +5 -5
  5. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/container.py +2 -2
  6. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/function.py +241 -3
  7. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/server.py +280 -3
  8. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/utils.py +29 -1
  9. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/client.pyi +2 -2
  10. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cls.py +1 -1
  11. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cls.pyi +1 -1
  12. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/functions.pyi +6 -6
  13. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/mount.py +12 -1
  14. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/queue.py +1 -1
  15. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/queue.pyi +2 -2
  16. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/volume.py +10 -0
  17. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/volume.pyi +3 -0
  18. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/PKG-INFO +1 -1
  19. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/api_grpc.py +0 -16
  20. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/api_pb2.py +1273 -1321
  21. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/api_pb2.pyi +32 -131
  22. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/api_pb2_grpc.py +0 -33
  23. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/api_pb2_grpc.pyi +0 -10
  24. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/modal_api_grpc.py +0 -1
  25. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_version/__init__.py +1 -1
  26. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/pyproject.toml +1 -0
  27. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/LICENSE +0 -0
  28. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/README.md +0 -0
  29. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/__init__.py +0 -0
  30. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/__main__.py +0 -0
  31. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_billing.py +0 -0
  32. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_cluster.py +0 -0
  33. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_cluster.pyi +0 -0
  34. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_container_entrypoint.py +0 -0
  35. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_environments.py +0 -0
  36. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_function_variants.py +0 -0
  37. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_functions.py +0 -0
  38. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_grpc_client.py +0 -0
  39. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_image.py +0 -0
  40. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_ipython.py +0 -0
  41. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_load_context.py +0 -0
  42. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_location.py +0 -0
  43. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_logs.py +0 -0
  44. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_logs_manager.py +0 -0
  45. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_logs_manager.pyi +0 -0
  46. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_metrics.py +0 -0
  47. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_metrics.pyi +0 -0
  48. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_object.py +0 -0
  49. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_outbound_policy.py +0 -0
  50. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_outbound_policy.pyi +0 -0
  51. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_output/__init__.py +0 -0
  52. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_output/manager.py +0 -0
  53. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_output/pty.py +0 -0
  54. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_output/rich.py +0 -0
  55. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_output/status.py +0 -0
  56. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_partial_function.py +0 -0
  57. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_resolver.py +0 -0
  58. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_resources.py +0 -0
  59. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/__init__.py +0 -0
  60. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/asgi.py +0 -0
  61. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/container_io_manager.py +0 -0
  62. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/container_io_manager.pyi +0 -0
  63. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/execution_context.py +0 -0
  64. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/execution_context.pyi +0 -0
  65. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  66. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/task_lifecycle_manager.py +0 -0
  67. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  68. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/telemetry.py +0 -0
  69. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/user_code_event_loop.py +0 -0
  70. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_runtime/user_code_imports.py +0 -0
  71. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_serialization.py +0 -0
  72. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_server.py +0 -0
  73. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_server.pyi +0 -0
  74. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_supports_logs.py +0 -0
  75. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_traceback.py +0 -0
  76. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_tunnel.py +0 -0
  77. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_tunnel.pyi +0 -0
  78. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_type_manager.py +0 -0
  79. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/__init__.py +0 -0
  80. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/app_utils.py +0 -0
  81. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/auth_token_manager.py +0 -0
  82. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/browser_utils.py +0 -0
  83. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/bytes_io_segment_payload.py +0 -0
  84. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/curl_utils.py +0 -0
  85. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/deprecation.py +0 -0
  86. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/display_utils.py +0 -0
  87. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/docker_utils.py +0 -0
  88. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/function_utils.py +0 -0
  89. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/git_utils.py +0 -0
  90. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/grpc_testing.py +0 -0
  91. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/grpc_utils.py +0 -0
  92. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/hash_utils.py +0 -0
  93. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/http_utils.py +0 -0
  94. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/idle_countdown.py +0 -0
  95. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/jwt_utils.py +0 -0
  96. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/logger.py +0 -0
  97. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/mount_utils.py +0 -0
  98. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/name_utils.py +0 -0
  99. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/package_utils.py +0 -0
  100. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/pattern_utils.py +0 -0
  101. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/proxy_support.py +0 -0
  102. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/rand_pb_testing.py +0 -0
  103. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/sandbox_fs_utils.py +0 -0
  104. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/shell_utils.py +0 -0
  105. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_utils/time_utils.py +0 -0
  106. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_vendor/__init__.py +0 -0
  107. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  108. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_vendor/cloudpickle.py +0 -0
  109. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_vendor/tblib.py +0 -0
  110. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_vendor/version.py +0 -0
  111. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_watcher.py +0 -0
  112. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/_workspace.py +0 -0
  113. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/app.py +0 -0
  114. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/app.pyi +0 -0
  115. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/billing.py +0 -0
  116. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/billing.pyi +0 -0
  117. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/2023.12.312.txt +0 -0
  118. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/2023.12.txt +0 -0
  119. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/2024.04.txt +0 -0
  120. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/2024.10.txt +0 -0
  121. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/2025.06.txt +0 -0
  122. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/PREVIEW.txt +0 -0
  123. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/README.md +0 -0
  124. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/builder/base-images.json +0 -0
  125. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/call_graph.py +0 -0
  126. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/__init__.py +0 -0
  127. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/_download.py +0 -0
  128. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/_help.py +0 -0
  129. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/_stats.py +0 -0
  130. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/_traceback.py +0 -0
  131. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/app.py +0 -0
  132. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/billing.py +0 -0
  133. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/changelog.py +0 -0
  134. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/config.py +0 -0
  135. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/curl.py +0 -0
  136. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/dashboard.py +0 -0
  137. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/dict.py +0 -0
  138. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/endpoint.py +0 -0
  139. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/entry_point.py +0 -0
  140. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/environment.py +0 -0
  141. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/image.py +0 -0
  142. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/import_refs.py +0 -0
  143. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/logo.py +0 -0
  144. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/network_file_system.py +0 -0
  145. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/profile.py +0 -0
  146. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/queues.py +0 -0
  147. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/run.py +0 -0
  148. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/secret.py +0 -0
  149. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/selector.py +0 -0
  150. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/shell.py +0 -0
  151. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/skills.py +0 -0
  152. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/token.py +0 -0
  153. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/volume.py +0 -0
  154. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cli/workspace.py +0 -0
  155. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/client.py +0 -0
  156. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cloud_bucket_mount.py +0 -0
  157. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cloud_bucket_mount.pyi +0 -0
  158. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cluster.py +0 -0
  159. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/cluster.pyi +0 -0
  160. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/config.py +0 -0
  161. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/container_process.py +0 -0
  162. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/container_process.pyi +0 -0
  163. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/dict.py +0 -0
  164. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/dict.pyi +0 -0
  165. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/environments.py +0 -0
  166. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/environments.pyi +0 -0
  167. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/exception.py +0 -0
  168. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/__init__.py +0 -0
  169. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/_flash_container_entry.py +0 -0
  170. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/flash.py +0 -0
  171. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/flash.pyi +0 -0
  172. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/flash_server.py +0 -0
  173. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/flash_server.pyi +0 -0
  174. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/experimental/ipython.py +0 -0
  175. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/file_pattern_matcher.py +0 -0
  176. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/functions.py +0 -0
  177. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/image.py +0 -0
  178. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/image.pyi +0 -0
  179. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/io_streams.py +0 -0
  180. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/io_streams.pyi +0 -0
  181. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/mount.pyi +0 -0
  182. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/network_file_system.py +0 -0
  183. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/network_file_system.pyi +0 -0
  184. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/object.py +0 -0
  185. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/object.pyi +0 -0
  186. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/output.py +0 -0
  187. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/parallel_map.py +0 -0
  188. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/parallel_map.pyi +0 -0
  189. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/partial_function.py +0 -0
  190. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/partial_function.pyi +0 -0
  191. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/proxy.py +0 -0
  192. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/proxy.pyi +0 -0
  193. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/py.typed +0 -0
  194. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/retries.py +0 -0
  195. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/runner.py +0 -0
  196. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/runner.pyi +0 -0
  197. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/running_app.py +0 -0
  198. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/__init__.py +0 -0
  199. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_common.py +0 -0
  200. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_filesystem.py +0 -0
  201. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_sandbox.py +0 -0
  202. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_sidecar.py +0 -0
  203. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_snapshot.py +0 -0
  204. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/_task_command_router_client.py +0 -0
  205. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/sandbox.py +0 -0
  206. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/sandbox/sandbox.pyi +0 -0
  207. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/schedule.py +0 -0
  208. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/scheduler_placement.py +0 -0
  209. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/secret.py +0 -0
  210. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/secret.pyi +0 -0
  211. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/server.py +0 -0
  212. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/server.pyi +0 -0
  213. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/serving.py +0 -0
  214. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/serving.pyi +0 -0
  215. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/skills/modal/SKILL.md +0 -0
  216. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/stream_type.py +0 -0
  217. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/token_flow.py +0 -0
  218. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/token_flow.pyi +0 -0
  219. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/types.py +0 -0
  220. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/workspace.py +0 -0
  221. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal/workspace.pyi +0 -0
  222. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/SOURCES.txt +0 -0
  223. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/dependency_links.txt +0 -0
  224. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/entry_points.txt +0 -0
  225. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/requires.txt +0 -0
  226. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal.egg-info/top_level.txt +0 -0
  227. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/__init__.py +0 -0
  228. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/gen_cli_docs.py +0 -0
  229. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/gen_cli_docs_main.py +0 -0
  230. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/gen_reference_docs.py +0 -0
  231. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/gen_reference_docs_main.py +0 -0
  232. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/mdmd/__init__.py +0 -0
  233. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/mdmd/mdmd.py +0 -0
  234. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/mdmd/signatures.py +0 -0
  235. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/mdmd/types.py +0 -0
  236. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_docs/reference_intro.md +0 -0
  237. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/__init__.py +0 -0
  238. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/py.typed +0 -0
  239. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/task_command_router_grpc.py +0 -0
  240. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/task_command_router_pb2.py +0 -0
  241. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/task_command_router_pb2.pyi +0 -0
  242. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  243. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  244. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/modal_version/__main__.py +0 -0
  245. {modal-1.6.2.dev7 → modal-1.6.2.dev8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.2.dev7
3
+ Version: 1.6.2.dev8
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -378,7 +378,7 @@ def retry(
378
378
 
379
379
  Usage:
380
380
 
381
- ```
381
+ ```python notest
382
382
  @retry
383
383
  async def may_fail_default():
384
384
  # ...
@@ -907,8 +907,10 @@ async def asyncnullcontext(*args, **kwargs):
907
907
  Note that for Python 3.10+ you can use contextlib.nullcontext() instead.
908
908
 
909
909
  Usage:
910
+ ```python notest
910
911
  async with asyncnullcontext():
911
912
  pass
913
+ ```
912
914
  """
913
915
  yield
914
916
 
@@ -18,6 +18,7 @@ from typing import (
18
18
  )
19
19
  from urllib.parse import urlparse
20
20
 
21
+ from google.protobuf.timestamp_pb2 import Timestamp
21
22
  from typing_extensions import Self
22
23
 
23
24
  from modal_proto import api_pb2
@@ -451,6 +452,7 @@ class FileUploadSpec:
451
452
  size: int
452
453
  content: bytes | None = None # Set for very small files to avoid double-read
453
454
  exists_in_store: bool | None = None # set after existence check
455
+ checked_at: Timestamp | None = None # server time of the existence check, set alongside exists_in_store
454
456
 
455
457
  def read_content(self) -> bytes:
456
458
  """Read content from source."""
@@ -19,7 +19,7 @@ _SOURCE_OPTIONS = {
19
19
  }
20
20
 
21
21
 
22
- def _parse_time_arg(value: str | None, default: datetime, *, now: datetime | None = None) -> datetime:
22
+ def _parse_time_arg(value: str | None, default: datetime, *, now: datetime) -> datetime:
23
23
  """Parse a time argument that can be a relative duration (e.g. '2h', '30m') or ISO 8601 datetime.
24
24
 
25
25
  Naive datetime values are interpreted in the user's local timezone.
@@ -33,7 +33,7 @@ def _parse_time_arg(value: str | None, default: datetime, *, now: datetime | Non
33
33
  except ValueError:
34
34
  pass
35
35
  else:
36
- return (now or datetime.now(timezone.utc)) - duration
36
+ return now - duration
37
37
 
38
38
  try:
39
39
  dt = datetime.fromisoformat(value)
@@ -62,7 +62,7 @@ def _parse_fetch_range(since: str | None, until: str | None) -> tuple[datetime,
62
62
  try:
63
63
  duration = parse_duration(value)
64
64
  except ValueError:
65
- result = _parse_time_arg(value, default=default)
65
+ result = _parse_time_arg(value, default=default, now=now)
66
66
  else:
67
67
  result = now - duration
68
68
  if result.astimezone(timezone.utc).microsecond:
@@ -124,8 +124,8 @@ async def _run_logs_command(
124
124
  return
125
125
 
126
126
  now = datetime.now(timezone.utc)
127
- since_dt = _parse_time_arg(since, default=now) if since else None
128
- until_dt = _parse_time_arg(until, default=now) if until else None
127
+ since_dt = _parse_time_arg(since, default=now, now=now) if since else None
128
+ until_dt = _parse_time_arg(until, default=now, now=now) if until else None
129
129
  if since_dt is not None:
130
130
  effective_until = until_dt or now
131
131
  if since_dt >= effective_until:
@@ -248,8 +248,8 @@ async def logs(
248
248
  since_dt = default_since_dt
249
249
  until_dt = default_until_dt
250
250
  else:
251
- since_dt = _parse_time_arg(since, default=default_since_dt)
252
- until_dt = _parse_time_arg(until, default=default_until_dt)
251
+ since_dt = _parse_time_arg(since, default=default_since_dt, now=now)
252
+ until_dt = _parse_time_arg(until, default=default_until_dt, now=now)
253
253
 
254
254
  if since is not None and until is not None and since_dt >= until_dt:
255
255
  # User provided both --since and --until, but did so incorrectly
@@ -7,6 +7,7 @@ from typing import Any, AsyncGenerator, Sequence, cast
7
7
 
8
8
  import click
9
9
  from click import UsageError
10
+ from rich.box import SIMPLE
10
11
  from rich.table import Column, Table
11
12
  from rich.text import Text
12
13
 
@@ -16,7 +17,7 @@ from modal._functions import _Function
16
17
  from modal._logs import _fetch_time_range, _seconds_to_timestamp, _timestamp_to_seconds
17
18
  from modal._object import _get_environment_name
18
19
  from modal._utils.async_utils import async_map_ordered, synchronizer
19
- from modal._utils.time_utils import parse_duration
20
+ from modal._utils.time_utils import parse_duration, timestamp_to_localized_str
20
21
  from modal.client import _Client
21
22
  from modal.exception import LogsFetchError, NotFoundError
22
23
  from modal.output import OutputManager
@@ -50,10 +51,12 @@ from .utils import (
50
51
  env_option,
51
52
  grouped_utc_timestamp,
52
53
  humanize_filesize,
54
+ parse_int,
53
55
  )
54
56
 
55
57
  function_cli = ModalGroup(name="function", help="Inspect Modal Functions.")
56
58
 
59
+
57
60
  _INPUT_METRIC_ORDER = ("Execution time (s)", "End-to-end latency (s)")
58
61
  _CONTAINER_METRIC_ORDER = (
59
62
  "Startup time (s)",
@@ -705,8 +708,8 @@ async def stats(
705
708
  ```
706
709
  """
707
710
  now = datetime.now(timezone.utc)
708
- until_dt = _parse_time_arg(until, default=now)
709
- since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW)
711
+ until_dt = _parse_time_arg(until, default=now, now=now)
712
+ since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW, now=now)
710
713
  if since_dt >= until_dt:
711
714
  raise UsageError("--since must be before --until.")
712
715
 
@@ -1288,6 +1291,241 @@ async def variants(
1288
1291
  )
1289
1292
 
1290
1293
 
1294
+ autoscaler_cli = ModalGroup(name="autoscaler", help="View and Configure Autoscaler settings for Modal Functions.")
1295
+ function_cli.add_command(autoscaler_cli)
1296
+
1297
+
1298
+ @autoscaler_cli.command("settings", no_args_is_help=True)
1299
+ @click.argument("function_identifier", metavar="FUNCTION")
1300
+ @click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
1301
+ @env_option
1302
+ @synchronizer.create_blocking
1303
+ async def autoscaler_info(function_identifier: str, json: bool = False, env: str | None = None):
1304
+ """Show the current Autsocaling settings for the provided Function.
1305
+
1306
+ In addition to the current settings, this command also shows any active overrides to these
1307
+ settings, along with metadata about who overrode the setting, and when. FUNCTION may be a
1308
+ Function ID or a deployed Function name in the form ``APP_NAME/FUNCTION_NAME``.
1309
+
1310
+ If a setting's value is the default and not overridden, its value is omitted in the output.
1311
+
1312
+ Examples:
1313
+
1314
+ Show autoscaling settings for a deployed Function:
1315
+
1316
+ ```
1317
+ modal function autoscaler settings my-app/my-function
1318
+ ```
1319
+
1320
+ Show autoscaling settings using a Function ID:
1321
+
1322
+ ```
1323
+ modal function autoscaler settings fu-1234
1324
+ ```
1325
+ """
1326
+
1327
+ environment_name = _get_environment_name(ensure_env(env))
1328
+ client = await _Client.from_env()
1329
+ function_id, handle_metadata, _ = await _resolve_function_id(
1330
+ client,
1331
+ function_identifier,
1332
+ environment_name,
1333
+ object_type="Function",
1334
+ command="autoscaler settings",
1335
+ )
1336
+
1337
+ if not handle_metadata.base_function_id and len(handle_metadata.class_parameter_info.schema) > 0:
1338
+ raise ValueError(
1339
+ f"`{function_identifier}` is a modal.Cls with parameters. Provide a specific "
1340
+ "parameterization via a Function ID. See `modal function variants`."
1341
+ )
1342
+
1343
+ autoscaler_response = await client._stub.FunctionGetSchedulingParams(
1344
+ api_pb2.FunctionGetSchedulingParamsRequest(function_id=function_id)
1345
+ )
1346
+
1347
+ autoscaler_settings = FunctionAutoscalerSettings._from_proto(autoscaler_response.autoscaler_configuration.settings)
1348
+ override_events = autoscaler_response.autoscaler_configuration.override_events
1349
+
1350
+ output_manager = OutputManager.get()
1351
+
1352
+ if json:
1353
+ current_settings = dataclasses.asdict(autoscaler_settings)
1354
+
1355
+ override_events_json: list[dict[str, str]] = []
1356
+ for k, v in override_events.items():
1357
+ if v.user_id:
1358
+ obj = {
1359
+ "setting": k,
1360
+ "updated_at": str(v.timestamp),
1361
+ "actor_name": v.requested_by,
1362
+ "actor_id": v.user_id,
1363
+ "actor_type": "user",
1364
+ }
1365
+ elif v.service_user_id:
1366
+ obj = {
1367
+ "setting": k,
1368
+ "updated_at": str(v.timestamp),
1369
+ "actor_name": v.requested_by,
1370
+ "actor_id": v.service_user_id,
1371
+ "actor_type": "service-user",
1372
+ }
1373
+ else:
1374
+ obj = {
1375
+ "setting": k,
1376
+ "updated_at": str(v.timestamp),
1377
+ "actor_id": "unknown",
1378
+ "actor_name": "unknown",
1379
+ "actor_type": "unknown",
1380
+ }
1381
+
1382
+ override_events_json.append(obj)
1383
+
1384
+ output_manager.print_json(
1385
+ json_lib.dumps(
1386
+ {
1387
+ "current_settings": current_settings,
1388
+ "override_events": override_events_json,
1389
+ }
1390
+ )
1391
+ )
1392
+
1393
+ return
1394
+
1395
+ human_readable = {
1396
+ "min_containers": "Min Containers",
1397
+ "max_containers": "Max Containers",
1398
+ "buffer_containers": "Buffer Containers",
1399
+ "scaledown_window": "Scaledown Window",
1400
+ }
1401
+
1402
+ if override_events:
1403
+ t = Table("Setting", "Value", "Overridden By", box=SIMPLE)
1404
+ else:
1405
+ t = Table("Setting", "Value", box=SIMPLE)
1406
+
1407
+ for key, pretty in human_readable.items():
1408
+ value = getattr(autoscaler_settings, key)
1409
+ value_str = str(value)
1410
+
1411
+ override_meta = "-"
1412
+ if key in override_events:
1413
+ override = override_events[key]
1414
+ override_meta = f"{override.requested_by} at {timestamp_to_localized_str(override.timestamp, isotz=False)}"
1415
+
1416
+ if key == "scaledown_window":
1417
+ value_str = f"{value} seconds"
1418
+
1419
+ if key not in override_events and not value:
1420
+ value_str = "-"
1421
+
1422
+ if override_events:
1423
+ t.add_row(pretty, value_str, override_meta)
1424
+ else:
1425
+ t.add_row(pretty, value_str)
1426
+
1427
+ output_manager.print(t)
1428
+
1429
+
1430
+ @autoscaler_cli.command("update", no_args_is_help=True)
1431
+ @click.argument("function_identifier", metavar="FUNCTION")
1432
+ @click.option("--max-containers", "max_containers", default=None, help="Maximum concurrent containers.")
1433
+ @click.option("--min-containers", "min_containers", default=None, help="Minimum number of containers to keep running.")
1434
+ @click.option(
1435
+ "--buffer-containers",
1436
+ "buffer_containers",
1437
+ default=None,
1438
+ help="Extra containers to keep warm beyond current demand.",
1439
+ )
1440
+ @click.option(
1441
+ "--scaledown-window",
1442
+ "scaledown_window",
1443
+ default=None,
1444
+ help="Maximum duration (in seconds) idle containers wait before scaling down.",
1445
+ )
1446
+ @env_option
1447
+ @synchronizer.create_blocking
1448
+ async def autoscaler_update(
1449
+ function_identifier: str,
1450
+ max_containers: str | None = None,
1451
+ min_containers: str | None = None,
1452
+ buffer_containers: str | None = None,
1453
+ scaledown_window: str | None = None,
1454
+ env: str | None = None,
1455
+ ):
1456
+ """Update the current Autsocaling settings for the provided Function.
1457
+
1458
+ FUNCTION may be a Function ID or a deployed Function name in the form ``APP_NAME/FUNCTION_NAME``.
1459
+
1460
+ Settings can be modified using the `--max-containers`, `--min-containers`, `--buffer-containers`,
1461
+ and `--scaledown-window` options.
1462
+
1463
+ If provided, Max Containers should be a positive integer, Min/Buffer Containers should be
1464
+ nonnegative integers, and Scaledown window should be a positive integer measured in seconds.
1465
+
1466
+ Examples:
1467
+
1468
+ Update Max Containers for a deployed Function:
1469
+
1470
+ ```
1471
+ modal function autoscaler update my-app/my-function --max-containers 10
1472
+ ```
1473
+
1474
+ Update both Min and Buffer Containers for a Function by ID:
1475
+
1476
+ ```
1477
+ modal function autoscaler update fu-1234 --min-containers 3 --buffer-containers 2
1478
+ ```
1479
+ """
1480
+
1481
+ environment_name = _get_environment_name(ensure_env(env))
1482
+ client = await _Client.from_env()
1483
+
1484
+ updates: dict[str, int] = {}
1485
+ for k, v in {
1486
+ "min_containers": min_containers,
1487
+ "max_containers": max_containers,
1488
+ "buffer_containers": buffer_containers,
1489
+ "scaledown_window": scaledown_window,
1490
+ }.items():
1491
+ if v is None:
1492
+ continue
1493
+
1494
+ try:
1495
+ updates[k] = parse_int(
1496
+ v,
1497
+ allow_negative=False,
1498
+ allow_zero=k in {"min_containers", "buffer_containers"},
1499
+ )
1500
+ except ValueError as e:
1501
+ option_name = f"--{k.replace('_', '-')}"
1502
+ raise click.UsageError(f"Invalid input for {option_name!r}: {e}")
1503
+
1504
+ output_manager = OutputManager.get()
1505
+ if not updates:
1506
+ output_manager.print("No autoscaler settings specified.")
1507
+ return
1508
+
1509
+ function_id, handle_metadata, _ = await _resolve_function_id(
1510
+ client,
1511
+ function_identifier,
1512
+ environment_name,
1513
+ object_type="Function",
1514
+ command="autoscaler update",
1515
+ )
1516
+
1517
+ if not handle_metadata.base_function_id and len(handle_metadata.class_parameter_info.schema) > 0:
1518
+ raise ValueError(
1519
+ f"`{function_identifier}` is a modal.Cls with parameters. Provide a specific "
1520
+ "parameterization via a Function ID. See `modal function variants`."
1521
+ )
1522
+
1523
+ f: _Function = _Function._new_hydrated(function_id, client, handle_metadata)
1524
+ await f.update_autoscaler(**updates)
1525
+
1526
+ output_manager.print(f"[green]✓[/green] Updated autoscaler settings for Function {handle_metadata.function_name}")
1527
+
1528
+
1291
1529
  @function_cli.command("metrics", no_args_is_help=True)
1292
1530
  @click.argument("identifier", metavar="FUNCTION")
1293
1531
  @click.option("--since", help="Start time: ISO 8601 or relative duration (e.g. 2h). Naive times are local.")
@@ -9,6 +9,7 @@ from typing import AsyncGenerator, Sequence, cast
9
9
 
10
10
  import click
11
11
  from click import UsageError
12
+ from rich.box import SIMPLE
12
13
  from rich.table import Column, Table
13
14
  from rich.text import Text
14
15
 
@@ -17,7 +18,7 @@ from modal._logs import _fetch_time_range, _seconds_to_timestamp, _timestamp_to_
17
18
  from modal._object import _get_environment_name
18
19
  from modal._server import _Server
19
20
  from modal._utils.async_utils import async_map_ordered, synchronizer
20
- from modal._utils.time_utils import parse_duration
21
+ from modal._utils.time_utils import parse_duration, timestamp_to_localized_str
21
22
  from modal.cli.utils import humanize_filesize
22
23
  from modal.client import _Client
23
24
  from modal.exception import LogsFetchError, NotFoundError
@@ -51,6 +52,7 @@ from .utils import (
51
52
  display_table,
52
53
  env_option,
53
54
  grouped_utc_timestamp,
55
+ parse_int,
54
56
  )
55
57
 
56
58
  server_cli = ModalGroup(name="server", help="Inspect Modal Servers.")
@@ -671,8 +673,8 @@ def _render_inference(history: api_pb2.ServerGetTimeRangeStatsResponse, use_colo
671
673
 
672
674
  def _server_stats_time_range(since: str | None, until: str | None) -> tuple[datetime, datetime, timedelta]:
673
675
  now = datetime.now(timezone.utc)
674
- until_dt = _parse_time_arg(until, default=now)
675
- since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW)
676
+ until_dt = _parse_time_arg(until, default=now, now=now)
677
+ since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW, now=now)
676
678
  if since_dt >= until_dt:
677
679
  raise UsageError("--since must be before --until.")
678
680
 
@@ -1086,6 +1088,281 @@ async def requests(
1086
1088
  )
1087
1089
 
1088
1090
 
1091
+ autoscaler_cli = ModalGroup(name="autoscaler", help="View and Configure Autoscaler settings for Modal Servers.")
1092
+ server_cli.add_command(autoscaler_cli)
1093
+
1094
+
1095
+ @autoscaler_cli.command("settings", no_args_is_help=True)
1096
+ @click.argument("server_identifier", metavar="FUNCTION")
1097
+ @click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
1098
+ @env_option
1099
+ @synchronizer.create_blocking
1100
+ async def autoscaler_info(server_identifier: str, json: bool = False, env: str | None = None):
1101
+ """Show the current Autsocaling settings for the provided Server.
1102
+
1103
+ In addition to the current settings, this command also shows any active overrides to these
1104
+ settings, along with metadata about who overrode the setting, and when. SERVER may be a
1105
+ Server ID or a deployed Server name in the form ``APP_NAME/SERVER_NAME``.
1106
+
1107
+ If a setting's value is the default and not overridden, its value is omitted in the output.
1108
+
1109
+ Examples:
1110
+
1111
+ Show autoscaling settings for a deployed Server:
1112
+
1113
+ ```
1114
+ modal server autoscaler settings my-app/my-server
1115
+ ```
1116
+
1117
+ Show autoscaling settings using an ID:
1118
+
1119
+ ```
1120
+ modal server autoscaler settings fu-1234
1121
+ ```
1122
+ """
1123
+
1124
+ environment_name = _get_environment_name(ensure_env(env))
1125
+ client = await _Client.from_env()
1126
+ function_id, _, _ = await _resolve_function_id(
1127
+ client,
1128
+ server_identifier,
1129
+ environment_name,
1130
+ object_type="Server",
1131
+ command="autoscaler settings",
1132
+ )
1133
+
1134
+ autoscaler_response = await client._stub.FunctionGetSchedulingParams(
1135
+ api_pb2.FunctionGetSchedulingParamsRequest(function_id=function_id)
1136
+ )
1137
+
1138
+ autoscaler_settings = ServerAutoscalerSettings._from_proto(autoscaler_response.autoscaler_configuration.settings)
1139
+ override_events = autoscaler_response.autoscaler_configuration.override_events
1140
+
1141
+ output_manager = OutputManager.get()
1142
+
1143
+ if json:
1144
+ current_settings = dataclasses.asdict(autoscaler_settings)
1145
+ if current_settings["target_concurrency"] == 0:
1146
+ current_settings["target_concurrency"] = None
1147
+
1148
+ override_events_json: list[dict[str, str]] = []
1149
+ for k, v in override_events.items():
1150
+ if k == "target_concurrency_float":
1151
+ continue
1152
+
1153
+ if v.user_id:
1154
+ obj = {
1155
+ "setting": k,
1156
+ "updated_at": str(v.timestamp),
1157
+ "actor_name": v.requested_by,
1158
+ "actor_id": v.user_id,
1159
+ "actor_type": "user",
1160
+ }
1161
+ elif v.service_user_id:
1162
+ obj = {
1163
+ "setting": k,
1164
+ "updated_at": str(v.timestamp),
1165
+ "actor_name": v.requested_by,
1166
+ "actor_id": v.service_user_id,
1167
+ "actor_type": "service-user",
1168
+ }
1169
+ else:
1170
+ obj = {
1171
+ "setting": k,
1172
+ "updated_at": str(v.timestamp),
1173
+ "actor_id": "unknown",
1174
+ "actor_name": "unknown",
1175
+ "actor_type": "unknown",
1176
+ }
1177
+
1178
+ override_events_json.append(obj)
1179
+
1180
+ output_manager.print_json(
1181
+ json_lib.dumps(
1182
+ {
1183
+ "current_settings": current_settings,
1184
+ "override_events": override_events_json,
1185
+ }
1186
+ )
1187
+ )
1188
+
1189
+ return
1190
+
1191
+ human_readable = {
1192
+ "min_containers": "Min Containers",
1193
+ "max_containers": "Max Containers",
1194
+ "buffer_containers": "Buffer Containers",
1195
+ "scaledown_window": "Scaledown Window",
1196
+ "scaleup_window": "Scaleup Window",
1197
+ "target_concurrency": "Target Concurrency",
1198
+ }
1199
+
1200
+ if override_events:
1201
+ t = Table("Setting", "Value", "Overridden By", box=SIMPLE)
1202
+ else:
1203
+ t = Table("Setting", "Value", box=SIMPLE)
1204
+
1205
+ for key, pretty in human_readable.items():
1206
+ value = getattr(autoscaler_settings, key)
1207
+ value_str = str(value)
1208
+
1209
+ override_meta = "-"
1210
+ if key in override_events:
1211
+ override = override_events[key]
1212
+ override_meta = f"{override.requested_by} at {timestamp_to_localized_str(override.timestamp, isotz=False)}"
1213
+
1214
+ if key in {"scaledown_window", "scaleup_window"}:
1215
+ value_str = f"{value} seconds"
1216
+
1217
+ if key == "target_concurrency":
1218
+ value_str = f"{value} requests/container"
1219
+
1220
+ if not value and (key == "target_concurrency" or key not in override_events):
1221
+ value_str = "-"
1222
+
1223
+ if override_events:
1224
+ t.add_row(pretty, value_str, override_meta)
1225
+ else:
1226
+ t.add_row(pretty, value_str)
1227
+
1228
+ output_manager.print(t)
1229
+
1230
+
1231
+ @autoscaler_cli.command("update", no_args_is_help=True)
1232
+ @click.argument("server_identifier", metavar="SERVER")
1233
+ @click.option("--max-containers", "max_containers", default=None, help="Maximum concurrent containers.")
1234
+ @click.option("--min-containers", "min_containers", default=None, help="Minimum number of containers to keep running.")
1235
+ @click.option(
1236
+ "--buffer-containers",
1237
+ "buffer_containers",
1238
+ default=None,
1239
+ help="Extra containers to keep warm beyond current demand.",
1240
+ )
1241
+ @click.option(
1242
+ "--scaledown-window",
1243
+ "scaledown_window",
1244
+ default=None,
1245
+ help="Maximum duration (in seconds) idle containers wait before scaling down.",
1246
+ )
1247
+ @click.option(
1248
+ "--scaleup-window",
1249
+ "scaleup_window",
1250
+ default=None,
1251
+ help="Seconds of sustained demand required before scaling up new containers. ",
1252
+ )
1253
+ @click.option(
1254
+ "--target-concurrency",
1255
+ "target_concurrency",
1256
+ default=None,
1257
+ help="Target number of concurrent requests per container. May be fractional.",
1258
+ )
1259
+ @env_option
1260
+ @synchronizer.create_blocking
1261
+ async def autoscaler_update(
1262
+ server_identifier: str,
1263
+ max_containers: str | None = None,
1264
+ min_containers: str | None = None,
1265
+ buffer_containers: str | None = None,
1266
+ scaledown_window: str | None = None,
1267
+ scaleup_window: str | None = None,
1268
+ target_concurrency: str | None = None,
1269
+ env: str | None = None,
1270
+ ):
1271
+ """Update the current Autsocaling settings for the provided Server.
1272
+
1273
+ SERVER may be a Server ID or a deployed Server name in the form ``APP_NAME/SERVER_NAME``.
1274
+
1275
+ Settings can be modified using the `--max-containers`, `--min-containers`, `--buffer-containers`,
1276
+ `--scaledown-window`, `--scaleup-window`, and `--target-concurrency` options.
1277
+
1278
+ If provided, Max Containers should be a positive integer, Min and Buffer Containers should be
1279
+ nonnegative integers, Scaledown and Scaleup windows should be positive integers measured in seconds,
1280
+ and Target Concurrency should either be a positive floating point number, or the string "none".
1281
+
1282
+ Examples:
1283
+
1284
+ Update Max Containers for a deployed Server:
1285
+
1286
+ ```
1287
+ modal server autoscaler update my-app/my-server --max-containers 10
1288
+ ```
1289
+
1290
+ Update both Min Containers and Target Concurrency for a Server by ID:
1291
+
1292
+ ```
1293
+ modal server autoscaler update fu-1234 --min-containers 3 --target-concurrency 2.5
1294
+ ```
1295
+
1296
+ Set Target Concurrency to "none" (disabling autoscaling for the Server)
1297
+
1298
+ ```
1299
+ modal server autoscaler update my-app/my-server --target-concurrency none
1300
+ ```
1301
+ """
1302
+
1303
+ environment_name = _get_environment_name(ensure_env(env))
1304
+ client = await _Client.from_env()
1305
+
1306
+ updates: dict[str, int | float] = {}
1307
+ for k, v in {
1308
+ "min_containers": min_containers,
1309
+ "max_containers": max_containers,
1310
+ "buffer_containers": buffer_containers,
1311
+ "scaledown_window": scaledown_window,
1312
+ "scaleup_window": scaleup_window,
1313
+ "target_concurrency": target_concurrency,
1314
+ }.items():
1315
+ if v is None:
1316
+ continue
1317
+
1318
+ if k == "target_concurrency":
1319
+ error_message = (
1320
+ f"Invalid input for '--target-concurrency': {v!r} must be a positive float or the string \"none\""
1321
+ )
1322
+
1323
+ if v == "none":
1324
+ updates[k] = 0
1325
+ continue
1326
+
1327
+ try:
1328
+ updates[k] = float(v)
1329
+ except ValueError as e:
1330
+ raise click.UsageError(error_message) from e
1331
+
1332
+ if updates[k] <= 0:
1333
+ raise click.UsageError(error_message)
1334
+
1335
+ continue
1336
+
1337
+ try:
1338
+ updates[k] = parse_int(
1339
+ v,
1340
+ allow_negative=False,
1341
+ allow_zero=k in {"min_containers", "buffer_containers"},
1342
+ )
1343
+ except ValueError as e:
1344
+ option_name = f"--{k.replace('_', '-')}"
1345
+ raise click.UsageError(f"Invalid input for {option_name!r}: {e}")
1346
+
1347
+ output_manager = OutputManager.get()
1348
+ if not updates:
1349
+ output_manager.print("No autoscaler settings specified.")
1350
+ return
1351
+
1352
+ function_id, handle_metadata, _ = await _resolve_function_id(
1353
+ client,
1354
+ server_identifier,
1355
+ environment_name,
1356
+ object_type="Server",
1357
+ command="autoscaler update",
1358
+ )
1359
+
1360
+ s = _Server._new_from_function(function_id, client, handle_metadata)
1361
+ await s.update_autoscaler(**updates) # type: ignore
1362
+
1363
+ output_manager.print(f"[green]✓[/green] Updated autoscaler settings for Server {handle_metadata.function_name}")
1364
+
1365
+
1089
1366
  @server_cli.command("metrics", no_args_is_help=True)
1090
1367
  @click.argument("identifier", metavar="SERVER")
1091
1368
  @click.option("--since", help="Start time: ISO 8601 or relative duration (e.g. 2h). Naive times are local.")