modal 1.1.5.dev2__tar.gz → 1.1.5.dev4__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 (190) hide show
  1. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/PKG-INFO +1 -1
  2. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/client.pyi +2 -2
  3. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/experimental/flash.py +80 -53
  4. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/experimental/flash.pyi +17 -5
  5. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/PKG-INFO +1 -1
  6. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_version/__init__.py +1 -1
  7. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/LICENSE +0 -0
  8. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/README.md +0 -0
  9. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/__init__.py +0 -0
  10. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/__main__.py +0 -0
  11. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_clustered_functions.py +0 -0
  12. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_clustered_functions.pyi +0 -0
  13. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_container_entrypoint.py +0 -0
  14. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_functions.py +0 -0
  15. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_ipython.py +0 -0
  16. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_location.py +0 -0
  17. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_object.py +0 -0
  18. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_output.py +0 -0
  19. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_partial_function.py +0 -0
  20. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_pty.py +0 -0
  21. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_resolver.py +0 -0
  22. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_resources.py +0 -0
  23. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/__init__.py +0 -0
  24. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/asgi.py +0 -0
  25. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/container_io_manager.py +0 -0
  26. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/container_io_manager.pyi +0 -0
  27. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/execution_context.py +0 -0
  28. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/execution_context.pyi +0 -0
  29. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  30. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/telemetry.py +0 -0
  31. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_runtime/user_code_imports.py +0 -0
  32. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_serialization.py +0 -0
  33. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_traceback.py +0 -0
  34. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_tunnel.py +0 -0
  35. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_tunnel.pyi +0 -0
  36. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_type_manager.py +0 -0
  37. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/__init__.py +0 -0
  38. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/app_utils.py +0 -0
  39. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/async_utils.py +0 -0
  40. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/auth_token_manager.py +0 -0
  41. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/blob_utils.py +0 -0
  42. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/bytes_io_segment_payload.py +0 -0
  43. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/deprecation.py +0 -0
  44. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/docker_utils.py +0 -0
  45. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/function_utils.py +0 -0
  46. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/git_utils.py +0 -0
  47. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/grpc_testing.py +0 -0
  48. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/grpc_utils.py +0 -0
  49. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/hash_utils.py +0 -0
  50. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/http_utils.py +0 -0
  51. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/jwt_utils.py +0 -0
  52. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/logger.py +0 -0
  53. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/mount_utils.py +0 -0
  54. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/name_utils.py +0 -0
  55. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/package_utils.py +0 -0
  56. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/pattern_utils.py +0 -0
  57. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/rand_pb_testing.py +0 -0
  58. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/shell_utils.py +0 -0
  59. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_utils/time_utils.py +0 -0
  60. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_vendor/__init__.py +0 -0
  61. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  62. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_vendor/cloudpickle.py +0 -0
  63. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_vendor/tblib.py +0 -0
  64. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/_watcher.py +0 -0
  65. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/app.py +0 -0
  66. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/app.pyi +0 -0
  67. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/2023.12.312.txt +0 -0
  68. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/2023.12.txt +0 -0
  69. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/2024.04.txt +0 -0
  70. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/2024.10.txt +0 -0
  71. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/2025.06.txt +0 -0
  72. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/PREVIEW.txt +0 -0
  73. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/README.md +0 -0
  74. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/builder/base-images.json +0 -0
  75. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/call_graph.py +0 -0
  76. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/__init__.py +0 -0
  77. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/_download.py +0 -0
  78. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/_traceback.py +0 -0
  79. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/app.py +0 -0
  80. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/cluster.py +0 -0
  81. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/config.py +0 -0
  82. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/container.py +0 -0
  83. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/dict.py +0 -0
  84. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/entry_point.py +0 -0
  85. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/environment.py +0 -0
  86. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/import_refs.py +0 -0
  87. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/launch.py +0 -0
  88. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/network_file_system.py +0 -0
  89. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/profile.py +0 -0
  90. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/programs/__init__.py +0 -0
  91. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/programs/launch_instance_ssh.py +0 -0
  92. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/programs/run_jupyter.py +0 -0
  93. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/programs/run_marimo.py +0 -0
  94. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/programs/vscode.py +0 -0
  95. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/queues.py +0 -0
  96. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/run.py +0 -0
  97. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/secret.py +0 -0
  98. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/token.py +0 -0
  99. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/utils.py +0 -0
  100. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cli/volume.py +0 -0
  101. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/client.py +0 -0
  102. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cloud_bucket_mount.py +0 -0
  103. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cloud_bucket_mount.pyi +0 -0
  104. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cls.py +0 -0
  105. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/cls.pyi +0 -0
  106. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/config.py +0 -0
  107. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/container_process.py +0 -0
  108. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/container_process.pyi +0 -0
  109. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/dict.py +0 -0
  110. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/dict.pyi +0 -0
  111. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/environments.py +0 -0
  112. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/environments.pyi +0 -0
  113. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/exception.py +0 -0
  114. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/experimental/__init__.py +0 -0
  115. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/experimental/ipython.py +0 -0
  116. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/file_io.py +0 -0
  117. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/file_io.pyi +0 -0
  118. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/file_pattern_matcher.py +0 -0
  119. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/functions.py +0 -0
  120. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/functions.pyi +0 -0
  121. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/gpu.py +0 -0
  122. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/image.py +0 -0
  123. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/image.pyi +0 -0
  124. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/io_streams.py +0 -0
  125. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/io_streams.pyi +0 -0
  126. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/mount.py +0 -0
  127. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/mount.pyi +0 -0
  128. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/network_file_system.py +0 -0
  129. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/network_file_system.pyi +0 -0
  130. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/object.py +0 -0
  131. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/object.pyi +0 -0
  132. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/output.py +0 -0
  133. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/parallel_map.py +0 -0
  134. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/parallel_map.pyi +0 -0
  135. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/partial_function.py +0 -0
  136. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/partial_function.pyi +0 -0
  137. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/proxy.py +0 -0
  138. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/proxy.pyi +0 -0
  139. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/py.typed +0 -0
  140. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/queue.py +0 -0
  141. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/queue.pyi +0 -0
  142. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/retries.py +0 -0
  143. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/runner.py +0 -0
  144. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/runner.pyi +0 -0
  145. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/running_app.py +0 -0
  146. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/sandbox.py +0 -0
  147. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/sandbox.pyi +0 -0
  148. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/schedule.py +0 -0
  149. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/scheduler_placement.py +0 -0
  150. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/secret.py +0 -0
  151. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/secret.pyi +0 -0
  152. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/serving.py +0 -0
  153. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/serving.pyi +0 -0
  154. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/snapshot.py +0 -0
  155. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/snapshot.pyi +0 -0
  156. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/stream_type.py +0 -0
  157. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/token_flow.py +0 -0
  158. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/token_flow.pyi +0 -0
  159. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/volume.py +0 -0
  160. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal/volume.pyi +0 -0
  161. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/SOURCES.txt +0 -0
  162. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/dependency_links.txt +0 -0
  163. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/entry_points.txt +0 -0
  164. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/requires.txt +0 -0
  165. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal.egg-info/top_level.txt +0 -0
  166. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/__init__.py +0 -0
  167. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/gen_cli_docs.py +0 -0
  168. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/gen_reference_docs.py +0 -0
  169. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/mdmd/__init__.py +0 -0
  170. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/mdmd/mdmd.py +0 -0
  171. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_docs/mdmd/signatures.py +0 -0
  172. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/__init__.py +0 -0
  173. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api.proto +0 -0
  174. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api_grpc.py +0 -0
  175. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api_pb2.py +0 -0
  176. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api_pb2.pyi +0 -0
  177. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api_pb2_grpc.py +0 -0
  178. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/api_pb2_grpc.pyi +0 -0
  179. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/modal_api_grpc.py +0 -0
  180. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/modal_options_grpc.py +0 -0
  181. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options.proto +0 -0
  182. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options_grpc.py +0 -0
  183. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options_pb2.py +0 -0
  184. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options_pb2.pyi +0 -0
  185. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options_pb2_grpc.py +0 -0
  186. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/options_pb2_grpc.pyi +0 -0
  187. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_proto/py.typed +0 -0
  188. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/modal_version/__main__.py +0 -0
  189. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/pyproject.toml +0 -0
  190. {modal-1.1.5.dev2 → modal-1.1.5.dev4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.5.dev2
3
+ Version: 1.1.5.dev4
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.1.5.dev2",
36
+ version: str = "1.1.5.dev4",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.1.5.dev2",
167
+ version: str = "1.1.5.dev4",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -21,7 +21,7 @@ from ..client import _Client
21
21
  from ..config import logger
22
22
  from ..exception import InvalidError
23
23
 
24
- MAX_FAILURES = 3
24
+ _MAX_FAILURES = 3
25
25
 
26
26
 
27
27
  class _FlashManager:
@@ -42,7 +42,9 @@ class _FlashManager:
42
42
  self.num_failures = 0
43
43
  self.task_id = os.environ["MODAL_TASK_ID"]
44
44
 
45
- async def check_port_connection(self, process: Optional[subprocess.Popen], timeout: int = 10):
45
+ async def is_port_connection_healthy(
46
+ self, process: Optional[subprocess.Popen], timeout: int = 5
47
+ ) -> tuple[bool, Optional[Exception]]:
46
48
  import socket
47
49
 
48
50
  start_time = time.monotonic()
@@ -50,13 +52,13 @@ class _FlashManager:
50
52
  while time.monotonic() - start_time < timeout:
51
53
  try:
52
54
  if process is not None and process.poll() is not None:
53
- return Exception(f"Process {process.pid} exited with code {process.returncode}")
55
+ return False, Exception(f"Process {process.pid} exited with code {process.returncode}")
54
56
  with socket.create_connection(("localhost", self.port), timeout=1):
55
- return
57
+ return True, None
56
58
  except (ConnectionRefusedError, OSError):
57
59
  await asyncio.sleep(0.1)
58
60
 
59
- return Exception(f"Waited too long for port {self.port} to start accepting connections")
61
+ return False, Exception(f"Waited too long for port {self.port} to start accepting connections")
60
62
 
61
63
  async def _start(self):
62
64
  self.tunnel = await self.tunnel_manager.__aenter__()
@@ -74,7 +76,7 @@ class _FlashManager:
74
76
  while True:
75
77
  try:
76
78
  # Check if the container should be drained (e.g., too many failures)
77
- if self.num_failures > MAX_FAILURES:
79
+ if self.num_failures > _MAX_FAILURES:
78
80
  logger.warning(
79
81
  f"[Modal Flash] Draining task {self.task_id} on {self.tunnel.url} due to too many failures."
80
82
  )
@@ -101,35 +103,38 @@ class _FlashManager:
101
103
  first_registration = True
102
104
  while True:
103
105
  try:
104
- await self.check_port_connection(process=self.process)
105
- resp = await self.client.stub.FlashContainerRegister(
106
- api_pb2.FlashContainerRegisterRequest(
107
- priority=10,
108
- weight=5,
109
- host=host,
110
- port=port,
111
- ),
112
- timeout=10,
113
- )
114
- self.num_failures = 0
115
- if first_registration:
116
- logger.warning(
117
- f"[Modal Flash] Listening at {resp.url} over {self.tunnel.url} for task_id {self.task_id}"
106
+ port_check_resp, port_check_error = await self.is_port_connection_healthy(process=self.process)
107
+ if port_check_resp:
108
+ resp = await self.client.stub.FlashContainerRegister(
109
+ api_pb2.FlashContainerRegisterRequest(
110
+ priority=10,
111
+ weight=5,
112
+ host=host,
113
+ port=port,
114
+ ),
115
+ timeout=10,
116
+ )
117
+ self.num_failures = 0
118
+ if first_registration:
119
+ logger.warning(
120
+ f"[Modal Flash] Listening at {resp.url} over {self.tunnel.url} for task_id {self.task_id}"
121
+ )
122
+ first_registration = False
123
+ else:
124
+ logger.error(
125
+ f"[Modal Flash] Deregistering container {self.task_id} on {self.tunnel.url} "
126
+ f"due to error: {port_check_error}, num_failures: {self.num_failures}"
127
+ )
128
+ self.num_failures += 1
129
+ await retry_transient_errors(
130
+ self.client.stub.FlashContainerDeregister,
131
+ api_pb2.FlashContainerDeregisterRequest(),
118
132
  )
119
- first_registration = False
120
133
  except asyncio.CancelledError:
121
134
  logger.warning("[Modal Flash] Shutting down...")
122
135
  break
123
136
  except Exception as e:
124
137
  logger.error(f"[Modal Flash] Heartbeat failed: {e}")
125
- self.num_failures += 1
126
- logger.error(
127
- f"[Modal Flash] Deregistering container {self.tunnel.url}, num_failures: {self.num_failures}"
128
- )
129
- await retry_transient_errors(
130
- self.client.stub.FlashContainerDeregister,
131
- api_pb2.FlashContainerDeregisterRequest(),
132
- )
133
138
 
134
139
  try:
135
140
  await asyncio.sleep(1)
@@ -167,7 +172,9 @@ FlashManager = synchronize_api(_FlashManager)
167
172
 
168
173
  @synchronizer.create_blocking
169
174
  async def flash_forward(
170
- port: int, process: Optional[subprocess.Popen] = None, health_check_url: Optional[str] = None
175
+ port: int,
176
+ process: Optional[subprocess.Popen] = None,
177
+ health_check_url: Optional[str] = None,
171
178
  ) -> _FlashManager:
172
179
  """
173
180
  Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
@@ -194,6 +201,7 @@ class _FlashPrometheusAutoscaler:
194
201
  target_metric_value: float,
195
202
  min_containers: Optional[int],
196
203
  max_containers: Optional[int],
204
+ buffer_containers: Optional[int],
197
205
  scale_up_tolerance: float,
198
206
  scale_down_tolerance: float,
199
207
  scale_up_stabilization_window_seconds: int,
@@ -221,6 +229,7 @@ class _FlashPrometheusAutoscaler:
221
229
  self.target_metric_value = target_metric_value
222
230
  self.min_containers = min_containers
223
231
  self.max_containers = max_containers
232
+ self.buffer_containers = buffer_containers
224
233
  self.scale_up_tolerance = scale_up_tolerance
225
234
  self.scale_down_tolerance = scale_down_tolerance
226
235
  self.scale_up_stabilization_window_seconds = scale_up_stabilization_window_seconds
@@ -286,6 +295,7 @@ class _FlashPrometheusAutoscaler:
286
295
  scale_down_stabilization_window_seconds=self.scale_down_stabilization_window_seconds,
287
296
  min_containers=self.min_containers,
288
297
  max_containers=self.max_containers,
298
+ buffer_containers=self.buffer_containers,
289
299
  )
290
300
 
291
301
  logger.warning(
@@ -395,6 +405,7 @@ class _FlashPrometheusAutoscaler:
395
405
  # Gets metrics from prometheus
396
406
  sum_metric = 0
397
407
  containers_with_metrics = 0
408
+ buffer_containers = self.buffer_containers or 0
398
409
  container_metrics_list = await asyncio.gather(
399
410
  *[
400
411
  self._get_metrics(f"https://{container.host}:{container.port}/{self.metrics_endpoint}")
@@ -411,29 +422,36 @@ class _FlashPrometheusAutoscaler:
411
422
  sum_metric += container_metrics[target_metric][0].value
412
423
  containers_with_metrics += 1
413
424
 
414
- # n_containers_missing_metric is the number of unhealthy containers + number of cold starting containers
425
+ # n_containers_missing = number of unhealthy containers + number of containers not registered in flash dns
415
426
  n_containers_missing_metric = current_replicas - containers_with_metrics
416
- # n_containers_unhealthy is the number of live containers that are not emitting metrics i.e. unhealthy
427
+ # n_containers_unhealthy = number of dns registered containers that are not emitting metrics
417
428
  n_containers_unhealthy = len(containers) - containers_with_metrics
418
429
 
419
- # Scale up assuming that every unhealthy container is at 2x the target metric value.
420
- scale_up_target_metric_value = (sum_metric + n_containers_unhealthy * target_metric_value) / (
421
- (containers_with_metrics + n_containers_unhealthy) or 1
422
- )
430
+ # number of total containers - buffer containers
431
+ # This is used in 1) scale ratio denominators 2) provisioning base.
432
+ # Max is used to handle case when buffer_containers are first initialized.
433
+ num_provisioned_containers = max(current_replicas - buffer_containers, 1)
434
+
435
+ # Scale up assuming that every unhealthy container is at (1 + scale_up_tolerance)x the target metric value.
436
+ # This way if all containers are unhealthy, we will increase our number of containers.
437
+ scale_up_target_metric_value = (
438
+ sum_metric + (1 + self.scale_up_tolerance) * n_containers_unhealthy * target_metric_value
439
+ ) / (num_provisioned_containers)
423
440
 
424
441
  # Scale down assuming that every container (including cold starting containers) are at the target metric value.
442
+ # The denominator is just num_provisioned_containers because we don't want to account for the buffer containers.
425
443
  scale_down_target_metric_value = (sum_metric + n_containers_missing_metric * target_metric_value) / (
426
- current_replicas or 1
444
+ num_provisioned_containers
427
445
  )
428
446
 
429
447
  scale_up_ratio = scale_up_target_metric_value / target_metric_value
430
448
  scale_down_ratio = scale_down_target_metric_value / target_metric_value
431
449
 
432
- desired_replicas = current_replicas
450
+ desired_replicas = num_provisioned_containers
433
451
  if scale_up_ratio > 1 + self.scale_up_tolerance:
434
- desired_replicas = math.ceil(current_replicas * scale_up_ratio)
452
+ desired_replicas = math.ceil(desired_replicas * scale_up_ratio)
435
453
  elif scale_down_ratio < 1 - self.scale_down_tolerance:
436
- desired_replicas = math.ceil(current_replicas * scale_down_ratio)
454
+ desired_replicas = math.ceil(desired_replicas * scale_down_ratio)
437
455
 
438
456
  logger.warning(
439
457
  f"[Modal Flash] Current replicas: {current_replicas}, "
@@ -442,6 +460,7 @@ class _FlashPrometheusAutoscaler:
442
460
  f"number of containers with metrics: {containers_with_metrics}, "
443
461
  f"number of containers unhealthy: {n_containers_unhealthy}, "
444
462
  f"number of containers missing metric (includes unhealthy): {n_containers_missing_metric}, "
463
+ f"number of provisioned containers: {num_provisioned_containers}, "
445
464
  f"scale up ratio: {scale_up_ratio}, "
446
465
  f"scale down ratio: {scale_down_ratio}, "
447
466
  f"desired replicas: {desired_replicas}"
@@ -503,6 +522,7 @@ class _FlashPrometheusAutoscaler:
503
522
  scale_down_stabilization_window_seconds: int = 60 * 5,
504
523
  min_containers: Optional[int] = None,
505
524
  max_containers: Optional[int] = None,
525
+ buffer_containers: Optional[int] = None,
506
526
  ) -> int:
507
527
  """
508
528
  Return the target number of containers following (simplified) Kubernetes HPA
@@ -553,6 +573,10 @@ class _FlashPrometheusAutoscaler:
553
573
  new_replicas = max(min_containers, new_replicas)
554
574
  if max_containers is not None:
555
575
  new_replicas = min(max_containers, new_replicas)
576
+
577
+ if buffer_containers is not None:
578
+ new_replicas += buffer_containers
579
+
556
580
  return new_replicas
557
581
 
558
582
  async def stop(self):
@@ -590,6 +614,8 @@ async def flash_prometheus_autoscaler(
590
614
  # How often to make autoscaling decisions.
591
615
  # Corresponds to --horizontal-pod-autoscaler-sync-period in Kubernetes.
592
616
  autoscaling_interval_seconds: int = 15,
617
+ # Whether to include overprovisioned containers in the scale up calculation.
618
+ buffer_containers: Optional[int] = None,
593
619
  ) -> _FlashPrometheusAutoscaler:
594
620
  """
595
621
  Autoscale a Flash service based on containers' Prometheus metrics.
@@ -607,19 +633,20 @@ async def flash_prometheus_autoscaler(
607
633
 
608
634
  client = await _Client.from_env()
609
635
  autoscaler = _FlashPrometheusAutoscaler(
610
- client,
611
- app_name,
612
- cls_name,
613
- metrics_endpoint,
614
- target_metric,
615
- target_metric_value,
616
- min_containers,
617
- max_containers,
618
- scale_up_tolerance,
619
- scale_down_tolerance,
620
- scale_up_stabilization_window_seconds,
621
- scale_down_stabilization_window_seconds,
622
- autoscaling_interval_seconds,
636
+ client=client,
637
+ app_name=app_name,
638
+ cls_name=cls_name,
639
+ metrics_endpoint=metrics_endpoint,
640
+ target_metric=target_metric,
641
+ target_metric_value=target_metric_value,
642
+ min_containers=min_containers,
643
+ max_containers=max_containers,
644
+ buffer_containers=buffer_containers,
645
+ scale_up_tolerance=scale_up_tolerance,
646
+ scale_down_tolerance=scale_down_tolerance,
647
+ scale_up_stabilization_window_seconds=scale_up_stabilization_window_seconds,
648
+ scale_down_stabilization_window_seconds=scale_down_stabilization_window_seconds,
649
+ autoscaling_interval_seconds=autoscaling_interval_seconds,
623
650
  )
624
651
  await autoscaler.start()
625
652
  return autoscaler
@@ -15,7 +15,9 @@ class _FlashManager:
15
15
  """Initialize self. See help(type(self)) for accurate signature."""
16
16
  ...
17
17
 
18
- async def check_port_connection(self, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
18
+ async def is_port_connection_healthy(
19
+ self, process: typing.Optional[subprocess.Popen], timeout: int = 5
20
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
19
21
  async def _start(self): ...
20
22
  async def _drain_container(self):
21
23
  """Background task that checks if we've encountered too many failures and drains the container if so."""
@@ -37,11 +39,15 @@ class FlashManager:
37
39
  health_check_url: typing.Optional[str] = None,
38
40
  ): ...
39
41
 
40
- class __check_port_connection_spec(typing_extensions.Protocol[SUPERSELF]):
41
- def __call__(self, /, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
42
- async def aio(self, /, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
42
+ class __is_port_connection_healthy_spec(typing_extensions.Protocol[SUPERSELF]):
43
+ def __call__(
44
+ self, /, process: typing.Optional[subprocess.Popen], timeout: int = 5
45
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
46
+ async def aio(
47
+ self, /, process: typing.Optional[subprocess.Popen], timeout: int = 5
48
+ ) -> tuple[bool, typing.Optional[Exception]]: ...
43
49
 
44
- check_port_connection: __check_port_connection_spec[typing_extensions.Self]
50
+ is_port_connection_healthy: __is_port_connection_healthy_spec[typing_extensions.Self]
45
51
 
46
52
  class ___start_spec(typing_extensions.Protocol[SUPERSELF]):
47
53
  def __call__(self, /): ...
@@ -120,6 +126,7 @@ class _FlashPrometheusAutoscaler:
120
126
  target_metric_value: float,
121
127
  min_containers: typing.Optional[int],
122
128
  max_containers: typing.Optional[int],
129
+ buffer_containers: typing.Optional[int],
123
130
  scale_up_tolerance: float,
124
131
  scale_down_tolerance: float,
125
132
  scale_up_stabilization_window_seconds: int,
@@ -149,6 +156,7 @@ class _FlashPrometheusAutoscaler:
149
156
  scale_down_stabilization_window_seconds: int = 300,
150
157
  min_containers: typing.Optional[int] = None,
151
158
  max_containers: typing.Optional[int] = None,
159
+ buffer_containers: typing.Optional[int] = None,
152
160
  ) -> int:
153
161
  """Return the target number of containers following (simplified) Kubernetes HPA
154
162
  stabilization-window semantics.
@@ -181,6 +189,7 @@ class FlashPrometheusAutoscaler:
181
189
  target_metric_value: float,
182
190
  min_containers: typing.Optional[int],
183
191
  max_containers: typing.Optional[int],
192
+ buffer_containers: typing.Optional[int],
184
193
  scale_up_tolerance: float,
185
194
  scale_down_tolerance: float,
186
195
  scale_up_stabilization_window_seconds: int,
@@ -247,6 +256,7 @@ class FlashPrometheusAutoscaler:
247
256
  scale_down_stabilization_window_seconds: int = 300,
248
257
  min_containers: typing.Optional[int] = None,
249
258
  max_containers: typing.Optional[int] = None,
259
+ buffer_containers: typing.Optional[int] = None,
250
260
  ) -> int:
251
261
  """Return the target number of containers following (simplified) Kubernetes HPA
252
262
  stabilization-window semantics.
@@ -288,6 +298,7 @@ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
288
298
  scale_up_stabilization_window_seconds: int = 0,
289
299
  scale_down_stabilization_window_seconds: int = 300,
290
300
  autoscaling_interval_seconds: int = 15,
301
+ buffer_containers: typing.Optional[int] = None,
291
302
  ) -> FlashPrometheusAutoscaler:
292
303
  """Autoscale a Flash service based on containers' Prometheus metrics.
293
304
 
@@ -313,6 +324,7 @@ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
313
324
  scale_up_stabilization_window_seconds: int = 0,
314
325
  scale_down_stabilization_window_seconds: int = 300,
315
326
  autoscaling_interval_seconds: int = 15,
327
+ buffer_containers: typing.Optional[int] = None,
316
328
  ) -> FlashPrometheusAutoscaler:
317
329
  """Autoscale a Flash service based on containers' Prometheus metrics.
318
330
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.5.dev2
3
+ Version: 1.1.5.dev4
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.1.5.dev2"
4
+ __version__ = "1.1.5.dev4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes