modal 1.1.4.dev15__tar.gz → 1.2.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.

Potentially problematic release.


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

Files changed (214) hide show
  1. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/PKG-INFO +7 -6
  2. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/README.md +4 -4
  3. modal-1.2.5.dev4/modal/_billing.py +80 -0
  4. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_clustered_functions.py +1 -3
  5. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_container_entrypoint.py +53 -249
  6. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_functions.py +218 -172
  7. modal-1.2.5.dev4/modal/_grpc_client.py +171 -0
  8. modal-1.2.5.dev4/modal/_load_context.py +106 -0
  9. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_object.py +54 -19
  10. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_output.py +44 -20
  11. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_partial_function.py +29 -4
  12. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_pty.py +7 -3
  13. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_resolver.py +21 -35
  14. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/asgi.py +1 -1
  15. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/container_io_manager.py +282 -173
  16. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/container_io_manager.pyi +32 -48
  17. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/gpu_memory_snapshot.py +20 -17
  18. modal-1.2.5.dev4/modal/_runtime/user_code_event_loop.py +80 -0
  19. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/user_code_imports.py +237 -67
  20. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_serialization.py +57 -1
  21. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_tunnel.py +11 -2
  22. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_tunnel.pyi +25 -3
  23. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/async_utils.py +27 -8
  24. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/auth_token_manager.py +1 -4
  25. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/blob_utils.py +11 -8
  26. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/function_utils.py +32 -10
  27. modal-1.2.5.dev4/modal/_utils/grpc_utils.py +455 -0
  28. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/mount_utils.py +26 -1
  29. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/name_utils.py +17 -3
  30. modal-1.2.5.dev4/modal/_utils/task_command_router_client.py +536 -0
  31. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/app.py +253 -92
  32. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/app.pyi +227 -46
  33. modal-1.2.5.dev4/modal/billing.py +5 -0
  34. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/_download.py +19 -3
  35. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/cluster.py +11 -3
  36. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/config.py +3 -1
  37. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/container.py +5 -4
  38. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/dict.py +5 -2
  39. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/entry_point.py +3 -2
  40. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/environment.py +5 -4
  41. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/launch.py +1 -2
  42. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/network_file_system.py +1 -4
  43. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/programs/run_jupyter.py +1 -1
  44. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/programs/run_marimo.py +1 -1
  45. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/programs/vscode.py +1 -1
  46. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/queues.py +5 -4
  47. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/run.py +10 -187
  48. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/secret.py +1 -2
  49. modal-1.2.5.dev4/modal/cli/shell.py +237 -0
  50. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/volume.py +7 -1
  51. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/client.py +14 -124
  52. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/client.pyi +8 -98
  53. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cloud_bucket_mount.py +5 -3
  54. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cloud_bucket_mount.pyi +5 -1
  55. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cls.py +123 -103
  56. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cls.pyi +37 -85
  57. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/config.py +27 -3
  58. modal-1.2.5.dev4/modal/container_process.py +472 -0
  59. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/container_process.pyi +95 -32
  60. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/dict.py +40 -59
  61. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/dict.pyi +24 -83
  62. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/environments.py +15 -27
  63. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/environments.pyi +5 -15
  64. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/exception.py +4 -0
  65. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/experimental/__init__.py +30 -42
  66. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/experimental/flash.py +230 -78
  67. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/experimental/flash.pyi +122 -9
  68. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/file_io.py +13 -27
  69. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/functions.pyi +37 -63
  70. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/image.py +208 -70
  71. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/image.pyi +239 -33
  72. modal-1.2.5.dev4/modal/io_streams.py +846 -0
  73. modal-1.2.5.dev4/modal/io_streams.pyi +530 -0
  74. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/mount.py +35 -143
  75. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/mount.pyi +25 -169
  76. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/network_file_system.py +22 -51
  77. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/network_file_system.pyi +16 -76
  78. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/object.pyi +35 -8
  79. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/parallel_map.py +59 -52
  80. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/parallel_map.pyi +4 -9
  81. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/partial_function.pyi +4 -1
  82. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/proxy.py +14 -6
  83. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/proxy.pyi +10 -2
  84. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/queue.py +30 -61
  85. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/queue.pyi +12 -76
  86. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/runner.py +112 -92
  87. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/runner.pyi +45 -27
  88. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/sandbox.py +337 -117
  89. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/sandbox.pyi +368 -41
  90. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/secret.py +61 -73
  91. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/secret.pyi +21 -42
  92. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/serving.py +7 -11
  93. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/serving.pyi +7 -8
  94. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/snapshot.py +11 -8
  95. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/token_flow.py +1 -1
  96. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/volume.py +68 -73
  97. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/volume.pyi +32 -77
  98. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/PKG-INFO +7 -6
  99. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/SOURCES.txt +19 -7
  100. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/requires.txt +1 -0
  101. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api.proto +249 -53
  102. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api_grpc.py +130 -1
  103. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api_pb2.py +1265 -1040
  104. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api_pb2.pyi +736 -51
  105. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api_pb2_grpc.py +266 -2
  106. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/api_pb2_grpc.pyi +86 -6
  107. modal-1.2.5.dev4/modal_proto/modal_api_grpc.py +194 -0
  108. modal-1.2.5.dev4/modal_proto/sandbox_router.proto +145 -0
  109. modal-1.2.5.dev4/modal_proto/sandbox_router_grpc.py +105 -0
  110. modal-1.2.5.dev4/modal_proto/sandbox_router_pb2.py +149 -0
  111. modal-1.2.5.dev4/modal_proto/sandbox_router_pb2.pyi +333 -0
  112. modal-1.2.5.dev4/modal_proto/sandbox_router_pb2_grpc.py +203 -0
  113. modal-1.2.5.dev4/modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  114. modal-1.2.5.dev4/modal_proto/task_command_router.proto +144 -0
  115. modal-1.2.5.dev4/modal_proto/task_command_router_grpc.py +105 -0
  116. modal-1.2.5.dev4/modal_proto/task_command_router_pb2.py +149 -0
  117. modal-1.2.5.dev4/modal_proto/task_command_router_pb2.pyi +333 -0
  118. modal-1.2.5.dev4/modal_proto/task_command_router_pb2_grpc.py +203 -0
  119. modal-1.2.5.dev4/modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  120. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_version/__init__.py +1 -1
  121. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/pyproject.toml +19 -3
  122. modal-1.1.4.dev15/modal/_utils/grpc_utils.py +0 -293
  123. modal-1.1.4.dev15/modal/container_process.py +0 -194
  124. modal-1.1.4.dev15/modal/io_streams.py +0 -467
  125. modal-1.1.4.dev15/modal/io_streams.pyi +0 -440
  126. modal-1.1.4.dev15/modal_proto/modal_api_grpc.py +0 -186
  127. modal-1.1.4.dev15/modal_proto/modal_options_grpc.py +0 -3
  128. modal-1.1.4.dev15/modal_proto/options.proto +0 -19
  129. modal-1.1.4.dev15/modal_proto/options_grpc.py +0 -3
  130. modal-1.1.4.dev15/modal_proto/options_pb2.py +0 -35
  131. modal-1.1.4.dev15/modal_proto/options_pb2.pyi +0 -20
  132. modal-1.1.4.dev15/modal_proto/options_pb2_grpc.py +0 -4
  133. modal-1.1.4.dev15/modal_proto/options_pb2_grpc.pyi +0 -7
  134. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/LICENSE +0 -0
  135. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/__init__.py +0 -0
  136. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/__main__.py +0 -0
  137. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_clustered_functions.pyi +0 -0
  138. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_ipython.py +0 -0
  139. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_location.py +0 -0
  140. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_resources.py +0 -0
  141. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/__init__.py +0 -0
  142. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/execution_context.py +0 -0
  143. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/execution_context.pyi +0 -0
  144. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_runtime/telemetry.py +0 -0
  145. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_traceback.py +0 -0
  146. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_type_manager.py +0 -0
  147. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/__init__.py +0 -0
  148. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/app_utils.py +0 -0
  149. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/bytes_io_segment_payload.py +0 -0
  150. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/deprecation.py +0 -0
  151. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/docker_utils.py +0 -0
  152. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/git_utils.py +0 -0
  153. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/grpc_testing.py +0 -0
  154. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/hash_utils.py +0 -0
  155. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/http_utils.py +0 -0
  156. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/jwt_utils.py +0 -0
  157. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/logger.py +0 -0
  158. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/package_utils.py +0 -0
  159. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/pattern_utils.py +0 -0
  160. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/rand_pb_testing.py +0 -0
  161. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/shell_utils.py +0 -0
  162. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_utils/time_utils.py +0 -0
  163. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_vendor/__init__.py +0 -0
  164. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  165. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_vendor/cloudpickle.py +0 -0
  166. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_vendor/tblib.py +0 -0
  167. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/_watcher.py +0 -0
  168. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/2023.12.312.txt +0 -0
  169. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/2023.12.txt +0 -0
  170. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/2024.04.txt +0 -0
  171. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/2024.10.txt +0 -0
  172. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/2025.06.txt +0 -0
  173. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/PREVIEW.txt +0 -0
  174. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/README.md +0 -0
  175. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/builder/base-images.json +0 -0
  176. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/call_graph.py +0 -0
  177. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/__init__.py +0 -0
  178. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/_traceback.py +0 -0
  179. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/app.py +0 -0
  180. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/import_refs.py +0 -0
  181. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/profile.py +0 -0
  182. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/programs/__init__.py +0 -0
  183. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/programs/launch_instance_ssh.py +0 -0
  184. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/token.py +0 -0
  185. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/cli/utils.py +0 -0
  186. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/experimental/ipython.py +0 -0
  187. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/file_io.pyi +0 -0
  188. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/file_pattern_matcher.py +0 -0
  189. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/functions.py +0 -0
  190. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/gpu.py +0 -0
  191. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/object.py +0 -0
  192. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/output.py +0 -0
  193. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/partial_function.py +0 -0
  194. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/py.typed +0 -0
  195. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/retries.py +0 -0
  196. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/running_app.py +0 -0
  197. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/schedule.py +0 -0
  198. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/scheduler_placement.py +0 -0
  199. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/snapshot.pyi +0 -0
  200. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/stream_type.py +0 -0
  201. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal/token_flow.pyi +0 -0
  202. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/dependency_links.txt +0 -0
  203. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/entry_points.txt +0 -0
  204. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal.egg-info/top_level.txt +0 -0
  205. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/__init__.py +0 -0
  206. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/gen_cli_docs.py +0 -0
  207. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/gen_reference_docs.py +0 -0
  208. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/mdmd/__init__.py +0 -0
  209. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/mdmd/mdmd.py +0 -0
  210. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_docs/mdmd/signatures.py +0 -0
  211. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/__init__.py +0 -0
  212. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_proto/py.typed +0 -0
  213. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/modal_version/__main__.py +0 -0
  214. {modal-1.1.4.dev15 → modal-1.2.5.dev4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.4.dev15
3
+ Version: 1.2.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
@@ -13,10 +13,11 @@ Classifier: Topic :: System :: Distributed Computing
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Requires-Python: >=3.9
16
+ Requires-Python: <3.14,>=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: aiohttp
20
+ Requires-Dist: cbor2
20
21
  Requires-Dist: certifi
21
22
  Requires-Dist: click~=8.1
22
23
  Requires-Dist: grpclib<0.4.9,>=0.4.7
@@ -31,14 +32,14 @@ Requires-Dist: watchfiles
31
32
  Requires-Dist: typing_extensions~=4.6
32
33
  Dynamic: license-file
33
34
 
34
- # Modal Python Library
35
+ # Modal Python SDK
35
36
 
36
37
  [![PyPI Version](https://img.shields.io/pypi/v/modal.svg)](https://pypi.org/project/modal/)
37
38
  [![License](https://img.shields.io/badge/license-apache_2.0-darkviolet.svg)](https://github.com/modal-labs/modal-client/blob/master/LICENSE)
38
39
  [![Tests](https://github.com/modal-labs/modal-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/modal-labs/modal-client/actions/workflows/ci-cd.yml)
39
40
  [![Slack](https://img.shields.io/badge/slack-join-blue.svg?logo=slack)](https://modal.com/slack)
40
41
 
41
- The [Modal](https://modal.com/) Python library provides convenient, on-demand
42
+ The [Modal](https://modal.com/) Python SDK provides convenient, on-demand
42
43
  access to serverless cloud compute from Python scripts on your local computer.
43
44
 
44
45
  ## Documentation
@@ -52,10 +53,10 @@ a [user guide](https://modal.com/docs/guide), and the detailed
52
53
 
53
54
  **This library requires Python 3.9 – 3.13.**
54
55
 
55
- Install the package with `pip`:
56
+ Install the package with `uv` or `pip`:
56
57
 
57
58
  ```bash
58
- pip install modal
59
+ uv pip install modal
59
60
  ```
60
61
 
61
62
  You can create a Modal account (or link your existing one) directly on the
@@ -1,11 +1,11 @@
1
- # Modal Python Library
1
+ # Modal Python SDK
2
2
 
3
3
  [![PyPI Version](https://img.shields.io/pypi/v/modal.svg)](https://pypi.org/project/modal/)
4
4
  [![License](https://img.shields.io/badge/license-apache_2.0-darkviolet.svg)](https://github.com/modal-labs/modal-client/blob/master/LICENSE)
5
5
  [![Tests](https://github.com/modal-labs/modal-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/modal-labs/modal-client/actions/workflows/ci-cd.yml)
6
6
  [![Slack](https://img.shields.io/badge/slack-join-blue.svg?logo=slack)](https://modal.com/slack)
7
7
 
8
- The [Modal](https://modal.com/) Python library provides convenient, on-demand
8
+ The [Modal](https://modal.com/) Python SDK provides convenient, on-demand
9
9
  access to serverless cloud compute from Python scripts on your local computer.
10
10
 
11
11
  ## Documentation
@@ -19,10 +19,10 @@ a [user guide](https://modal.com/docs/guide), and the detailed
19
19
 
20
20
  **This library requires Python 3.9 – 3.13.**
21
21
 
22
- Install the package with `pip`:
22
+ Install the package with `uv` or `pip`:
23
23
 
24
24
  ```bash
25
- pip install modal
25
+ uv pip install modal
26
26
  ```
27
27
 
28
28
  You can create a Modal account (or link your existing one) directly on the
@@ -0,0 +1,80 @@
1
+ # Copyright Modal Labs 2025
2
+ from datetime import datetime, timezone
3
+ from decimal import Decimal
4
+ from typing import Any, Optional, TypedDict
5
+
6
+ from modal_proto import api_pb2
7
+
8
+ from .client import _Client
9
+ from .exception import InvalidError
10
+
11
+
12
+ class WorkspaceBillingReportItem(TypedDict):
13
+ object_id: str
14
+ description: str
15
+ environment_name: str
16
+ interval_start: datetime
17
+ cost: Decimal
18
+ tags: dict[str, str]
19
+
20
+
21
+ async def _workspace_billing_report(
22
+ *,
23
+ start: datetime, # Start of the report, inclusive
24
+ end: Optional[datetime] = None, # End of the report, exclusive
25
+ resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
26
+ tag_names: Optional[list[str]] = None, # Optional additional metadata to include
27
+ client: Optional[_Client] = None,
28
+ ) -> list[dict[str, Any]]:
29
+ """Generate a tabular report of workspace usage by object and time.
30
+
31
+ The result will be a list of dictionaries for each interval (determined by `resolution`)
32
+ between the `start` and `end` limits. The dictionary represents a single Modal object
33
+ that billing can be attributed to (e.g., an App) along with metadata (including user-defined
34
+ tags) for identifying that object.
35
+
36
+ The `start` and `end` parameters are required to either have a UTC timezone or to be
37
+ timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
38
+ be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
39
+ parameters are partial: `start` will be rounded to the beginning of its interval, while
40
+ partial `end` intervals will be excluded.
41
+
42
+ Additional user-provided metadata can be included in the report if the objects have tags
43
+ and `tag_names` (i.e., keys) are specified in the request. Note that tags will be attributed
44
+ to the entire interval even if they were added or removed at some point within it.
45
+
46
+ """
47
+ if client is None:
48
+ client = await _Client.from_env()
49
+
50
+ tag_names = tag_names or []
51
+
52
+ if end is None:
53
+ end = datetime.now(timezone.utc)
54
+
55
+ for dt in (start, end):
56
+ if dt.tzinfo is None:
57
+ dt = dt.replace(tzinfo=timezone.utc)
58
+ elif dt.tzinfo != timezone.utc:
59
+ raise InvalidError("Timezone-aware start/end limits must be in UTC.")
60
+
61
+ request = api_pb2.WorkspaceBillingReportRequest(
62
+ resolution=resolution,
63
+ tag_names=tag_names,
64
+ )
65
+ request.start_timestamp.FromDatetime(start)
66
+ request.end_timestamp.FromDatetime(end)
67
+
68
+ rows = []
69
+ async for pb_item in client.stub.WorkspaceBillingReport.unary_stream(request):
70
+ item = {
71
+ "object_id": pb_item.object_id,
72
+ "description": pb_item.description,
73
+ "environment_name": pb_item.environment_name,
74
+ "interval_start": pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
75
+ "cost": Decimal(pb_item.cost),
76
+ "tags": dict(pb_item.tags),
77
+ }
78
+ rows.append(item)
79
+
80
+ return rows
@@ -5,7 +5,6 @@ from dataclasses import dataclass
5
5
  from typing import Optional
6
6
 
7
7
  from modal._utils.async_utils import synchronize_api
8
- from modal._utils.grpc_utils import retry_transient_errors
9
8
  from modal.client import _Client
10
9
  from modal.exception import InvalidError
11
10
  from modal_proto import api_pb2
@@ -61,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
61
60
  os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
62
61
 
63
62
  if world_size > 1:
64
- resp: api_pb2.TaskClusterHelloResponse = await retry_transient_errors(
65
- client.stub.TaskClusterHello,
63
+ resp = await client.stub.TaskClusterHello(
66
64
  api_pb2.TaskClusterHelloRequest(
67
65
  task_id=task_id,
68
66
  container_ip=container_ip,
@@ -15,31 +15,23 @@ if telemetry_socket:
15
15
  instrument_imports(telemetry_socket)
16
16
 
17
17
  import asyncio
18
- import inspect
19
18
  import queue
20
19
  import signal
21
- import sys
22
20
  import threading
23
21
  import time
24
- from collections.abc import Sequence
25
- from typing import TYPE_CHECKING, Any, Callable, Optional
22
+ import types
23
+ from typing import TYPE_CHECKING, Any, Optional, cast
26
24
 
27
25
  from google.protobuf.message import Message
28
26
 
29
27
  from modal._clustered_functions import initialize_clustered_function
30
- from modal._partial_function import (
31
- _find_callables_for_obj,
32
- _PartialFunctionFlags,
33
- )
28
+ from modal._runtime.user_code_event_loop import UserCodeEventLoop
34
29
  from modal._serialization import deserialize, deserialize_params
35
- from modal._utils.async_utils import TaskContext, synchronizer
36
- from modal._utils.function_utils import (
37
- callable_has_non_self_params,
38
- )
30
+ from modal._utils.async_utils import TaskContext, aclosing, synchronizer
39
31
  from modal.app import App, _App
40
32
  from modal.client import Client, _Client
41
33
  from modal.config import logger
42
- from modal.exception import ExecutionError, InputCancellation, InvalidError
34
+ from modal.exception import ExecutionError, InputCancellation
43
35
  from modal.running_app import RunningApp, running_app_from_layout
44
36
  from modal_proto import api_pb2
45
37
 
@@ -53,6 +45,7 @@ from ._runtime.container_io_manager import (
53
45
  if TYPE_CHECKING:
54
46
  import modal._object
55
47
  import modal._runtime.container_io_manager
48
+ import modal._runtime.user_code_imports
56
49
 
57
50
 
58
51
  class DaemonizedThreadPool:
@@ -101,81 +94,6 @@ class DaemonizedThreadPool:
101
94
  self.inputs.put((func, args))
102
95
 
103
96
 
104
- class UserCodeEventLoop:
105
- """Run an async event loop as a context manager and handle signals.
106
-
107
- This will run all *user supplied* async code, i.e. async functions, as well as async enter/exit managers
108
-
109
- The following signals are handled while a coroutine is running on the event loop until
110
- completion (and then handlers are deregistered):
111
-
112
- - `SIGUSR1`: converted to an async task cancellation. Note that this only affects the event
113
- loop, and the signal handler defined here doesn't run for sync functions.
114
- - `SIGINT`: Unless the global signal handler has been set to SIGIGN, the loop's signal handler
115
- is set to cancel the current task and raise KeyboardInterrupt to the caller.
116
- """
117
-
118
- def __enter__(self):
119
- self.loop = asyncio.new_event_loop()
120
- self.tasks = set()
121
- return self
122
-
123
- def __exit__(self, exc_type, exc_value, traceback):
124
- self.loop.run_until_complete(self.loop.shutdown_asyncgens())
125
- if sys.version_info[:2] >= (3, 9):
126
- self.loop.run_until_complete(self.loop.shutdown_default_executor()) # Introduced in Python 3.9
127
-
128
- for task in self.tasks:
129
- task.cancel()
130
-
131
- self.loop.close()
132
-
133
- def create_task(self, coro):
134
- task = self.loop.create_task(coro)
135
- self.tasks.add(task)
136
- task.add_done_callback(self.tasks.discard)
137
- return task
138
-
139
- def run(self, coro):
140
- task = asyncio.ensure_future(coro, loop=self.loop)
141
- self._sigints = 0
142
-
143
- def _sigint_handler():
144
- # cancel the task in order to have run_until_complete return soon and
145
- # prevent a bunch of unwanted tracebacks when shutting down the
146
- # event loop.
147
-
148
- # this basically replicates the sigint handler installed by asyncio.run()
149
- self._sigints += 1
150
- if self._sigints == 1:
151
- # first sigint is graceful
152
- task.cancel()
153
- return
154
-
155
- # this should normally not happen, but the second sigint would "hard kill" the event loop!
156
- raise KeyboardInterrupt()
157
-
158
- ignore_sigint = signal.getsignal(signal.SIGINT) == signal.SIG_IGN
159
- if not ignore_sigint:
160
- self.loop.add_signal_handler(signal.SIGINT, _sigint_handler)
161
-
162
- # Before Python 3.9 there is no argument to Task.cancel
163
- if sys.version_info[:2] >= (3, 9):
164
- self.loop.add_signal_handler(signal.SIGUSR1, task.cancel, "Input was cancelled by user")
165
- else:
166
- self.loop.add_signal_handler(signal.SIGUSR1, task.cancel)
167
-
168
- try:
169
- return self.loop.run_until_complete(task)
170
- except asyncio.CancelledError:
171
- if self._sigints > 0:
172
- raise KeyboardInterrupt()
173
- finally:
174
- self.loop.remove_signal_handler(signal.SIGUSR1)
175
- if not ignore_sigint:
176
- self.loop.remove_signal_handler(signal.SIGINT)
177
-
178
-
179
97
  def call_function(
180
98
  user_code_event_loop: UserCodeEventLoop,
181
99
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
@@ -184,17 +102,13 @@ def call_function(
184
102
  batch_wait_ms: int,
185
103
  ):
186
104
  async def run_input_async(io_context: IOContext) -> None:
187
- started_at = time.time()
188
105
  reset_context = execution_context._set_current_context_ids(
189
106
  io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
190
107
  )
108
+ started_at = time.time()
191
109
  async with container_io_manager.handle_input_exception.aio(io_context, started_at):
192
- res = io_context.call_finalized_function()
193
110
  # TODO(erikbern): any exception below shouldn't be considered a user exception
194
111
  if io_context.finalized_function.is_generator:
195
- if not inspect.isasyncgen(res):
196
- raise InvalidError(f"Async generator function returned value of type {type(res)}")
197
-
198
112
  # Send up to this many outputs at a time.
199
113
  current_function_call_id = execution_context.current_function_call_id()
200
114
  assert current_function_call_id is not None # Set above.
@@ -204,33 +118,24 @@ def call_function(
204
118
  async with container_io_manager.generator_output_sender(
205
119
  current_function_call_id,
206
120
  current_attempt_token,
207
- io_context.finalized_function.data_format,
121
+ io_context._generator_output_format(),
208
122
  generator_queue,
209
123
  ):
210
124
  item_count = 0
211
- async for value in res:
212
- await container_io_manager._queue_put.aio(generator_queue, value)
213
- item_count += 1
125
+ async with aclosing(io_context.call_generator_async()) as gen:
126
+ async for value in gen:
127
+ await container_io_manager._queue_put.aio(generator_queue, value)
128
+ item_count += 1
214
129
 
215
- message = api_pb2.GeneratorDone(items_total=item_count)
216
- await container_io_manager.push_outputs.aio(
217
- io_context,
218
- started_at,
219
- message,
220
- api_pb2.DATA_FORMAT_GENERATOR_DONE,
130
+ await container_io_manager._send_outputs.aio(
131
+ started_at, io_context.output_items_generator_done(started_at, item_count)
221
132
  )
222
133
  else:
223
- if not inspect.iscoroutine(res) or inspect.isgenerator(res) or inspect.isasyncgen(res):
224
- raise InvalidError(
225
- f"Async (non-generator) function returned value of type {type(res)}"
226
- " You might need to use @app.function(..., is_generator=True)."
227
- )
228
- value = await res
134
+ value = await io_context.call_function_async()
229
135
  await container_io_manager.push_outputs.aio(
230
136
  io_context,
231
137
  started_at,
232
138
  value,
233
- io_context.finalized_function.data_format,
234
139
  )
235
140
  reset_context()
236
141
 
@@ -240,13 +145,9 @@ def call_function(
240
145
  io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
241
146
  )
242
147
  with container_io_manager.handle_input_exception(io_context, started_at):
243
- res = io_context.call_finalized_function()
244
-
245
148
  # TODO(erikbern): any exception below shouldn't be considered a user exception
246
149
  if io_context.finalized_function.is_generator:
247
- if not inspect.isgenerator(res):
248
- raise InvalidError(f"Generator function returned value of type {type(res)}")
249
-
150
+ gen = io_context.call_generator_sync()
250
151
  # Send up to this many outputs at a time.
251
152
  current_function_call_id = execution_context.current_function_call_id()
252
153
  assert current_function_call_id is not None # Set above.
@@ -256,25 +157,20 @@ def call_function(
256
157
  with container_io_manager.generator_output_sender(
257
158
  current_function_call_id,
258
159
  current_attempt_token,
259
- io_context.finalized_function.data_format,
160
+ io_context._generator_output_format(),
260
161
  generator_queue,
261
162
  ):
262
163
  item_count = 0
263
- for value in res:
164
+ for value in gen:
264
165
  container_io_manager._queue_put(generator_queue, value)
265
166
  item_count += 1
266
167
 
267
- message = api_pb2.GeneratorDone(items_total=item_count)
268
- container_io_manager.push_outputs(io_context, started_at, message, api_pb2.DATA_FORMAT_GENERATOR_DONE)
269
- else:
270
- if inspect.iscoroutine(res) or inspect.isgenerator(res) or inspect.isasyncgen(res):
271
- raise InvalidError(
272
- f"Sync (non-generator) function return value of type {type(res)}."
273
- " You might need to use @app.function(..., is_generator=True)."
274
- )
275
- container_io_manager.push_outputs(
276
- io_context, started_at, res, io_context.finalized_function.data_format
168
+ container_io_manager._send_outputs(
169
+ started_at, io_context.output_items_generator_done(started_at, item_count)
277
170
  )
171
+ else:
172
+ values = io_context.call_function_sync()
173
+ container_io_manager.push_outputs(io_context, started_at, values)
278
174
  reset_context()
279
175
 
280
176
  if container_io_manager.input_concurrency_enabled:
@@ -353,23 +249,25 @@ def call_function(
353
249
  signal.signal(signal.SIGUSR1, usr1_handler) # reset signal handler
354
250
 
355
251
 
356
- def call_lifecycle_functions(
357
- event_loop: UserCodeEventLoop,
358
- container_io_manager, #: ContainerIOManager, TODO: this type is generated at runtime
359
- funcs: Sequence[Callable[..., Any]],
360
- ) -> None:
361
- """Call function(s), can be sync or async, but any return values are ignored."""
362
- with container_io_manager.handle_user_exception():
363
- for func in funcs:
364
- # We are deprecating parametrized exit methods but want to gracefully handle old code.
365
- # We can remove this once the deprecation in the actual @exit decorator is enforced.
366
- args = (None, None, None) if callable_has_non_self_params(func) else ()
367
- # in case func is non-async, it's executed here and sigint will by default
368
- # interrupt it using a KeyboardInterrupt exception
369
- res = func(*args)
370
- if inspect.iscoroutine(res):
371
- # if however func is async, we have to jump through some hoops
372
- event_loop.run(res)
252
+ def get_serialized_user_class_and_function(
253
+ function_def: api_pb2.Function, client: _Client
254
+ ) -> tuple[Optional[type], Optional[types.FunctionType]]:
255
+ if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
256
+ assert function_def.function_serialized or function_def.class_serialized
257
+
258
+ if function_def.function_serialized:
259
+ ser_fun = deserialize(function_def.function_serialized, client)
260
+ else:
261
+ ser_fun = None
262
+
263
+ if function_def.class_serialized:
264
+ ser_usr_cls = deserialize(function_def.class_serialized, client)
265
+ else:
266
+ ser_usr_cls = None
267
+ else:
268
+ ser_usr_cls, ser_fun = None, None
269
+
270
+ return ser_usr_cls, ser_fun
373
271
 
374
272
 
375
273
  def main(container_args: api_pb2.ContainerArguments, client: Client):
@@ -379,34 +277,20 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
379
277
  active_app: _App
380
278
  service: Service
381
279
  function_def = container_args.function_def
382
- is_auto_snapshot: bool = function_def.is_auto_snapshot
383
280
  # The worker sets this flag to "1" for snapshot and restore tasks. Otherwise, this flag is unset,
384
281
  # in which case snapshots should be disabled.
385
282
  is_snapshotting_function = (
386
283
  function_def.is_checkpointing_function and os.environ.get("MODAL_ENABLE_SNAP_RESTORE") == "1"
387
284
  )
388
285
 
389
- _client: _Client = synchronizer._translate_in(client) # TODO(erikbern): ugly
286
+ _client: _Client = cast(_Client, synchronizer._translate_in(client)) # TODO(erikbern): ugly
390
287
 
391
288
  # Call ContainerHello - currently a noop but might be used later for things
392
289
  container_io_manager.hello()
393
290
 
394
291
  with container_io_manager.heartbeats(is_snapshotting_function), UserCodeEventLoop() as event_loop:
395
292
  # If this is a serialized function, fetch the definition from the server
396
- if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
397
- assert function_def.function_serialized or function_def.class_serialized
398
-
399
- if function_def.function_serialized:
400
- ser_fun = deserialize(function_def.function_serialized, _client)
401
- else:
402
- ser_fun = None
403
-
404
- if function_def.class_serialized:
405
- ser_usr_cls = deserialize(function_def.class_serialized, _client)
406
- else:
407
- ser_usr_cls = None
408
- else:
409
- ser_usr_cls, ser_fun = None, None
293
+ ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
410
294
 
411
295
  # Initialize the function, importing user code.
412
296
  with container_io_manager.handle_user_exception():
@@ -437,9 +321,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
437
321
  param_kwargs,
438
322
  )
439
323
  else:
324
+ assert ser_usr_cls is None
440
325
  service = import_single_function_service(
441
326
  function_def,
442
- ser_usr_cls,
443
327
  ser_fun,
444
328
  )
445
329
 
@@ -458,7 +342,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
458
342
 
459
343
  # Initialize objects on the app.
460
344
  # This is basically only functions and classes - anything else is deprecated and will be unsupported soon
461
- app: App = synchronizer._translate_out(active_app)
345
+ app: App = cast(App, synchronizer._translate_out(active_app))
462
346
  app._init_container(client, container_app)
463
347
 
464
348
  # Hydrate all function dependencies.
@@ -472,10 +356,13 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
472
356
  f"Function has {len(service.service_deps)} dependencies"
473
357
  f" but container got {len(dep_object_ids)} object ids.\n"
474
358
  f"Code deps: {service.service_deps}\n"
475
- f"Object ids: {dep_object_ids}"
359
+ f"Object ids: {dep_object_ids}\n"
360
+ "\n"
361
+ "This can happen if you are defining Modal objects under a conditional statement "
362
+ "that evaluates differently in the local and remote environments."
476
363
  )
477
364
  for object_id, obj in zip(dep_object_ids, service.service_deps):
478
- metadata: Message = container_app.object_handle_metadata[object_id]
365
+ metadata: Optional[Message] = container_app.object_handle_metadata[object_id]
479
366
  obj._hydrate(object_id, _client, metadata)
480
367
 
481
368
  # Initialize clustered functions.
@@ -486,91 +373,8 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
486
373
  function_def._experimental_group_size,
487
374
  )
488
375
 
489
- # Identify all "enter" methods that need to run before we snapshot.
490
- if service.user_cls_instance is not None and not is_auto_snapshot:
491
- pre_snapshot_methods = _find_callables_for_obj(
492
- service.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
493
- )
494
- call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
495
-
496
- # If this container is being used to create a checkpoint, checkpoint the container after
497
- # global imports and initialization. Checkpointed containers run from this point onwards.
498
- if is_snapshotting_function:
499
- container_io_manager.memory_snapshot()
500
-
501
- # Install hooks for interactive functions.
502
- def breakpoint_wrapper():
503
- # note: it would be nice to not have breakpoint_wrapper() included in the backtrace
504
- container_io_manager.interact(from_breakpoint=True)
505
- import pdb
506
-
507
- frame = inspect.currentframe().f_back
508
-
509
- pdb.Pdb().set_trace(frame)
510
-
511
- sys.breakpointhook = breakpoint_wrapper
512
-
513
- # Identify the "enter" methods to run after resuming from a snapshot.
514
- if service.user_cls_instance is not None and not is_auto_snapshot:
515
- post_snapshot_methods = _find_callables_for_obj(
516
- service.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
517
- )
518
- call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
519
-
520
- with container_io_manager.handle_user_exception():
521
- finalized_functions = service.get_finalized_functions(function_def, container_io_manager)
522
- # Execute the function.
523
- lifespan_background_tasks = []
524
- try:
525
- for finalized_function in finalized_functions.values():
526
- if finalized_function.lifespan_manager:
527
- lifespan_background_tasks.append(
528
- event_loop.create_task(finalized_function.lifespan_manager.background_task())
529
- )
530
- with container_io_manager.handle_user_exception():
531
- event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
532
- call_function(
533
- event_loop,
534
- container_io_manager,
535
- finalized_functions,
536
- batch_max_size,
537
- batch_wait_ms,
538
- )
539
- finally:
540
- # Run exit handlers. From this point onward, ignore all SIGINT signals that come from
541
- # graceful shutdowns originating on the worker, as well as stray SIGUSR1 signals that
542
- # may have been sent to cancel inputs.
543
- int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
544
- usr1_handler = signal.signal(signal.SIGUSR1, signal.SIG_IGN)
545
-
546
- try:
547
- try:
548
- # run lifespan shutdown for asgi apps
549
- for finalized_function in finalized_functions.values():
550
- if finalized_function.lifespan_manager:
551
- with container_io_manager.handle_user_exception():
552
- event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
553
- finally:
554
- # no need to keep the lifespan asgi call around - we send it no more messages
555
- for lifespan_background_task in lifespan_background_tasks:
556
- lifespan_background_task.cancel() # prevent dangling tasks
557
-
558
- # Identify "exit" methods and run them.
559
- # want to make sure this is called even if the lifespan manager fails
560
- if service.user_cls_instance is not None and not is_auto_snapshot:
561
- exit_methods = _find_callables_for_obj(service.user_cls_instance, _PartialFunctionFlags.EXIT)
562
- call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
563
-
564
- # Finally, commit on exit to catch uncommitted volume changes and surface background
565
- # commit errors.
566
- container_io_manager.volume_commit(
567
- [v.volume_id for v in function_def.volume_mounts if v.allow_background_commits]
568
- )
569
- finally:
570
- # Restore the original signal handler, needed for container_test hygiene since the
571
- # test runs `main()` multiple times in the same process.
572
- signal.signal(signal.SIGINT, int_handler)
573
- signal.signal(signal.SIGUSR1, usr1_handler)
376
+ with service.execution_context(event_loop, container_io_manager) as finalized_functions:
377
+ call_function(event_loop, container_io_manager, finalized_functions, batch_max_size, batch_wait_ms)
574
378
 
575
379
 
576
380
  if __name__ == "__main__":