modal 1.2.5.dev4__tar.gz → 1.2.5.dev10__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (202) hide show
  1. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/PKG-INFO +1 -1
  2. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_functions.py +3 -0
  3. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_partial_function.py +4 -1
  4. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/user_code_imports.py +95 -77
  5. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/function_utils.py +2 -2
  6. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/grpc_utils.py +3 -1
  7. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/task_command_router_client.py +8 -6
  8. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/app.py +19 -3
  9. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/client.pyi +2 -2
  10. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/experimental/__init__.py +6 -1
  11. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/experimental/flash.py +86 -12
  12. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/experimental/flash.pyi +38 -0
  13. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/functions.pyi +1 -0
  14. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/io_streams.py +78 -99
  15. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/io_streams.pyi +28 -26
  16. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/runner.py +1 -1
  17. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/PKG-INFO +1 -1
  18. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_version/__init__.py +1 -1
  19. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/LICENSE +0 -0
  20. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/README.md +0 -0
  21. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/__init__.py +0 -0
  22. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/__main__.py +0 -0
  23. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_billing.py +0 -0
  24. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_clustered_functions.py +0 -0
  25. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_clustered_functions.pyi +0 -0
  26. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_container_entrypoint.py +0 -0
  27. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_grpc_client.py +0 -0
  28. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_ipython.py +0 -0
  29. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_load_context.py +0 -0
  30. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_location.py +0 -0
  31. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_object.py +0 -0
  32. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_output.py +0 -0
  33. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_pty.py +0 -0
  34. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_resolver.py +0 -0
  35. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_resources.py +0 -0
  36. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/__init__.py +0 -0
  37. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/asgi.py +0 -0
  38. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/container_io_manager.py +0 -0
  39. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/container_io_manager.pyi +0 -0
  40. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/execution_context.py +0 -0
  41. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/execution_context.pyi +0 -0
  42. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  43. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/telemetry.py +0 -0
  44. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_runtime/user_code_event_loop.py +0 -0
  45. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_serialization.py +0 -0
  46. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_traceback.py +0 -0
  47. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_tunnel.py +0 -0
  48. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_tunnel.pyi +0 -0
  49. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_type_manager.py +0 -0
  50. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/__init__.py +0 -0
  51. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/app_utils.py +0 -0
  52. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/async_utils.py +0 -0
  53. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/auth_token_manager.py +0 -0
  54. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/blob_utils.py +0 -0
  55. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/bytes_io_segment_payload.py +0 -0
  56. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/deprecation.py +0 -0
  57. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/docker_utils.py +0 -0
  58. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/git_utils.py +0 -0
  59. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/grpc_testing.py +0 -0
  60. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/hash_utils.py +0 -0
  61. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/http_utils.py +0 -0
  62. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/jwt_utils.py +0 -0
  63. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/logger.py +0 -0
  64. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/mount_utils.py +0 -0
  65. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/name_utils.py +0 -0
  66. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/package_utils.py +0 -0
  67. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/pattern_utils.py +0 -0
  68. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/rand_pb_testing.py +0 -0
  69. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/shell_utils.py +0 -0
  70. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_utils/time_utils.py +0 -0
  71. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_vendor/__init__.py +0 -0
  72. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  73. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_vendor/cloudpickle.py +0 -0
  74. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_vendor/tblib.py +0 -0
  75. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/_watcher.py +0 -0
  76. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/app.pyi +0 -0
  77. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/billing.py +0 -0
  78. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/2023.12.312.txt +0 -0
  79. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/2023.12.txt +0 -0
  80. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/2024.04.txt +0 -0
  81. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/2024.10.txt +0 -0
  82. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/2025.06.txt +0 -0
  83. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/PREVIEW.txt +0 -0
  84. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/README.md +0 -0
  85. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/builder/base-images.json +0 -0
  86. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/call_graph.py +0 -0
  87. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/__init__.py +0 -0
  88. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/_download.py +0 -0
  89. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/_traceback.py +0 -0
  90. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/app.py +0 -0
  91. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/cluster.py +0 -0
  92. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/config.py +0 -0
  93. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/container.py +0 -0
  94. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/dict.py +0 -0
  95. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/entry_point.py +0 -0
  96. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/environment.py +0 -0
  97. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/import_refs.py +0 -0
  98. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/launch.py +0 -0
  99. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/network_file_system.py +0 -0
  100. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/profile.py +0 -0
  101. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/programs/__init__.py +0 -0
  102. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/programs/launch_instance_ssh.py +0 -0
  103. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/programs/run_jupyter.py +0 -0
  104. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/programs/run_marimo.py +0 -0
  105. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/programs/vscode.py +0 -0
  106. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/queues.py +0 -0
  107. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/run.py +0 -0
  108. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/secret.py +0 -0
  109. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/shell.py +0 -0
  110. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/token.py +0 -0
  111. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/utils.py +0 -0
  112. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cli/volume.py +0 -0
  113. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/client.py +0 -0
  114. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cloud_bucket_mount.py +0 -0
  115. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cloud_bucket_mount.pyi +0 -0
  116. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cls.py +0 -0
  117. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/cls.pyi +0 -0
  118. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/config.py +0 -0
  119. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/container_process.py +0 -0
  120. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/container_process.pyi +0 -0
  121. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/dict.py +0 -0
  122. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/dict.pyi +0 -0
  123. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/environments.py +0 -0
  124. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/environments.pyi +0 -0
  125. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/exception.py +0 -0
  126. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/experimental/ipython.py +0 -0
  127. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/file_io.py +0 -0
  128. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/file_io.pyi +0 -0
  129. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/file_pattern_matcher.py +0 -0
  130. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/functions.py +0 -0
  131. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/gpu.py +0 -0
  132. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/image.py +0 -0
  133. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/image.pyi +0 -0
  134. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/mount.py +0 -0
  135. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/mount.pyi +0 -0
  136. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/network_file_system.py +0 -0
  137. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/network_file_system.pyi +0 -0
  138. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/object.py +0 -0
  139. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/object.pyi +0 -0
  140. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/output.py +0 -0
  141. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/parallel_map.py +0 -0
  142. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/parallel_map.pyi +0 -0
  143. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/partial_function.py +0 -0
  144. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/partial_function.pyi +0 -0
  145. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/proxy.py +0 -0
  146. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/proxy.pyi +0 -0
  147. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/py.typed +0 -0
  148. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/queue.py +0 -0
  149. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/queue.pyi +0 -0
  150. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/retries.py +0 -0
  151. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/runner.pyi +0 -0
  152. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/running_app.py +0 -0
  153. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/sandbox.py +0 -0
  154. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/sandbox.pyi +0 -0
  155. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/schedule.py +0 -0
  156. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/scheduler_placement.py +0 -0
  157. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/secret.py +0 -0
  158. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/secret.pyi +0 -0
  159. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/serving.py +0 -0
  160. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/serving.pyi +0 -0
  161. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/snapshot.py +0 -0
  162. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/snapshot.pyi +0 -0
  163. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/stream_type.py +0 -0
  164. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/token_flow.py +0 -0
  165. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/token_flow.pyi +0 -0
  166. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/volume.py +0 -0
  167. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal/volume.pyi +0 -0
  168. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/SOURCES.txt +0 -0
  169. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/dependency_links.txt +0 -0
  170. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/entry_points.txt +0 -0
  171. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/requires.txt +0 -0
  172. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal.egg-info/top_level.txt +0 -0
  173. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/__init__.py +0 -0
  174. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/gen_cli_docs.py +0 -0
  175. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/gen_reference_docs.py +0 -0
  176. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/mdmd/__init__.py +0 -0
  177. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/mdmd/mdmd.py +0 -0
  178. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_docs/mdmd/signatures.py +0 -0
  179. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/__init__.py +0 -0
  180. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api.proto +0 -0
  181. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api_grpc.py +0 -0
  182. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api_pb2.py +0 -0
  183. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api_pb2.pyi +0 -0
  184. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api_pb2_grpc.py +0 -0
  185. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/api_pb2_grpc.pyi +0 -0
  186. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/modal_api_grpc.py +0 -0
  187. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/py.typed +0 -0
  188. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router.proto +0 -0
  189. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router_grpc.py +0 -0
  190. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router_pb2.py +0 -0
  191. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router_pb2.pyi +0 -0
  192. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  193. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  194. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router.proto +0 -0
  195. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router_grpc.py +0 -0
  196. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router_pb2.py +0 -0
  197. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router_pb2.pyi +0 -0
  198. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  199. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  200. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/modal_version/__main__.py +0 -0
  201. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/pyproject.toml +0 -0
  202. {modal-1.2.5.dev4 → modal-1.2.5.dev10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev4
3
+ Version: 1.2.5.dev10
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -701,6 +701,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
701
701
  _experimental_proxy_ip: Optional[str] = None,
702
702
  _experimental_custom_scaling_factor: Optional[float] = None,
703
703
  restrict_output: bool = False,
704
+ http_config: Optional[api_pb2.HTTPConfig] = None,
704
705
  ) -> "_Function":
705
706
  """mdmd:hidden
706
707
 
@@ -1046,6 +1047,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1046
1047
  function_schema=function_schema,
1047
1048
  supported_input_formats=supported_input_formats,
1048
1049
  supported_output_formats=supported_output_formats,
1050
+ http_config=http_config,
1049
1051
  )
1050
1052
 
1051
1053
  if isinstance(gpu, list):
@@ -1083,6 +1085,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1083
1085
  untrusted=function_definition.untrusted,
1084
1086
  supported_input_formats=supported_input_formats,
1085
1087
  supported_output_formats=supported_output_formats,
1088
+ http_config=http_config,
1086
1089
  )
1087
1090
 
1088
1091
  ranked_functions = []
@@ -46,6 +46,7 @@ class _PartialFunctionFlags(enum.IntFlag):
46
46
  BATCHED = 64
47
47
  CONCURRENT = 128
48
48
  CLUSTERED = 256 # Experimental: Clustered functions
49
+ HTTP_WEB_INTERFACE = 512 # Experimental: HTTP server
49
50
 
50
51
  @staticmethod
51
52
  def all() -> int:
@@ -76,6 +77,7 @@ class _PartialFunctionParams:
76
77
  target_concurrent_inputs: Optional[int] = None
77
78
  build_timeout: Optional[int] = None
78
79
  rdma: Optional[bool] = None
80
+ http_config: Optional[api_pb2.HTTPConfig] = None
79
81
 
80
82
  def update(self, other: "_PartialFunctionParams") -> None:
81
83
  """Update self with params set in other."""
@@ -158,8 +160,9 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
158
160
  raise InvalidError("Interface decorators cannot be combined with lifecycle decorators.")
159
161
 
160
162
  has_web_interface = self.flags & _PartialFunctionFlags.WEB_INTERFACE
163
+ has_http_web_interface = self.flags & _PartialFunctionFlags.HTTP_WEB_INTERFACE
161
164
  has_callable_interface = self.flags & _PartialFunctionFlags.CALLABLE_INTERFACE
162
- if has_web_interface and has_callable_interface:
165
+ if (has_web_interface or has_http_web_interface) and has_callable_interface:
163
166
  self.registered = True # Hacky, avoid false-positive warning
164
167
  raise InvalidError("Callable decorators cannot be combined with web interface decorators.")
165
168
 
@@ -64,12 +64,10 @@ def call_lifecycle_functions(
64
64
 
65
65
 
66
66
  @contextmanager
67
- def _run_service_lifecycle(
67
+ def lifecycle_asgi(
68
68
  event_loop: UserCodeEventLoop,
69
69
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
70
- function_def: api_pb2.Function,
71
70
  finalized_functions: dict[str, FinalizedFunction],
72
- exit_callback: Optional[Callable[[], None]],
73
71
  ) -> Generator[None, None, None]:
74
72
  lifespan_background_tasks = []
75
73
  try:
@@ -82,38 +80,34 @@ def _run_service_lifecycle(
82
80
  event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
83
81
  yield
84
82
  finally:
85
- # Run exit handlers. From this point onward, ignore all SIGINT signals that come from
86
- # graceful shutdowns originating on the worker, as well as stray SIGUSR1 signals that
87
- # may have been sent to cancel inputs.
88
- int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
89
- usr1_handler = signal.signal(signal.SIGUSR1, signal.SIG_IGN)
90
83
  try:
91
- try:
92
- # run lifespan shutdown for asgi apps
93
- for finalized_function in finalized_functions.values():
94
- if finalized_function.lifespan_manager:
95
- with container_io_manager.handle_user_exception():
96
- event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
97
- finally:
98
- # no need to keep the lifespan asgi call around - we send it no more messages
99
- for task in lifespan_background_tasks:
100
- task.cancel()
101
-
102
- # Identify "exit" methods and run them.
103
- # want to make sure this is called even if the lifespan manager fails
104
- if exit_callback:
105
- exit_callback()
106
-
107
- # Finally, commit on exit to catch uncommitted volume changes and surface background
108
- # commit errors.
109
- container_io_manager.volume_commit(
110
- [v.volume_id for v in function_def.volume_mounts if v.allow_background_commits]
111
- )
84
+ # run lifespan shutdown for asgi apps
85
+ for finalized_function in finalized_functions.values():
86
+ if finalized_function.lifespan_manager:
87
+ with container_io_manager.handle_user_exception():
88
+ event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
112
89
  finally:
113
- # Restore the original signal handler, needed for container_test hygiene since the
114
- # test runs `main()` multiple times in the same process.
115
- signal.signal(signal.SIGINT, int_handler)
116
- signal.signal(signal.SIGUSR1, usr1_handler)
90
+ # no need to keep the lifespan asgi call around - we send it no more messages
91
+ for task in lifespan_background_tasks:
92
+ task.cancel()
93
+
94
+
95
+ def disable_signals():
96
+ int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
97
+ usr1_handler = signal.signal(signal.SIGUSR1, signal.SIG_IGN)
98
+ return int_handler, usr1_handler
99
+
100
+
101
+ def try_enable_signals(int_handler, usr1_handler):
102
+ if int_handler is not None and usr1_handler is not None:
103
+ signal.signal(signal.SIGINT, int_handler)
104
+ signal.signal(signal.SIGUSR1, usr1_handler)
105
+
106
+
107
+ def volume_commit(
108
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager", function_def: api_pb2.Function
109
+ ):
110
+ container_io_manager.volume_commit([v.volume_id for v in function_def.volume_mounts if v.allow_background_commits])
117
111
 
118
112
 
119
113
  class Service(metaclass=ABCMeta):
@@ -127,14 +121,30 @@ class Service(metaclass=ABCMeta):
127
121
  user_cls_instance: Any
128
122
  app: "modal.app._App"
129
123
  service_deps: Optional[Sequence["modal._object._Object"]]
124
+ function_def: api_pb2.Function
130
125
 
131
126
  @abstractmethod
132
127
  def get_finalized_functions(
133
128
  self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
134
129
  ) -> dict[str, "FinalizedFunction"]: ...
135
130
 
131
+ @abstractmethod
136
132
  @contextmanager
133
+ def lifecycle_presnapshot(
134
+ self,
135
+ event_loop: UserCodeEventLoop,
136
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
137
+ ) -> Generator[None, None, None]: ...
138
+
137
139
  @abstractmethod
140
+ @contextmanager
141
+ def lifecycle_postsnapshot(
142
+ self,
143
+ event_loop: UserCodeEventLoop,
144
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
145
+ ) -> Generator[None, None, None]: ...
146
+
147
+ @contextmanager
138
148
  def execution_context(
139
149
  self,
140
150
  event_loop: UserCodeEventLoop,
@@ -150,6 +160,34 @@ class Service(metaclass=ABCMeta):
150
160
  6. Yield finalized_functions for execution
151
161
  7. Handles cleanup (lifespan shutdown, 'exit' methods)
152
162
  """
163
+ int_handler, usr1_handler = None, None
164
+ try:
165
+ # 1. Pre-snapshot Enter
166
+ with self.lifecycle_presnapshot(event_loop, container_io_manager):
167
+ # 2. Snapshot -- If this container is being used to create a checkpoint, checkpoint the container after
168
+ # global imports and initialization. Checkpointed containers run from this point onwards.
169
+ maybe_snapshot(container_io_manager, self.function_def)
170
+ # 3. Breakpoint wrapper
171
+ create_breakpoint_wrapper(container_io_manager)
172
+ # 4. Post-snapshot Enter
173
+ with self.lifecycle_postsnapshot(event_loop, container_io_manager):
174
+ # Get Functions
175
+ with container_io_manager.handle_user_exception():
176
+ finalized_functions = self.get_finalized_functions(self.function_def, container_io_manager)
177
+ # 5. Start ASGI lifespan
178
+ with lifecycle_asgi(event_loop, container_io_manager, finalized_functions):
179
+ # 6. Yield Finalized Functions
180
+ try:
181
+ yield finalized_functions
182
+ finally:
183
+ int_handler, usr1_handler = disable_signals()
184
+ finally:
185
+ # 9. Volume commit - runs OUTSIDE all lifecycle managers so exit handlers
186
+ # have a chance to write to disk before we commit volumes
187
+ try:
188
+ volume_commit(container_io_manager, self.function_def)
189
+ finally:
190
+ try_enable_signals(int_handler, usr1_handler)
153
191
 
154
192
 
155
193
  def construct_webhook_callable(
@@ -265,27 +303,22 @@ class ImportedFunction(Service):
265
303
  }
266
304
 
267
305
  @contextmanager
268
- def execution_context(
306
+ def lifecycle_presnapshot(
269
307
  self,
270
308
  event_loop: UserCodeEventLoop,
271
309
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
272
- ) -> Generator[dict[str, "FinalizedFunction"], None, None]:
273
- # If this container is being used to create a checkpoint, checkpoint the container after
274
- # global imports and initialization. Checkpointed containers run from this point onwards.
275
- maybe_snapshot(container_io_manager, self.function_def)
276
- create_breakpoint_wrapper(container_io_manager)
277
-
278
- with container_io_manager.handle_user_exception():
279
- finalized_functions = self.get_finalized_functions(self.function_def, container_io_manager)
310
+ ):
311
+ # This is a no-op for imported functions since @enter methods are not supported
312
+ yield
280
313
 
281
- with _run_service_lifecycle(
282
- event_loop,
283
- container_io_manager,
284
- self.function_def,
285
- finalized_functions,
286
- None,
287
- ):
288
- yield finalized_functions
314
+ @contextmanager
315
+ def lifecycle_postsnapshot(
316
+ self,
317
+ event_loop: UserCodeEventLoop,
318
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
319
+ ):
320
+ # This is a no-op for imported functions since @enter methods are not supported
321
+ yield
289
322
 
290
323
 
291
324
  @dataclass
@@ -340,53 +373,38 @@ class ImportedClass(Service):
340
373
  return finalized_functions
341
374
 
342
375
  @contextmanager
343
- def execution_context(
376
+ def lifecycle_presnapshot(
344
377
  self,
345
378
  event_loop: UserCodeEventLoop,
346
379
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
347
- ) -> Generator[dict[str, "FinalizedFunction"], None, None]:
348
- # 1. Pre-snapshot Enter
380
+ ):
349
381
  # Identify all "enter" methods that need to run before we snapshot.
350
382
  if not self.function_def.is_auto_snapshot:
351
383
  pre_snapshot_methods = _find_callables_for_obj(
352
384
  self.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
353
385
  )
354
386
  call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
387
+ yield
355
388
 
356
- # 2. Snapshot
357
- # If this container is being used to create a checkpoint, checkpoint the container after
358
- # global imports and initialization. Checkpointed containers run from this point onwards.
359
- maybe_snapshot(container_io_manager, self.function_def)
360
-
361
- # 3. Breakpoint wrapper
362
- create_breakpoint_wrapper(container_io_manager)
363
-
364
- # 4. Post-snapshot Enter
389
+ @contextmanager
390
+ def lifecycle_postsnapshot(
391
+ self,
392
+ event_loop: UserCodeEventLoop,
393
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
394
+ ):
365
395
  # Identify the "enter" methods to run after resuming from a snapshot.
366
396
  if not self.function_def.is_auto_snapshot:
367
397
  post_snapshot_methods = _find_callables_for_obj(
368
398
  self.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
369
399
  )
370
400
  call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
371
-
372
- # 5. Get Functions & Start Lifespan
373
- with container_io_manager.handle_user_exception():
374
- finalized_functions = self.get_finalized_functions(self.function_def, container_io_manager)
375
-
376
- def exit_callback():
401
+ try:
402
+ yield
403
+ finally:
377
404
  if not self.function_def.is_auto_snapshot:
378
405
  exit_methods = _find_callables_for_obj(self.user_cls_instance, _PartialFunctionFlags.EXIT)
379
406
  call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
380
407
 
381
- with _run_service_lifecycle(
382
- event_loop,
383
- container_io_manager,
384
- self.function_def,
385
- finalized_functions,
386
- exit_callback,
387
- ):
388
- yield finalized_functions
389
-
390
408
 
391
409
  def get_user_class_instance(_cls: modal.cls._Cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> typing.Any:
392
410
  """Returns instance of the underlying class to be used as the `self`
@@ -75,8 +75,8 @@ def is_global_object(object_qual_name: str):
75
75
  return "<locals>" not in object_qual_name.split(".")
76
76
 
77
77
 
78
- def is_flash_object(experimental_options: Optional[dict[str, Any]]) -> bool:
79
- return experimental_options.get("flash", False) if experimental_options else False
78
+ def is_flash_object(experimental_options: Optional[dict[str, Any]], http_config: Optional[api_pb2.HTTPConfig]) -> bool:
79
+ return bool(experimental_options and experimental_options.get("flash", False)) or http_config is not None
80
80
 
81
81
 
82
82
  def is_method_fn(object_qual_name: str):
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
3
  import contextlib
4
+ import os
4
5
  import platform
5
6
  import socket
6
7
  import time
@@ -396,7 +397,8 @@ async def _retry_transient_errors(
396
397
  ):
397
398
  last_server_retry_warning_time = now
398
399
  logger.warning(
399
- f"Warning: Received {exc.status} status: {exc.message}. "
400
+ f"Warning: Received {exc.status}{os.linesep}"
401
+ f"{exc.message}{os.linesep}"
400
402
  f"Will retry in {server_delay:0.2f} seconds."
401
403
  )
402
404
 
@@ -5,7 +5,7 @@ import json
5
5
  import ssl
6
6
  import time
7
7
  import urllib.parse
8
- from typing import AsyncIterator, Optional
8
+ from typing import AsyncGenerator, Optional
9
9
 
10
10
  import grpclib.client
11
11
  import grpclib.config
@@ -18,6 +18,7 @@ from modal.exception import ExecTimeoutError
18
18
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
19
19
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
20
20
 
21
+ from .async_utils import aclosing
21
22
  from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES, connect_channel
22
23
 
23
24
 
@@ -242,7 +243,7 @@ class TaskCommandRouterClient:
242
243
  # Quotes around the type required for protobuf 3.19.
243
244
  file_descriptor: "api_pb2.FileDescriptor.ValueType",
244
245
  deadline: Optional[float] = None,
245
- ) -> AsyncIterator[sr_pb2.TaskExecStdioReadResponse]:
246
+ ) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
246
247
  """Stream stdout/stderr batches from the task, properly retrying on transient errors.
247
248
 
248
249
  Args:
@@ -253,7 +254,7 @@ class TaskCommandRouterClient:
253
254
  None, wait forever. If the deadline is exceeded, raises an
254
255
  ExecTimeoutError.
255
256
  Returns:
256
- AsyncIterator[sr_pb2.TaskExecStdioReadResponse]: A stream of stdout/stderr batches.
257
+ AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]: A stream of stdout/stderr batches.
257
258
  Raises:
258
259
  ExecTimeoutError: If the deadline is exceeded.
259
260
  Other errors: If retries are exhausted on transient errors or if there's an error
@@ -268,8 +269,9 @@ class TaskCommandRouterClient:
268
269
  else:
269
270
  raise ValueError(f"Invalid file descriptor: {file_descriptor}")
270
271
 
271
- async for item in self._stream_stdio(task_id, exec_id, sr_fd, deadline):
272
- yield item
272
+ async with aclosing(self._stream_stdio(task_id, exec_id, sr_fd, deadline)) as stream:
273
+ async for item in stream:
274
+ yield item
273
275
 
274
276
  async def exec_stdin_write(
275
277
  self, task_id: str, exec_id: str, offset: int, data: bytes, eof: bool
@@ -453,7 +455,7 @@ class TaskCommandRouterClient:
453
455
  # Quotes around the type required for protobuf 3.19.
454
456
  file_descriptor: "sr_pb2.TaskExecStdioFileDescriptor.ValueType",
455
457
  deadline: Optional[float] = None,
456
- ) -> AsyncIterator[sr_pb2.TaskExecStdioReadResponse]:
458
+ ) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
457
459
  """Stream stdio from the task, properly updating the offset and retrying on transient errors.
458
460
  Raises ExecTimeoutError if the deadline is exceeded.
459
461
  """
@@ -827,7 +827,7 @@ class _App:
827
827
  batch_max_size = f.params.batch_max_size
828
828
  batch_wait_ms = f.params.batch_wait_ms
829
829
  if f.flags & _PartialFunctionFlags.CONCURRENT:
830
- verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options))
830
+ verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options, None))
831
831
  max_concurrent_inputs = f.params.max_concurrent_inputs
832
832
  target_concurrent_inputs = f.params.target_concurrent_inputs
833
833
  else:
@@ -1025,11 +1025,17 @@ class _App:
1025
1025
  def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
1026
1026
  local_state = self._local_state
1027
1027
  # Check if the decorated object is a class
1028
+ http_config = None
1028
1029
  if isinstance(wrapped_cls, _PartialFunction):
1029
1030
  wrapped_cls.registered = True
1030
1031
  user_cls = wrapped_cls.user_cls
1032
+ if wrapped_cls.flags & _PartialFunctionFlags.HTTP_WEB_INTERFACE:
1033
+ http_config = wrapped_cls.params.http_config
1031
1034
  if wrapped_cls.flags & _PartialFunctionFlags.CONCURRENT:
1032
- verify_concurrent_params(params=wrapped_cls.params, is_flash=is_flash_object(experimental_options))
1035
+ verify_concurrent_params(
1036
+ params=wrapped_cls.params,
1037
+ is_flash=is_flash_object(experimental_options or {}, http_config=http_config),
1038
+ )
1033
1039
  max_concurrent_inputs = wrapped_cls.params.max_concurrent_inputs
1034
1040
  target_concurrent_inputs = wrapped_cls.params.target_concurrent_inputs
1035
1041
  else:
@@ -1039,6 +1045,7 @@ class _App:
1039
1045
  if wrapped_cls.flags & _PartialFunctionFlags.CLUSTERED:
1040
1046
  cluster_size = wrapped_cls.params.cluster_size
1041
1047
  rdma = wrapped_cls.params.rdma
1048
+
1042
1049
  else:
1043
1050
  cluster_size = None
1044
1051
  rdma = None
@@ -1083,10 +1090,17 @@ class _App:
1083
1090
  "The `@modal.concurrent` decorator cannot be used on methods; decorate the class instead."
1084
1091
  )
1085
1092
 
1093
+ for method in _find_partial_methods_for_user_cls(
1094
+ user_cls, _PartialFunctionFlags.HTTP_WEB_INTERFACE
1095
+ ).values():
1096
+ method.registered = True # Avoid warning about not registering the method (hacky!)
1097
+ raise InvalidError(
1098
+ "The `@modal.http_server` decorator cannot be used on methods; decorate the class instead."
1099
+ )
1100
+
1086
1101
  info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)
1087
1102
 
1088
1103
  i6pn_enabled = i6pn or cluster_size is not None
1089
-
1090
1104
  cls_func = _Function.from_local(
1091
1105
  info,
1092
1106
  app=self,
@@ -1117,6 +1131,7 @@ class _App:
1117
1131
  block_network=block_network,
1118
1132
  restrict_modal_access=restrict_modal_access,
1119
1133
  max_inputs=max_inputs,
1134
+ http_config=http_config,
1120
1135
  i6pn_enabled=i6pn_enabled,
1121
1136
  cluster_size=cluster_size,
1122
1137
  rdma=rdma,
@@ -1130,6 +1145,7 @@ class _App:
1130
1145
  self._add_function(cls_func, is_web_endpoint=False)
1131
1146
 
1132
1147
  cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
1148
+
1133
1149
  for method_name, partial_function in cls._method_partials.items():
1134
1150
  if partial_function.params.webhook_config is not None:
1135
1151
  full_name = f"{user_cls.__name__}.{method_name}"
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.5.dev4",
35
+ version: str = "1.2.5.dev10",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.5.dev4",
166
+ version: str = "1.2.5.dev10",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -19,7 +19,12 @@ from ..cls import _Cls
19
19
  from ..exception import InvalidError
20
20
  from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
21
21
  from ..secret import _Secret
22
- from .flash import flash_forward, flash_get_containers, flash_prometheus_autoscaler # noqa: F401
22
+ from .flash import ( # noqa: F401
23
+ flash_forward,
24
+ flash_get_containers,
25
+ flash_prometheus_autoscaler,
26
+ http_server,
27
+ )
23
28
 
24
29
 
25
30
  def stop_fetching_inputs():
@@ -7,9 +7,10 @@ import sys
7
7
  import time
8
8
  import traceback
9
9
  from collections import defaultdict
10
- from typing import Any, Optional
10
+ from typing import Any, Callable, Optional, Union
11
11
  from urllib.parse import urlparse
12
12
 
13
+ from modal._partial_function import _PartialFunctionFlags
13
14
  from modal.cls import _Cls
14
15
  from modal.dict import _Dict
15
16
  from modal_proto import api_pb2
@@ -28,15 +29,17 @@ class _FlashManager:
28
29
  self,
29
30
  client: _Client,
30
31
  port: int,
31
- process: Optional[subprocess.Popen] = None,
32
+ process: Optional[subprocess.Popen] = None, # to be deprecated
32
33
  health_check_url: Optional[str] = None,
34
+ startup_timeout: int = 30,
33
35
  h2_enabled: bool = False,
34
36
  ):
35
37
  self.client = client
36
38
  self.port = port
39
+ self.process = process
37
40
  # Health check is not currently being used
38
41
  self.health_check_url = health_check_url
39
- self.process = process
42
+ self.startup_timeout = startup_timeout
40
43
  self.tunnel_manager = _forward_tunnel(port, h2_enabled=h2_enabled, client=client)
41
44
  self.stopped = False
42
45
  self.num_failures = 0
@@ -49,10 +52,15 @@ class _FlashManager:
49
52
 
50
53
  start_time = time.monotonic()
51
54
 
55
+ def check_process_is_running() -> Optional[Exception]:
56
+ if process is not None and process.poll() is not None:
57
+ return Exception(f"Process {process.pid} exited with code {process.returncode}")
58
+ return None
59
+
52
60
  while time.monotonic() - start_time < timeout:
53
61
  try:
54
- if process is not None and process.poll() is not None:
55
- return False, Exception(f"Process {process.pid} exited with code {process.returncode}")
62
+ if error := check_process_is_running():
63
+ return False, error
56
64
  with socket.create_connection(("localhost", self.port), timeout=0.5):
57
65
  return True, None
58
66
  except (ConnectionRefusedError, OSError):
@@ -101,6 +109,7 @@ class _FlashManager:
101
109
 
102
110
  async def _run_heartbeat(self, host: str, port: int):
103
111
  first_registration = True
112
+ start_time = time.monotonic()
104
113
  while True:
105
114
  try:
106
115
  port_check_resp, port_check_error = await self.is_port_connection_healthy(process=self.process)
@@ -122,12 +131,15 @@ class _FlashManager:
122
131
  )
123
132
  first_registration = False
124
133
  else:
125
- logger.error(
126
- f"[Modal Flash] Deregistering container {self.task_id} on {self.tunnel.url} "
127
- f"due to error: {port_check_error}, num_failures: {self.num_failures}"
128
- )
129
- self.num_failures += 1
130
- await self.client.stub.FlashContainerDeregister(api_pb2.FlashContainerDeregisterRequest())
134
+ if first_registration and (time.monotonic() - start_time < self.startup_timeout):
135
+ continue
136
+ else:
137
+ logger.error(
138
+ f"[Modal Flash] Deregistering container {self.task_id} on {self.tunnel.url} "
139
+ f"due to error: {port_check_error}, num_failures: {self.num_failures}"
140
+ )
141
+ self.num_failures += 1
142
+ await self.client.stub.FlashContainerDeregister(api_pb2.FlashContainerDeregisterRequest())
131
143
 
132
144
  except asyncio.CancelledError:
133
145
  logger.warning("[Modal Flash] Shutting down...")
@@ -171,6 +183,7 @@ async def flash_forward(
171
183
  port: int,
172
184
  process: Optional[subprocess.Popen] = None,
173
185
  health_check_url: Optional[str] = None,
186
+ startup_timeout: int = 30,
174
187
  h2_enabled: bool = False,
175
188
  ) -> _FlashManager:
176
189
  """
@@ -180,7 +193,14 @@ async def flash_forward(
180
193
  """
181
194
  client = await _Client.from_env()
182
195
 
183
- manager = _FlashManager(client, port, process=process, health_check_url=health_check_url, h2_enabled=h2_enabled)
196
+ manager = _FlashManager(
197
+ client,
198
+ port,
199
+ process=process,
200
+ health_check_url=health_check_url,
201
+ startup_timeout=startup_timeout,
202
+ h2_enabled=h2_enabled,
203
+ )
184
204
  await manager._start()
185
205
  return manager
186
206
 
@@ -618,3 +638,57 @@ async def flash_get_containers(app_name: str, cls_name: str) -> list[dict[str, A
618
638
  req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
619
639
  resp = await client.stub.FlashContainerList(req)
620
640
  return resp.containers
641
+
642
+
643
+ def _http_server(
644
+ port: Optional[int] = None,
645
+ *,
646
+ proxy_regions: list[str] = [], # The regions to proxy the HTTP server to.
647
+ startup_timeout: int = 30, # Maximum number of seconds to wait for the HTTP server to start.
648
+ exit_grace_period: Optional[int] = None, # The time to wait for the HTTP server to exit gracefully.
649
+ ):
650
+ """Decorator for Flash-enabled HTTP servers on Modal classes.
651
+
652
+ Args:
653
+ port: The local port to forward to the HTTP server.
654
+ proxy_regions: The regions to proxy the HTTP server to.
655
+ startup_timeout: The maximum time to wait for the HTTP server to start.
656
+ exit_grace_period: The time to wait for the HTTP server to exit gracefully.
657
+
658
+ """
659
+ if port is None:
660
+ raise InvalidError(
661
+ "Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@modal.http_server()`."
662
+ )
663
+ if not isinstance(port, int) or port < 1 or port > 65535:
664
+ raise InvalidError("First argument of `@http_server` must be a local port, such as `@http_server(8000)`.")
665
+ if startup_timeout <= 0:
666
+ raise InvalidError("The `startup_timeout` argument of `@http_server` must be positive.")
667
+ if exit_grace_period is not None and exit_grace_period < 0:
668
+ raise InvalidError("The `exit_grace_period` argument of `@http_server` must be non-negative.")
669
+
670
+ from modal._partial_function import _PartialFunction, _PartialFunctionParams
671
+
672
+ params = _PartialFunctionParams(
673
+ http_config=api_pb2.HTTPConfig(
674
+ port=port,
675
+ proxy_regions=proxy_regions,
676
+ startup_timeout=startup_timeout or 0,
677
+ exit_grace_period=exit_grace_period or 0,
678
+ )
679
+ )
680
+
681
+ def wrapper(obj: Union[Callable[..., Any], _PartialFunction]) -> _PartialFunction:
682
+ flags = _PartialFunctionFlags.HTTP_WEB_INTERFACE
683
+
684
+ if isinstance(obj, _PartialFunction):
685
+ pf = obj.stack(flags, params)
686
+ else:
687
+ pf = _PartialFunction(obj, flags, params)
688
+ pf.validate_obj_compatibility("`http_server`")
689
+ return pf
690
+
691
+ return wrapper
692
+
693
+
694
+ http_server = synchronize_api(_http_server, target_module=__name__)