modal 1.5.4.dev20__tar.gz → 1.5.4.dev22__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 (236) hide show
  1. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/PKG-INFO +1 -1
  2. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_clustered_functions.py +8 -5
  3. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_functions.py +19 -4
  4. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/execution_context.py +6 -0
  5. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/execution_context.pyi +1 -0
  6. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_server.py +7 -2
  7. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/function_utils.py +23 -0
  8. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/task_command_router_client.py +1 -0
  9. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/time_utils.py +4 -1
  10. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_workspace.py +83 -1
  11. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/app.py +6 -4
  12. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/app.pyi +8 -4
  13. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/billing.py +30 -0
  14. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/client.py +1 -0
  15. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/client.pyi +4 -2
  16. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/config.py +1 -0
  17. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/flash.py +15 -9
  18. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/flash.pyi +6 -2
  19. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/functions.pyi +3 -3
  20. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/runner.py +6 -6
  21. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/runner.pyi +5 -3
  22. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox.py +101 -67
  23. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox.pyi +21 -9
  24. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/server.pyi +14 -4
  25. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/token_flow.py +7 -5
  26. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/types.py +9 -2
  27. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/workspace.pyi +20 -0
  28. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/PKG-INFO +1 -1
  29. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2.py +146 -120
  30. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2.pyi +42 -1
  31. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2.py +92 -92
  32. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2.pyi +10 -2
  33. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_version/__init__.py +1 -1
  34. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/pyproject.toml +4 -10
  35. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/LICENSE +0 -0
  36. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/README.md +0 -0
  37. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/__init__.py +0 -0
  38. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/__main__.py +0 -0
  39. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_billing.py +0 -0
  40. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_clustered_functions.pyi +0 -0
  41. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_container_entrypoint.py +0 -0
  42. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_environments.py +0 -0
  43. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_function_variants.py +0 -0
  44. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_grpc_client.py +0 -0
  45. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_image.py +0 -0
  46. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_ipython.py +0 -0
  47. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_load_context.py +0 -0
  48. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_location.py +0 -0
  49. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs.py +0 -0
  50. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs_manager.py +0 -0
  51. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_logs_manager.pyi +0 -0
  52. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_object.py +0 -0
  53. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/__init__.py +0 -0
  54. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/manager.py +0 -0
  55. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/pty.py +0 -0
  56. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/rich.py +0 -0
  57. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_output/status.py +0 -0
  58. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_partial_function.py +0 -0
  59. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_resolver.py +0 -0
  60. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_resources.py +0 -0
  61. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/__init__.py +0 -0
  62. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/asgi.py +0 -0
  63. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/container_io_manager.py +0 -0
  64. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
  65. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  66. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
  67. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  68. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/telemetry.py +0 -0
  69. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
  70. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_runtime/user_code_imports.py +0 -0
  71. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_serialization.py +0 -0
  72. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_supports_logs.py +0 -0
  73. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_traceback.py +0 -0
  74. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_tunnel.py +0 -0
  75. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_tunnel.pyi +0 -0
  76. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_type_manager.py +0 -0
  77. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/__init__.py +0 -0
  78. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/app_utils.py +0 -0
  79. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/async_utils.py +0 -0
  80. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/auth_token_manager.py +0 -0
  81. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/blob_utils.py +0 -0
  82. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/browser_utils.py +0 -0
  83. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
  84. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/curl_utils.py +0 -0
  85. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/deprecation.py +0 -0
  86. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/display_utils.py +0 -0
  87. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/docker_utils.py +0 -0
  88. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/git_utils.py +0 -0
  89. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/grpc_testing.py +0 -0
  90. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/grpc_utils.py +0 -0
  91. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/hash_utils.py +0 -0
  92. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/http_utils.py +0 -0
  93. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/jwt_utils.py +0 -0
  94. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/logger.py +0 -0
  95. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/mount_utils.py +0 -0
  96. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/name_utils.py +0 -0
  97. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/package_utils.py +0 -0
  98. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/pattern_utils.py +0 -0
  99. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/proxy_support.py +0 -0
  100. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/rand_pb_testing.py +0 -0
  101. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
  102. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_utils/shell_utils.py +0 -0
  103. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/__init__.py +0 -0
  104. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  105. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/cloudpickle.py +0 -0
  106. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/tblib.py +0 -0
  107. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_vendor/version.py +0 -0
  108. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/_watcher.py +0 -0
  109. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/billing.py +0 -0
  110. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/billing.pyi +0 -0
  111. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2023.12.312.txt +0 -0
  112. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2023.12.txt +0 -0
  113. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2024.04.txt +0 -0
  114. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2024.10.txt +0 -0
  115. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/2025.06.txt +0 -0
  116. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/PREVIEW.txt +0 -0
  117. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/README.md +0 -0
  118. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/builder/base-images.json +0 -0
  119. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/call_graph.py +0 -0
  120. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/__init__.py +0 -0
  121. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_download.py +0 -0
  122. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_help.py +0 -0
  123. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/_traceback.py +0 -0
  124. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/app.py +0 -0
  125. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/bootstrap.py +0 -0
  126. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/changelog.py +0 -0
  127. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/cluster.py +0 -0
  128. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/config.py +0 -0
  129. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/container.py +0 -0
  130. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/curl.py +0 -0
  131. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/dashboard.py +0 -0
  132. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/dict.py +0 -0
  133. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/endpoint.py +0 -0
  134. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/entry_point.py +0 -0
  135. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/environment.py +0 -0
  136. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/image.py +0 -0
  137. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/import_refs.py +0 -0
  138. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/launch.py +0 -0
  139. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/logo.py +0 -0
  140. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/network_file_system.py +0 -0
  141. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/profile.py +0 -0
  142. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/__init__.py +0 -0
  143. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/run_jupyter.py +0 -0
  144. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/programs/vscode.py +0 -0
  145. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/queues.py +0 -0
  146. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/run.py +0 -0
  147. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/secret.py +0 -0
  148. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/selector.py +0 -0
  149. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/shell.py +0 -0
  150. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/skills.py +0 -0
  151. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/token.py +0 -0
  152. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/utils.py +0 -0
  153. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/volume.py +0 -0
  154. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cli/workspace.py +0 -0
  155. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cloud_bucket_mount.py +0 -0
  156. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cloud_bucket_mount.pyi +0 -0
  157. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cls.py +0 -0
  158. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/cls.pyi +0 -0
  159. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/container_process.py +0 -0
  160. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/container_process.pyi +0 -0
  161. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/dict.py +0 -0
  162. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/dict.pyi +0 -0
  163. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/environments.py +0 -0
  164. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/environments.pyi +0 -0
  165. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/exception.py +0 -0
  166. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/__init__.py +0 -0
  167. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/experimental/ipython.py +0 -0
  168. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_io.py +0 -0
  169. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_io.pyi +0 -0
  170. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/file_pattern_matcher.py +0 -0
  171. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/functions.py +0 -0
  172. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/image.py +0 -0
  173. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/image.pyi +0 -0
  174. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/io_streams.py +0 -0
  175. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/io_streams.pyi +0 -0
  176. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/mount.py +0 -0
  177. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/mount.pyi +0 -0
  178. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/network_file_system.py +0 -0
  179. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/network_file_system.pyi +0 -0
  180. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/object.py +0 -0
  181. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/object.pyi +0 -0
  182. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/output.py +0 -0
  183. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/parallel_map.py +0 -0
  184. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/parallel_map.pyi +0 -0
  185. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/partial_function.py +0 -0
  186. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/partial_function.pyi +0 -0
  187. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/proxy.py +0 -0
  188. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/proxy.pyi +0 -0
  189. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/py.typed +0 -0
  190. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/queue.py +0 -0
  191. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/queue.pyi +0 -0
  192. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/retries.py +0 -0
  193. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/running_app.py +0 -0
  194. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox_fs.py +0 -0
  195. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/sandbox_fs.pyi +0 -0
  196. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/schedule.py +0 -0
  197. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/scheduler_placement.py +0 -0
  198. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/secret.py +0 -0
  199. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/secret.pyi +0 -0
  200. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/server.py +0 -0
  201. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/serving.py +0 -0
  202. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/serving.pyi +0 -0
  203. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/skills/modal/SKILL.md +0 -0
  204. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/snapshot.py +0 -0
  205. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/snapshot.pyi +0 -0
  206. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/stream_type.py +0 -0
  207. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/token_flow.pyi +0 -0
  208. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/volume.py +0 -0
  209. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/volume.pyi +0 -0
  210. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal/workspace.py +0 -0
  211. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/SOURCES.txt +0 -0
  212. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/dependency_links.txt +0 -0
  213. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/entry_points.txt +0 -0
  214. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/requires.txt +0 -0
  215. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal.egg-info/top_level.txt +0 -0
  216. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/__init__.py +0 -0
  217. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_cli_docs.py +0 -0
  218. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
  219. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_reference_docs.py +0 -0
  220. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
  221. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/__init__.py +0 -0
  222. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/mdmd.py +0 -0
  223. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/signatures.py +0 -0
  224. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/mdmd/types.py +0 -0
  225. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_docs/reference_intro.md +0 -0
  226. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/__init__.py +0 -0
  227. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_grpc.py +0 -0
  228. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2_grpc.py +0 -0
  229. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/api_pb2_grpc.pyi +0 -0
  230. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/modal_api_grpc.py +0 -0
  231. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/py.typed +0 -0
  232. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_grpc.py +0 -0
  233. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  234. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  235. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/modal_version/__main__.py +0 -0
  236. {modal-1.5.4.dev20 → modal-1.5.4.dev22}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.4.dev20
3
+ Version: 1.5.4.dev22
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -2,6 +2,7 @@
2
2
  import os
3
3
  import socket
4
4
  from dataclasses import dataclass
5
+ from typing import cast
5
6
 
6
7
  from modal._utils.async_utils import synchronize_api
7
8
  from modal.client import _Client
@@ -41,9 +42,11 @@ def get_fabric_peers() -> list[int]:
41
42
  async def _initialize_clustered_function(client: _Client, task_id: str, world_size: int):
42
43
  global cluster_info
43
44
 
44
- def get_i6pn():
45
+ def get_i6pn() -> str:
45
46
  """Returns the ipv6 address assigned to this container."""
46
- return socket.getaddrinfo("i6pn.modal.local", None, socket.AF_INET6)[0][4][0]
47
+ # An AF_INET6 result always carries a (host, port, flowinfo, scope_id) sockaddr,
48
+ # but getaddrinfo is typed for every address family, some of which lead with an int
49
+ return cast(str, socket.getaddrinfo("i6pn.modal.local", None, socket.AF_INET6)[0][4][0])
47
50
 
48
51
  hostname = socket.gethostname()
49
52
  container_ip = get_i6pn()
@@ -77,9 +80,9 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
77
80
  cluster_info = ClusterInfo(
78
81
  rank=resp.cluster_rank,
79
82
  cluster_id=resp.cluster_id,
80
- container_ips=resp.container_ips,
81
- container_ipv4_ips=resp.container_ipv4_ips,
82
- fabric_ids=resp.fabric_ids,
83
+ container_ips=list(resp.container_ips),
84
+ container_ipv4_ips=list(resp.container_ipv4_ips),
85
+ fabric_ids=list(resp.fabric_ids),
83
86
  )
84
87
  else:
85
88
  cluster_info = ClusterInfo(
@@ -56,7 +56,9 @@ from ._utils.function_utils import (
56
56
  _stream_function_call_data,
57
57
  get_function_type,
58
58
  is_async,
59
+ normalize_fractional_target_concurrency,
59
60
  parse_gpu_config,
61
+ validate_target_concurrency,
60
62
  )
61
63
  from ._utils.grpc_utils import Retry, RetryWarningMessage
62
64
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes, validate_volumes_by_object_id
@@ -681,7 +683,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
681
683
  scaleup_window: int | None = None,
682
684
  scaledown_window: int | None = None,
683
685
  max_concurrent_inputs: int | None = None,
684
- target_concurrent_inputs: int | None = None,
686
+ target_concurrent_inputs: float | None = None,
685
687
  batch_max_size: int | None = None,
686
688
  batch_wait_ms: int | None = None,
687
689
  cloud: str | None = None,
@@ -789,6 +791,17 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
789
791
  scaledown_window=scaledown_window,
790
792
  )
791
793
 
794
+ target_concurrent_inputs_int = 0
795
+ if target_concurrent_inputs is not None:
796
+ if is_server:
797
+ validate_target_concurrency(target_concurrent_inputs, "target_inputs", allow_fractional=True)
798
+ autoscaler_settings.target_concurrency_float = normalize_fractional_target_concurrency(
799
+ target_concurrent_inputs
800
+ )
801
+ else:
802
+ validate_target_concurrency(target_concurrent_inputs, "target_inputs", allow_fractional=False)
803
+ target_concurrent_inputs_int = int(target_concurrent_inputs)
804
+
792
805
  # For clustered functions, container settings must be multiples of cluster_size
793
806
  if cluster_size is not None and cluster_size > 1:
794
807
  for field in ["min_containers", "max_containers", "buffer_containers"]:
@@ -1014,7 +1027,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1014
1027
  app_name=app_name,
1015
1028
  is_builder_function=is_builder_function,
1016
1029
  max_concurrent_inputs=max_concurrent_inputs or 0,
1017
- target_concurrent_inputs=target_concurrent_inputs or 0,
1030
+ target_concurrent_inputs=target_concurrent_inputs_int,
1018
1031
  batch_max_size=batch_max_size or 0,
1019
1032
  batch_linger_ms=batch_wait_ms or 0,
1020
1033
  worker_id=config.get("worker_id"),
@@ -1200,7 +1213,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1200
1213
  buffer_containers: int | None = None,
1201
1214
  scaleup_window: int | None = None,
1202
1215
  scaledown_window: int | None = None,
1203
- target_concurrency: int | None = None,
1216
+ target_concurrency: float | None = None,
1204
1217
  ) -> ServerAutoscalerSettings:
1205
1218
  settings = api_pb2.AutoscalerSettings(
1206
1219
  min_containers=min_containers,
@@ -1208,8 +1221,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1208
1221
  buffer_containers=buffer_containers,
1209
1222
  scaleup_window=scaleup_window,
1210
1223
  scaledown_window=scaledown_window,
1211
- target_concurrency=target_concurrency,
1212
1224
  )
1225
+ if target_concurrency is not None:
1226
+ validate_target_concurrency(target_concurrency, "target_concurrency", allow_fractional=True)
1227
+ settings.target_concurrency_float = normalize_fractional_target_concurrency(target_concurrency)
1213
1228
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1214
1229
  response = await self.client.stub.FunctionUpdateSchedulingParams(request)
1215
1230
 
@@ -120,3 +120,9 @@ def _import_context():
120
120
  yield
121
121
  finally:
122
122
  _is_currently_importing = False
123
+
124
+
125
+ def _in_import_context() -> bool:
126
+ # Accessor rather than direct attribute reads, since module-level variables
127
+ # are not exposed in the generated .pyi stub for this module
128
+ return _is_currently_importing
@@ -75,6 +75,7 @@ def _set_current_context_ids(
75
75
  input_ids: list[str], function_call_ids: list[str], attempt_tokens: list[str]
76
76
  ) -> collections.abc.Callable[[], None]: ...
77
77
  def _import_context(): ...
78
+ def _in_import_context() -> bool: ...
78
79
 
79
80
  _current_input_id: contextvars.ContextVar
80
81
 
@@ -124,7 +124,7 @@ class _Server:
124
124
  async def update_autoscaler(
125
125
  self,
126
126
  *,
127
- target_concurrency: int | None = None,
127
+ target_concurrency: float | None = None,
128
128
  min_containers: int | None = None,
129
129
  max_containers: int | None = None,
130
130
  buffer_containers: int | None = None,
@@ -140,7 +140,9 @@ class _Server:
140
140
  its static configuration.
141
141
 
142
142
  Args:
143
- target_concurrency: Target number of concurrent requests per container.
143
+ target_concurrency:
144
+ Target number of concurrent requests per container. May be fractional, e.g. 1.5 to
145
+ target three concurrent requests per two containers.
144
146
  min_containers: Minimum number of containers to keep running regardless of demand.
145
147
  max_containers: Limit on the number of containers that can be concurrently running.
146
148
  buffer_containers: Extra containers to scale up beyond current demand.
@@ -167,6 +169,9 @@ class _Server:
167
169
  # Adjust Server autoscaling to target 20 concurrent requests per replica
168
170
  server.update_autoscaler(target_concurrency=20)
169
171
 
172
+ # Target three concurrent requests for every two containers
173
+ server.update_autoscaler(target_concurrency=1.5)
174
+
170
175
  # Disable the Server autoscaling by setting target_concurrency to 0
171
176
  server.update_autoscaler(target_concurrency=0)
172
177
  ```
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
3
  import inspect
4
+ import math
4
5
  import os
5
6
  import typing
6
7
  from collections.abc import AsyncGenerator, Callable
@@ -81,6 +82,28 @@ def is_flash_object(experimental_options: dict[str, Any] | None, http_config: ap
81
82
  return bool(experimental_options and experimental_options.get("flash", False)) or http_config is not None
82
83
 
83
84
 
85
+ def validate_target_concurrency(value: float, param_name: str, allow_fractional: bool) -> None:
86
+ """Validate a user-supplied target concurrency.
87
+
88
+ Only `@app.server` may use a fractional Flash autoscaling target.
89
+ """
90
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
91
+ raise InvalidError(f"The `{param_name}` argument must be a number, not {type(value).__name__}.")
92
+ if not math.isfinite(value):
93
+ raise InvalidError(f"The `{param_name}` argument must be finite, not {value}.")
94
+ if value < 0:
95
+ raise InvalidError(f"The `{param_name}` argument must be non-negative.")
96
+ if not allow_fractional and not isinstance(value, int):
97
+ raise InvalidError(
98
+ f"The `{param_name}` argument must be an integer; fractional values are only supported for Servers."
99
+ )
100
+
101
+
102
+ def normalize_fractional_target_concurrency(value: float) -> float:
103
+ """Clamp (0, 1) up to 1 and round to the nearest hundredth for Flash targets."""
104
+ return 1.0 if 0 < value < 1 else round(float(value), 2)
105
+
106
+
84
107
  def is_method_fn(object_qual_name: str):
85
108
  # methods have names like Cls.foo.
86
109
  if "<locals>" in object_qual_name:
@@ -14,6 +14,7 @@ from contextlib import suppress
14
14
  from typing import BinaryIO, TypeVar
15
15
 
16
16
  import grpclib.client
17
+ import grpclib.events
17
18
  from grpclib import GRPCError, Status
18
19
  from grpclib.exceptions import StreamTerminatedError
19
20
 
@@ -8,7 +8,7 @@ def is_leap(year: int):
8
8
  return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
9
9
 
10
10
 
11
- def month_length(month: int, year: int):
11
+ def month_length(month: int, year: int) -> int:
12
12
  match month:
13
13
  # January, March, May, July, August, October, December
14
14
  case 1 | 3 | 5 | 7 | 8 | 10 | 12:
@@ -22,6 +22,9 @@ def month_length(month: int, year: int):
22
22
  case 2:
23
23
  return 29 if is_leap(year) else 28
24
24
 
25
+ case _:
26
+ raise ValueError(f"Invalid month: {month}")
27
+
25
28
 
26
29
  # also accepts negative inputs for n_months
27
30
  def add_months(s: datetime, n_months: int):
@@ -1,11 +1,14 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import builtins
3
+ import warnings
4
+ from collections.abc import Mapping
3
5
  from datetime import datetime, timezone
6
+ from decimal import Decimal
4
7
  from typing import Optional
5
8
 
6
9
  from google.protobuf.empty_pb2 import Empty
7
10
 
8
- from modal.exception import InvalidError
11
+ from modal.exception import DeprecationError, InvalidError
9
12
  from modal_proto import api_pb2
10
13
 
11
14
  from ._load_context import LoadContext
@@ -246,6 +249,64 @@ class _WorkspaceProxyTokenManager:
246
249
  return environment.object_id
247
250
 
248
251
 
252
+ class _WorkspaceBillingRates(Mapping[str, Decimal]):
253
+ def __init__(
254
+ self, rates: Mapping[str, str], warnings: Mapping[str, str], errors: Mapping[str, str], formatted: str
255
+ ):
256
+ self._rates = {k: Decimal(v) for k, v in rates.items()}
257
+ self._warnings = warnings
258
+ self._errors = errors
259
+ self._formatted = formatted
260
+
261
+ def __getitem__(self, key: str) -> Decimal:
262
+ if key in self._errors:
263
+ raise DeprecationError(self._errors[key])
264
+ elif key in self._warnings:
265
+ warnings.warn(self._warnings[key], DeprecationError)
266
+
267
+ return self._rates[key]
268
+
269
+ def __iter__(self):
270
+ for k in self._rates:
271
+ # We don't want iteration over the dict to surface any deprecated keys
272
+ if k in self._warnings:
273
+ continue
274
+ if k in self._errors:
275
+ continue
276
+
277
+ yield k
278
+
279
+ def __len__(self):
280
+ return len(set(self._rates.keys()) - set(self._warnings.keys()) - set(self._errors.keys()))
281
+
282
+ # Ensures that `rates.get()` doesn't throw `DeprecationError`s
283
+ # fixme(ayush): pyright seems to assume that the return type of `.get` for a `Mapping[K, V]` is
284
+ # always `V` regardless of the type of `default`. Fix this somehow?
285
+ def get(self, key, default=None): # pyright: ignore [reportIncompatibleMethodOverride]
286
+ try:
287
+ return self[key]
288
+ except KeyError:
289
+ return default
290
+ except DeprecationError as e:
291
+ warnings.warn(str(e), DeprecationError)
292
+ return default
293
+
294
+ # Ensures that `'k' in rates` doesn't throw `DeprecationError`s
295
+ def __contains__(self, key):
296
+ try:
297
+ self[key]
298
+ except (KeyError, DeprecationError):
299
+ return False
300
+ else:
301
+ return True
302
+
303
+ def __str__(self) -> str:
304
+ return self._formatted
305
+
306
+ def __repr__(self) -> str:
307
+ return repr({k: v for k, v in self.items()})
308
+
309
+
249
310
  class _WorkspaceBillingManager:
250
311
  """mdmd:namespace"""
251
312
 
@@ -253,6 +314,27 @@ class _WorkspaceBillingManager:
253
314
  """mdmd:hidden"""
254
315
  self._workspace = workspace
255
316
 
317
+ async def rates(self) -> _WorkspaceBillingRates:
318
+ """Return current pricing rates for the given workspace.
319
+
320
+ Returns:
321
+ A single mapping containing cost values. All values are reported as `decimal.Decimal`s.
322
+ """
323
+
324
+ if not self._workspace.is_hydrated:
325
+ await self._workspace.hydrate()
326
+
327
+ response = await self._workspace.client.stub.WorkspaceBillingRates(
328
+ api_pb2.WorkspaceBillingRatesRequest(),
329
+ )
330
+
331
+ return _WorkspaceBillingRates(
332
+ response.rates,
333
+ response.deprecation_warnings,
334
+ response.deprecation_errors,
335
+ response.formatted,
336
+ )
337
+
256
338
  async def report(
257
339
  self,
258
340
  *,
@@ -39,6 +39,7 @@ from ._utils.function_utils import (
39
39
  is_flash_object,
40
40
  is_global_object,
41
41
  is_method_fn,
42
+ validate_target_concurrency,
42
43
  )
43
44
  from ._utils.mount_utils import validate_volumes
44
45
  from ._utils.name_utils import check_object_name, check_tag_dict
@@ -1288,7 +1289,7 @@ class _App:
1288
1289
  cpu: float | tuple[float, float] | None = None, # CPU cores to request
1289
1290
  memory: int | tuple[int, int] | None = None, # Memory in MiB to request
1290
1291
  ephemeral_disk: int | None = None, # Ephemeral disk size in MiB
1291
- target_concurrency: int | None = None, # Target concurrency for the server; 0 disables autoscaling
1292
+ target_concurrency: float | None = None, # Target concurrency for the server; 0 disables autoscaling
1292
1293
  min_containers: int | None = None, # Minimum number of containers to keep warm
1293
1294
  max_containers: int | None = None, # Maximum number of containers
1294
1295
  buffer_containers: int | None = None, # Additional idle containers under active load
@@ -1335,7 +1336,9 @@ class _App:
1335
1336
  Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
1336
1337
  additionally specify a hard limit in MiB.
1337
1338
  ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
1338
- target_concurrency: Target concurrency for the server; 0 disables autoscaling.
1339
+ target_concurrency:
1340
+ Target number of concurrent requests per container; 0 disables autoscaling. May be
1341
+ fractional, e.g. 1.5 to target three concurrent requests per two containers.
1339
1342
  min_containers: Minimum number of containers to keep running regardless of demand.
1340
1343
  max_containers: Limit on the number of containers that can be concurrently running.
1341
1344
  buffer_containers: Extra containers to scale up beyond current demand.
@@ -1383,8 +1386,7 @@ class _App:
1383
1386
  )
1384
1387
 
1385
1388
  if target_concurrency is not None:
1386
- if not isinstance(target_concurrency, int) or target_concurrency < 0:
1387
- raise InvalidError("The `target_concurrency` argument must be a non-negative integer.")
1389
+ validate_target_concurrency(target_concurrency, "target_concurrency", allow_fractional=True)
1388
1390
 
1389
1391
  if scaleup_window is not None and scaleup_window <= 0:
1390
1392
  raise InvalidError("`scaleup_window` must be > 0")
@@ -744,7 +744,7 @@ class _App:
744
744
  cpu: typing.Union[float, tuple[float, float], None] = None,
745
745
  memory: typing.Union[int, tuple[int, int], None] = None,
746
746
  ephemeral_disk: typing.Optional[int] = None,
747
- target_concurrency: typing.Optional[int] = None,
747
+ target_concurrency: typing.Optional[float] = None,
748
748
  min_containers: typing.Optional[int] = None,
749
749
  max_containers: typing.Optional[int] = None,
750
750
  buffer_containers: typing.Optional[int] = None,
@@ -791,7 +791,9 @@ class _App:
791
791
  Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
792
792
  additionally specify a hard limit in MiB.
793
793
  ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
794
- target_concurrency: Target concurrency for the server; 0 disables autoscaling.
794
+ target_concurrency:
795
+ Target number of concurrent requests per container; 0 disables autoscaling. May be
796
+ fractional, e.g. 1.5 to target three concurrent requests per two containers.
795
797
  min_containers: Minimum number of containers to keep running regardless of demand.
796
798
  max_containers: Limit on the number of containers that can be concurrently running.
797
799
  buffer_containers: Extra containers to scale up beyond current demand.
@@ -1750,7 +1752,7 @@ class App:
1750
1752
  cpu: typing.Union[float, tuple[float, float], None] = None,
1751
1753
  memory: typing.Union[int, tuple[int, int], None] = None,
1752
1754
  ephemeral_disk: typing.Optional[int] = None,
1753
- target_concurrency: typing.Optional[int] = None,
1755
+ target_concurrency: typing.Optional[float] = None,
1754
1756
  min_containers: typing.Optional[int] = None,
1755
1757
  max_containers: typing.Optional[int] = None,
1756
1758
  buffer_containers: typing.Optional[int] = None,
@@ -1797,7 +1799,9 @@ class App:
1797
1799
  Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
1798
1800
  additionally specify a hard limit in MiB.
1799
1801
  ephemeral_disk: Specify, in MiB, the ephemeral disk size for the server.
1800
- target_concurrency: Target concurrency for the server; 0 disables autoscaling.
1802
+ target_concurrency:
1803
+ Target number of concurrent requests per container; 0 disables autoscaling. May be
1804
+ fractional, e.g. 1.5 to target three concurrent requests per two containers.
1801
1805
  min_containers: Minimum number of containers to keep running regardless of demand.
1802
1806
  max_containers: Limit on the number of containers that can be concurrently running.
1803
1807
  buffer_containers: Extra containers to scale up beyond current demand.
@@ -5,6 +5,7 @@ from json import dumps
5
5
 
6
6
  import click
7
7
  from rich.box import SIMPLE_HEAD
8
+ from rich.padding import Padding
8
9
  from rich.table import Table
9
10
  from rich.text import Text
10
11
 
@@ -296,3 +297,32 @@ async def summary(for_: str | None, json: bool):
296
297
  )
297
298
 
298
299
  output.print(t)
300
+
301
+
302
+ @billing_cli.command("rates")
303
+ @click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
304
+ @synchronizer.create_blocking
305
+ async def rates(json: bool = False):
306
+ """Return current pricing rates.
307
+
308
+ Examples:
309
+
310
+ ```bash
311
+ modal billing rates
312
+ ```
313
+ """
314
+
315
+ rates = await _Workspace.from_context().billing.rates()
316
+
317
+ output = OutputManager.get()
318
+
319
+ if json:
320
+ output.print_json(
321
+ dumps(
322
+ {k: str(v) for k, v in rates.items()},
323
+ sort_keys=True,
324
+ )
325
+ )
326
+ return
327
+
328
+ output.print(Padding(Text(rates._formatted), (1, 2)))
@@ -84,6 +84,7 @@ class _Client:
84
84
  _snapshotted: bool = False
85
85
  _connection_manager: ConnectionManager | None = None
86
86
  client_type: "api_pb2.ClientType.ValueType"
87
+ server_url: str
87
88
 
88
89
  def __init__(
89
90
  self,
@@ -31,13 +31,14 @@ class _Client:
31
31
  _snapshotted: bool
32
32
  _connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
33
33
  client_type: int
34
+ server_url: str
34
35
 
35
36
  def __init__(
36
37
  self,
37
38
  server_url: str,
38
39
  client_type: int,
39
40
  credentials: typing.Optional[tuple[str, str]],
40
- version: str = "1.5.4.dev20",
41
+ version: str = "1.5.4.dev22",
41
42
  ):
42
43
  """mdmd:hidden
43
44
  The Modal client object is not intended to be instantiated directly by users.
@@ -201,13 +202,14 @@ class Client:
201
202
  _snapshotted: bool
202
203
  _connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
203
204
  client_type: int
205
+ server_url: str
204
206
 
205
207
  def __init__(
206
208
  self,
207
209
  server_url: str,
208
210
  client_type: int,
209
211
  credentials: typing.Optional[tuple[str, str]],
210
- version: str = "1.5.4.dev20",
212
+ version: str = "1.5.4.dev22",
211
213
  ):
212
214
  """mdmd:hidden
213
215
  The Modal client object is not intended to be instantiated directly by users.
@@ -321,6 +321,7 @@ _SETTINGS = {
321
321
  "traceback": _Setting(False, transform=_to_boolean),
322
322
  "image_builder_version": _Setting(),
323
323
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
324
+ "sandbox_v2": _Setting(None, transform=_to_boolean),
324
325
  "snapshot_debug": _Setting(False, transform=_to_boolean),
325
326
  "cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
326
327
  "build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),
@@ -17,7 +17,7 @@ from modal.cls import _Cls
17
17
  from modal.dict import _Dict
18
18
  from modal_proto import api_pb2
19
19
 
20
- from .._runtime.container_io_manager import UserException
20
+ from .._runtime.task_lifecycle_manager import UserException
21
21
  from .._server import validate_http_server_config
22
22
  from .._tunnel import _forward as _forward_tunnel
23
23
  from .._utils.async_utils import synchronize_api, synchronizer
@@ -86,6 +86,7 @@ class _FlashManager:
86
86
  self.tunnel = await self.tunnel_manager.__aenter__()
87
87
  parsed_url = urlparse(self.tunnel.url)
88
88
  host = parsed_url.hostname
89
+ assert host is not None, f"Tunnel URL has no host: {self.tunnel.url}"
89
90
  port = parsed_url.port or 443
90
91
 
91
92
  if self.is_server:
@@ -326,7 +327,7 @@ class _FlashPrometheusAutoscaler:
326
327
  self.autoscaling_interval_seconds = autoscaling_interval_seconds
327
328
 
328
329
  FlashClass = _Cls.from_name(app_name, cls_name)
329
- self.fn = FlashClass._class_service_function
330
+ self.fn = FlashClass._get_class_service_function()
330
331
  self.cls = FlashClass()
331
332
 
332
333
  self.http_client = aiohttp.ClientSession()
@@ -335,7 +336,7 @@ class _FlashPrometheusAutoscaler:
335
336
  create_if_missing=True,
336
337
  )
337
338
 
338
- self.autoscaler_thread = None
339
+ self.autoscaler_thread: asyncio.Task | None = None
339
340
 
340
341
  async def start(self):
341
342
  await self.fn.hydrate(client=self.client)
@@ -519,11 +520,13 @@ class _FlashPrometheusAutoscaler:
519
520
  return sum_metric, n_containers_with_metrics
520
521
 
521
522
  async def _get_metrics(self, url: str) -> dict[str, list[Any]] | None: # technically any should be Sample
522
- from prometheus_client.parser import Sample, text_string_to_metric_families
523
+ import aiohttp
524
+ from prometheus_client.parser import text_string_to_metric_families
525
+ from prometheus_client.samples import Sample
523
526
 
524
527
  # Fetch the metrics from the endpoint
525
528
  try:
526
- response = await self.http_client.get(url, timeout=3)
529
+ response = await self.http_client.get(url, timeout=aiohttp.ClientTimeout(total=3))
527
530
  response.raise_for_status()
528
531
  except asyncio.TimeoutError:
529
532
  logger.warning(f"[Modal Flash] Timeout getting metrics from {url}")
@@ -627,6 +630,8 @@ class _FlashPrometheusAutoscaler:
627
630
  return new_replicas
628
631
 
629
632
  async def stop(self):
633
+ if self.autoscaler_thread is None:
634
+ return
630
635
  self.autoscaler_thread.cancel()
631
636
  await self.autoscaler_thread
632
637
 
@@ -696,20 +701,21 @@ async def flash_prometheus_autoscaler(
696
701
 
697
702
 
698
703
  @synchronizer.create_blocking
699
- async def flash_get_containers(app_name: str, cls_name: str) -> list[dict[str, Any]]:
704
+ async def flash_get_containers(app_name: str, cls_name: str) -> list[Any]:
700
705
  """
701
706
  Return a list of flash containers for a deployed Flash service.
702
707
 
708
+ Each entry exposes `task_id`, `host`, and `port` attributes.
709
+
703
710
  This is a highly experimental method that can break or be removed at any time without warning.
704
711
  Do not use this method unless explicitly instructed to do so by Modal support.
705
712
  """
706
713
  client = await _Client.from_env()
707
- fn = _Cls.from_name(app_name, cls_name)._class_service_function
708
- assert fn is not None
714
+ fn = _Cls.from_name(app_name, cls_name)._get_class_service_function()
709
715
  await fn.hydrate(client=client)
710
716
  req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
711
717
  resp = await client.stub.FlashContainerList(req)
712
- return resp.containers
718
+ return list(resp.containers)
713
719
 
714
720
 
715
721
  def _http_server(
@@ -404,17 +404,21 @@ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
404
404
  flash_prometheus_autoscaler: __flash_prometheus_autoscaler_spec
405
405
 
406
406
  class __flash_get_containers_spec(typing_extensions.Protocol):
407
- def __call__(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
407
+ def __call__(self, /, app_name: str, cls_name: str) -> list[typing.Any]:
408
408
  """Return a list of flash containers for a deployed Flash service.
409
409
 
410
+ Each entry exposes `task_id`, `host`, and `port` attributes.
411
+
410
412
  This is a highly experimental method that can break or be removed at any time without warning.
411
413
  Do not use this method unless explicitly instructed to do so by Modal support.
412
414
  """
413
415
  ...
414
416
 
415
- async def aio(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
417
+ async def aio(self, /, app_name: str, cls_name: str) -> list[typing.Any]:
416
418
  """Return a list of flash containers for a deployed Flash service.
417
419
 
420
+ Each entry exposes `task_id`, `host`, and `port` attributes.
421
+
418
422
  This is a highly experimental method that can break or be removed at any time without warning.
419
423
  Do not use this method unless explicitly instructed to do so by Modal support.
420
424
  """
@@ -117,7 +117,7 @@ class Function(
117
117
  scaleup_window: typing.Optional[int] = None,
118
118
  scaledown_window: typing.Optional[int] = None,
119
119
  max_concurrent_inputs: typing.Optional[int] = None,
120
- target_concurrent_inputs: typing.Optional[int] = None,
120
+ target_concurrent_inputs: typing.Optional[float] = None,
121
121
  batch_max_size: typing.Optional[int] = None,
122
122
  batch_wait_ms: typing.Optional[int] = None,
123
123
  cloud: typing.Optional[str] = None,
@@ -179,7 +179,7 @@ class Function(
179
179
  buffer_containers: typing.Optional[int] = None,
180
180
  scaleup_window: typing.Optional[int] = None,
181
181
  scaledown_window: typing.Optional[int] = None,
182
- target_concurrency: typing.Optional[int] = None,
182
+ target_concurrency: typing.Optional[float] = None,
183
183
  ) -> modal.types.ServerAutoscalerSettings: ...
184
184
  async def aio(
185
185
  self,
@@ -190,7 +190,7 @@ class Function(
190
190
  buffer_containers: typing.Optional[int] = None,
191
191
  scaleup_window: typing.Optional[int] = None,
192
192
  scaledown_window: typing.Optional[int] = None,
193
- target_concurrency: typing.Optional[int] = None,
193
+ target_concurrency: typing.Optional[float] = None,
194
194
  ) -> modal.types.ServerAutoscalerSettings: ...
195
195
 
196
196
  _update_autoscaler_server: ___update_autoscaler_server_spec