modal 1.4.4.dev22__tar.gz → 1.5.1__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 (234) hide show
  1. {modal-1.4.4.dev22 → modal-1.5.1}/PKG-INFO +6 -3
  2. {modal-1.4.4.dev22 → modal-1.5.1}/README.md +1 -1
  3. {modal-1.4.4.dev22 → modal-1.5.1}/modal/__init__.py +2 -0
  4. modal-1.5.1/modal/_billing.py +134 -0
  5. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_environments.py +85 -2
  6. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_functions.py +13 -9
  7. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_grpc_client.py +3 -3
  8. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_image.py +39 -4
  9. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_partial_function.py +1 -1
  10. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/gpu_memory_snapshot.py +5 -0
  11. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/task_lifecycle_manager.py +9 -4
  12. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_server.py +55 -18
  13. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/blob_utils.py +3 -1
  14. modal-1.5.1/modal/_utils/curl_utils.py +115 -0
  15. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/grpc_utils.py +85 -8
  16. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/http_utils.py +20 -1
  17. modal-1.5.1/modal/_utils/proxy_support.py +102 -0
  18. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/task_command_router_client.py +29 -2
  19. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/time_utils.py +23 -0
  20. modal-1.5.1/modal/_workspace.py +342 -0
  21. {modal-1.4.4.dev22 → modal-1.5.1}/modal/app.py +37 -23
  22. {modal-1.4.4.dev22 → modal-1.5.1}/modal/app.pyi +56 -42
  23. {modal-1.4.4.dev22 → modal-1.5.1}/modal/billing.py +3 -2
  24. modal-1.5.1/modal/billing.pyi +176 -0
  25. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_help.py +3 -3
  26. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/app.py +24 -3
  27. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/billing.py +108 -77
  28. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/changelog.py +1 -1
  29. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/config.py +1 -1
  30. modal-1.5.1/modal/cli/curl.py +137 -0
  31. modal-1.5.1/modal/cli/endpoint.py +352 -0
  32. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/entry_point.py +8 -2
  33. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/environment.py +140 -1
  34. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/image.py +7 -7
  35. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/shell.py +7 -14
  36. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/utils.py +16 -4
  37. modal-1.5.1/modal/cli/workspace.py +129 -0
  38. {modal-1.4.4.dev22 → modal-1.5.1}/modal/client.py +1 -1
  39. {modal-1.4.4.dev22 → modal-1.5.1}/modal/client.pyi +2 -10
  40. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cls.py +2 -2
  41. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cls.pyi +4 -4
  42. {modal-1.4.4.dev22 → modal-1.5.1}/modal/config.py +1 -0
  43. {modal-1.4.4.dev22 → modal-1.5.1}/modal/environments.py +2 -0
  44. {modal-1.4.4.dev22 → modal-1.5.1}/modal/environments.pyi +91 -1
  45. {modal-1.4.4.dev22 → modal-1.5.1}/modal/exception.py +1 -1
  46. {modal-1.4.4.dev22 → modal-1.5.1}/modal/functions.pyi +21 -18
  47. {modal-1.4.4.dev22 → modal-1.5.1}/modal/partial_function.pyi +1 -1
  48. {modal-1.4.4.dev22 → modal-1.5.1}/modal/runner.py +16 -12
  49. {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox.py +174 -64
  50. {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox.pyi +122 -38
  51. {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox_fs.py +39 -2
  52. {modal-1.4.4.dev22 → modal-1.5.1}/modal/schedule.py +1 -1
  53. {modal-1.4.4.dev22 → modal-1.5.1}/modal/secret.py +39 -9
  54. {modal-1.4.4.dev22 → modal-1.5.1}/modal/secret.pyi +19 -0
  55. {modal-1.4.4.dev22 → modal-1.5.1}/modal/server.pyi +78 -15
  56. modal-1.5.1/modal/workspace.py +25 -0
  57. modal-1.5.1/modal/workspace.pyi +450 -0
  58. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/PKG-INFO +6 -3
  59. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/SOURCES.txt +6 -0
  60. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/requires.txt +5 -1
  61. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_cli_docs.py +48 -2
  62. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/mdmd.py +10 -1
  63. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/types.py +1 -0
  64. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_grpc.py +195 -3
  65. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2.py +1353 -1093
  66. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2.pyi +522 -21
  67. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2_grpc.py +401 -3
  68. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/api_pb2_grpc.pyi +126 -2
  69. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/modal_api_grpc.py +12 -0
  70. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_grpc.py +32 -0
  71. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2.py +119 -79
  72. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2.pyi +68 -1
  73. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2_grpc.py +71 -2
  74. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/task_command_router_pb2_grpc.pyi +30 -4
  75. {modal-1.4.4.dev22 → modal-1.5.1}/modal_version/__init__.py +1 -1
  76. {modal-1.4.4.dev22 → modal-1.5.1}/pyproject.toml +30 -16
  77. modal-1.4.4.dev22/modal/_billing.py +0 -94
  78. modal-1.4.4.dev22/modal/_workspace.py +0 -116
  79. modal-1.4.4.dev22/modal/workspace.py +0 -12
  80. modal-1.4.4.dev22/modal/workspace.pyi +0 -105
  81. {modal-1.4.4.dev22 → modal-1.5.1}/LICENSE +0 -0
  82. {modal-1.4.4.dev22 → modal-1.5.1}/modal/__main__.py +0 -0
  83. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_clustered_functions.py +0 -0
  84. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_clustered_functions.pyi +0 -0
  85. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_container_entrypoint.py +0 -0
  86. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_function_variants.py +0 -0
  87. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_ipython.py +0 -0
  88. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_load_context.py +0 -0
  89. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_location.py +0 -0
  90. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_logs.py +0 -0
  91. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_object.py +0 -0
  92. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/__init__.py +0 -0
  93. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/manager.py +0 -0
  94. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/pty.py +0 -0
  95. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/rich.py +0 -0
  96. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_output/status.py +0 -0
  97. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_resolver.py +0 -0
  98. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_resources.py +0 -0
  99. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/__init__.py +0 -0
  100. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/asgi.py +0 -0
  101. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/container_io_manager.py +0 -0
  102. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/container_io_manager.pyi +0 -0
  103. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/execution_context.py +0 -0
  104. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/execution_context.pyi +0 -0
  105. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  106. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/telemetry.py +0 -0
  107. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/user_code_event_loop.py +0 -0
  108. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_runtime/user_code_imports.py +0 -0
  109. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_serialization.py +0 -0
  110. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_traceback.py +0 -0
  111. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_tunnel.py +0 -0
  112. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_tunnel.pyi +0 -0
  113. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_type_manager.py +0 -0
  114. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/__init__.py +0 -0
  115. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/app_utils.py +0 -0
  116. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/async_utils.py +0 -0
  117. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/auth_token_manager.py +0 -0
  118. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/browser_utils.py +0 -0
  119. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
  120. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/deprecation.py +0 -0
  121. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/docker_utils.py +0 -0
  122. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/function_utils.py +0 -0
  123. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/git_utils.py +0 -0
  124. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/grpc_testing.py +0 -0
  125. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/hash_utils.py +0 -0
  126. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/jwt_utils.py +0 -0
  127. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/logger.py +0 -0
  128. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/mount_utils.py +0 -0
  129. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/name_utils.py +0 -0
  130. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/package_utils.py +0 -0
  131. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/pattern_utils.py +0 -0
  132. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/rand_pb_testing.py +0 -0
  133. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/sandbox_fs_utils.py +0 -0
  134. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_utils/shell_utils.py +0 -0
  135. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/__init__.py +0 -0
  136. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  137. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/cloudpickle.py +0 -0
  138. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/tblib.py +0 -0
  139. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_vendor/version.py +0 -0
  140. {modal-1.4.4.dev22 → modal-1.5.1}/modal/_watcher.py +0 -0
  141. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2023.12.312.txt +0 -0
  142. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2023.12.txt +0 -0
  143. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2024.04.txt +0 -0
  144. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2024.10.txt +0 -0
  145. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/2025.06.txt +0 -0
  146. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/PREVIEW.txt +0 -0
  147. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/README.md +0 -0
  148. {modal-1.4.4.dev22 → modal-1.5.1}/modal/builder/base-images.json +0 -0
  149. {modal-1.4.4.dev22 → modal-1.5.1}/modal/call_graph.py +0 -0
  150. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/__init__.py +0 -0
  151. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_download.py +0 -0
  152. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/_traceback.py +0 -0
  153. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/bootstrap.py +0 -0
  154. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/cluster.py +0 -0
  155. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/container.py +0 -0
  156. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/dashboard.py +0 -0
  157. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/dict.py +0 -0
  158. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/import_refs.py +0 -0
  159. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/launch.py +0 -0
  160. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/logo.py +0 -0
  161. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/network_file_system.py +0 -0
  162. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/profile.py +0 -0
  163. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/__init__.py +0 -0
  164. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/run_jupyter.py +0 -0
  165. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/programs/vscode.py +0 -0
  166. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/queues.py +0 -0
  167. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/run.py +0 -0
  168. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/secret.py +0 -0
  169. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/selector.py +0 -0
  170. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/skills.py +0 -0
  171. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/token.py +0 -0
  172. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cli/volume.py +0 -0
  173. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cloud_bucket_mount.py +0 -0
  174. {modal-1.4.4.dev22 → modal-1.5.1}/modal/cloud_bucket_mount.pyi +0 -0
  175. {modal-1.4.4.dev22 → modal-1.5.1}/modal/container_process.py +0 -0
  176. {modal-1.4.4.dev22 → modal-1.5.1}/modal/container_process.pyi +0 -0
  177. {modal-1.4.4.dev22 → modal-1.5.1}/modal/dict.py +0 -0
  178. {modal-1.4.4.dev22 → modal-1.5.1}/modal/dict.pyi +0 -0
  179. {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/__init__.py +0 -0
  180. {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/flash.py +0 -0
  181. {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/flash.pyi +0 -0
  182. {modal-1.4.4.dev22 → modal-1.5.1}/modal/experimental/ipython.py +0 -0
  183. {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_io.py +0 -0
  184. {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_io.pyi +0 -0
  185. {modal-1.4.4.dev22 → modal-1.5.1}/modal/file_pattern_matcher.py +0 -0
  186. {modal-1.4.4.dev22 → modal-1.5.1}/modal/functions.py +0 -0
  187. {modal-1.4.4.dev22 → modal-1.5.1}/modal/image.py +0 -0
  188. {modal-1.4.4.dev22 → modal-1.5.1}/modal/image.pyi +0 -0
  189. {modal-1.4.4.dev22 → modal-1.5.1}/modal/io_streams.py +0 -0
  190. {modal-1.4.4.dev22 → modal-1.5.1}/modal/io_streams.pyi +0 -0
  191. {modal-1.4.4.dev22 → modal-1.5.1}/modal/mount.py +0 -0
  192. {modal-1.4.4.dev22 → modal-1.5.1}/modal/mount.pyi +0 -0
  193. {modal-1.4.4.dev22 → modal-1.5.1}/modal/network_file_system.py +0 -0
  194. {modal-1.4.4.dev22 → modal-1.5.1}/modal/network_file_system.pyi +0 -0
  195. {modal-1.4.4.dev22 → modal-1.5.1}/modal/object.py +0 -0
  196. {modal-1.4.4.dev22 → modal-1.5.1}/modal/object.pyi +0 -0
  197. {modal-1.4.4.dev22 → modal-1.5.1}/modal/output.py +0 -0
  198. {modal-1.4.4.dev22 → modal-1.5.1}/modal/parallel_map.py +0 -0
  199. {modal-1.4.4.dev22 → modal-1.5.1}/modal/parallel_map.pyi +0 -0
  200. {modal-1.4.4.dev22 → modal-1.5.1}/modal/partial_function.py +0 -0
  201. {modal-1.4.4.dev22 → modal-1.5.1}/modal/proxy.py +0 -0
  202. {modal-1.4.4.dev22 → modal-1.5.1}/modal/proxy.pyi +0 -0
  203. {modal-1.4.4.dev22 → modal-1.5.1}/modal/py.typed +0 -0
  204. {modal-1.4.4.dev22 → modal-1.5.1}/modal/queue.py +0 -0
  205. {modal-1.4.4.dev22 → modal-1.5.1}/modal/queue.pyi +0 -0
  206. {modal-1.4.4.dev22 → modal-1.5.1}/modal/retries.py +0 -0
  207. {modal-1.4.4.dev22 → modal-1.5.1}/modal/runner.pyi +0 -0
  208. {modal-1.4.4.dev22 → modal-1.5.1}/modal/running_app.py +0 -0
  209. {modal-1.4.4.dev22 → modal-1.5.1}/modal/sandbox_fs.pyi +0 -0
  210. {modal-1.4.4.dev22 → modal-1.5.1}/modal/scheduler_placement.py +0 -0
  211. {modal-1.4.4.dev22 → modal-1.5.1}/modal/server.py +0 -0
  212. {modal-1.4.4.dev22 → modal-1.5.1}/modal/serving.py +0 -0
  213. {modal-1.4.4.dev22 → modal-1.5.1}/modal/serving.pyi +0 -0
  214. {modal-1.4.4.dev22 → modal-1.5.1}/modal/skills/modal/SKILL.md +0 -0
  215. {modal-1.4.4.dev22 → modal-1.5.1}/modal/snapshot.py +0 -0
  216. {modal-1.4.4.dev22 → modal-1.5.1}/modal/snapshot.pyi +0 -0
  217. {modal-1.4.4.dev22 → modal-1.5.1}/modal/stream_type.py +0 -0
  218. {modal-1.4.4.dev22 → modal-1.5.1}/modal/token_flow.py +0 -0
  219. {modal-1.4.4.dev22 → modal-1.5.1}/modal/token_flow.pyi +0 -0
  220. {modal-1.4.4.dev22 → modal-1.5.1}/modal/volume.py +0 -0
  221. {modal-1.4.4.dev22 → modal-1.5.1}/modal/volume.pyi +0 -0
  222. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/dependency_links.txt +0 -0
  223. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/entry_points.txt +0 -0
  224. {modal-1.4.4.dev22 → modal-1.5.1}/modal.egg-info/top_level.txt +0 -0
  225. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/__init__.py +0 -0
  226. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_cli_docs_main.py +0 -0
  227. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_reference_docs.py +0 -0
  228. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/gen_reference_docs_main.py +0 -0
  229. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/__init__.py +0 -0
  230. {modal-1.4.4.dev22 → modal-1.5.1}/modal_docs/mdmd/signatures.py +0 -0
  231. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/__init__.py +0 -0
  232. {modal-1.4.4.dev22 → modal-1.5.1}/modal_proto/py.typed +0 -0
  233. {modal-1.4.4.dev22 → modal-1.5.1}/modal_version/__main__.py +0 -0
  234. {modal-1.4.4.dev22 → modal-1.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.4.dev22
3
+ Version: 1.5.1
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -23,12 +23,15 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
23
23
  Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
24
24
  Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
25
25
  Requires-Dist: rich>=12.0.0
26
- Requires-Dist: synchronicity~=0.12.3
26
+ Requires-Dist: synchronicity~=0.12.5
27
27
  Requires-Dist: toml
28
28
  Requires-Dist: types-certifi
29
29
  Requires-Dist: types-toml
30
30
  Requires-Dist: watchfiles
31
31
  Requires-Dist: typing_extensions~=4.6
32
+ Provides-Extra: api-proxy-support
33
+ Requires-Dist: python-socks[asyncio]>=2.5; extra == "api-proxy-support"
34
+ Requires-Dist: aiohttp-socks>=0.8; extra == "api-proxy-support"
32
35
  Dynamic: license-file
33
36
 
34
37
  # Modal Python SDK
@@ -45,7 +48,7 @@ access to serverless cloud compute from Python scripts on your local computer.
45
48
  See the [online documentation](https://modal.com/docs/guide) for many
46
49
  [example applications](https://modal.com/docs/examples),
47
50
  a [user guide](https://modal.com/docs/guide), and the detailed
48
- [API reference](https://modal.com/docs/reference).
51
+ [API reference](https://modal.com/docs/sdk/py/latest).
49
52
 
50
53
  ## Installation
51
54
 
@@ -12,7 +12,7 @@ access to serverless cloud compute from Python scripts on your local computer.
12
12
  See the [online documentation](https://modal.com/docs/guide) for many
13
13
  [example applications](https://modal.com/docs/examples),
14
14
  a [user guide](https://modal.com/docs/guide), and the detailed
15
- [API reference](https://modal.com/docs/reference).
15
+ [API reference](https://modal.com/docs/sdk/py/latest).
16
16
 
17
17
  ## Installation
18
18
 
@@ -43,6 +43,7 @@ try:
43
43
  from .schedule import Cron, Period
44
44
  from .scheduler_placement import SchedulerPlacement
45
45
  from .secret import Secret
46
+ from .server import Server
46
47
  from .snapshot import SandboxSnapshot
47
48
  from .volume import Volume
48
49
  from .workspace import Workspace
@@ -80,6 +81,7 @@ __all__ = [
80
81
  "SandboxSnapshot",
81
82
  "SchedulerPlacement",
82
83
  "Secret",
84
+ "Server",
83
85
  "Tunnel",
84
86
  "Volume",
85
87
  "Workspace",
@@ -0,0 +1,134 @@
1
+ # Copyright Modal Labs 2025
2
+ from dataclasses import FrozenInstanceError, dataclass
3
+ from datetime import datetime, timezone
4
+ from decimal import Decimal
5
+ from typing import Any, Iterable, TypedDict
6
+
7
+ from modal_proto import api_pb2
8
+
9
+ from ._utils.deprecation import deprecation_warning
10
+ from .client import _Client
11
+
12
+
13
+ class WorkspaceBillingReportItem(TypedDict):
14
+ object_id: str
15
+ description: str
16
+ environment_name: str
17
+ interval_start: datetime
18
+ cost: Decimal
19
+ tags: dict[str, str]
20
+
21
+
22
+ @dataclass(slots=True, frozen=True)
23
+ class BillingReportItem:
24
+ object_id: str
25
+ description: str
26
+ environment_name: str
27
+ interval_start: datetime
28
+ cost: Decimal
29
+ cost_by_resource: dict[str, Decimal]
30
+ tags: dict[str, str]
31
+
32
+ def __getitem__(self, key: str) -> Any:
33
+ """mdmd:ignore"""
34
+ if key not in self.__slots__:
35
+ raise KeyError(key)
36
+
37
+ return getattr(self, key)
38
+
39
+ def __setitem__(self, key: str, _: Any):
40
+ """mdmd:ignore"""
41
+ raise FrozenInstanceError(f"cannot assign to field {key!r}")
42
+
43
+ def keys(self) -> Iterable[str]:
44
+ """mdmd:ignore"""
45
+ yield from self.__slots__
46
+
47
+ def values(self) -> Iterable[Any]:
48
+ """mdmd:ignore"""
49
+ for k in self.__slots__:
50
+ yield getattr(self, k)
51
+
52
+ def items(self) -> Iterable[tuple[str, Any]]:
53
+ """mdmd:ignore"""
54
+ for k in self.__slots__:
55
+ yield k, getattr(self, k)
56
+
57
+ @classmethod
58
+ def _from_proto(cls, pb_item: api_pb2.WorkspaceBillingReportItem) -> "BillingReportItem":
59
+ return cls(
60
+ object_id=pb_item.object_id,
61
+ description=pb_item.description,
62
+ environment_name=pb_item.environment_name,
63
+ interval_start=pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
64
+ cost=Decimal(pb_item.cost),
65
+ cost_by_resource={k: Decimal(v) for k, v in pb_item.cost_by_resource.items()},
66
+ tags=dict(pb_item.tags),
67
+ )
68
+
69
+
70
+ async def _workspace_billing_report(
71
+ *,
72
+ start: datetime, # Start of the report, inclusive
73
+ end: datetime | None = None, # End of the report, exclusive
74
+ resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
75
+ tag_names: list[str] | None = None, # Optional additional metadata to include
76
+ client: _Client | None = None,
77
+ ) -> list[WorkspaceBillingReportItem]:
78
+ """Generate a tabular report of workspace usage by object and time.
79
+
80
+ The result will be a list of dictionaries for each interval (determined by `resolution`)
81
+ between the `start` and `end` limits. The dictionary represents a single Modal object
82
+ that billing can be attributed to (e.g., an App) along with metadata (including user-defined
83
+ tags) for identifying that object. The dictionary also contains a breakdown of the cost value
84
+ attributed to individual resources (for an App, this can be CPU, Memory, specific GPU types,
85
+ etc.). The specific resource types included in the breakdown are subject to change as
86
+ Modal's billing model evolves.
87
+
88
+ The `start` and `end` parameters are required to either have a UTC timezone or to be
89
+ timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
90
+ be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
91
+ parameters are partial: `start` will be rounded to the beginning of its interval, while
92
+ partial `end` intervals will be excluded.
93
+
94
+ Additional user-provided metadata can be included in the report if the objects have tags
95
+ and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
96
+ to include all tags in the report. Note that tags will be attributed to the entire interval even
97
+ if they were added or removed at some point within it. If the tag name was not in use during an
98
+ interval, it will be absent from the tags dictionary in that output row.
99
+
100
+ It's also possible to generate reports using the
101
+ [`modal billing report`](https://modal.com/docs/cli/latest/billing) CLI command. The CLI
102
+ has a few convenience features for generating reports across relative time ranges.
103
+
104
+ """
105
+
106
+ deprecation_warning(
107
+ (2026, 6, 18),
108
+ "The `modal.billing.workspace_billing_report()` function is deprecated. "
109
+ "Use the `workspace.billing.report()` method on the `modal.Workspace` object instead.",
110
+ )
111
+
112
+ from ._workspace import _Workspace
113
+
114
+ data = await _Workspace.from_context(client=client).billing.report(
115
+ start=start,
116
+ end=end,
117
+ resolution=resolution,
118
+ tag_names=tag_names,
119
+ )
120
+
121
+ res: list[WorkspaceBillingReportItem] = []
122
+ for datum in data:
123
+ item: WorkspaceBillingReportItem = {
124
+ "object_id": datum.object_id,
125
+ "description": datum.description,
126
+ "environment_name": datum.environment_name,
127
+ "interval_start": datum.interval_start,
128
+ "cost": Decimal(datum.cost),
129
+ "tags": dict(datum.tags),
130
+ }
131
+
132
+ res.append(item)
133
+
134
+ return res
@@ -3,6 +3,7 @@ import asyncio
3
3
  import builtins
4
4
  from collections.abc import Iterable, Mapping
5
5
  from dataclasses import dataclass
6
+ from datetime import datetime, timezone
6
7
  from typing import Literal
7
8
 
8
9
  from google.protobuf.empty_pb2 import Empty
@@ -12,6 +13,7 @@ from synchronicity import classproperty
12
13
 
13
14
  from modal_proto import api_pb2
14
15
 
16
+ from ._billing import BillingReportItem
15
17
  from ._load_context import LoadContext
16
18
  from ._object import _Object
17
19
  from ._resolver import Resolver
@@ -104,7 +106,7 @@ class _EnvironmentManager:
104
106
  MemberRole = Literal["viewer", "contributor"]
105
107
 
106
108
 
107
- def _role_to_proto(role: str) -> api_pb2.EnvironmentRole.ValueType:
109
+ def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
108
110
  match role:
109
111
  case "viewer":
110
112
  return api_pb2.ENVIRONMENT_ROLE_VIEWER
@@ -318,7 +320,7 @@ class _Environment(_Object, type_prefix="en"):
318
320
  # def settings(self) -> EnvironmentSettings:
319
321
  # return self._settings
320
322
 
321
- def _hydrate_metadata(self, metadata: Message):
323
+ def _hydrate_metadata(self, metadata: Message | None):
322
324
  # Overridden concrete implementation of base class method
323
325
  assert metadata and isinstance(metadata, api_pb2.EnvironmentMetadata)
324
326
  self._name = metadata.name or None
@@ -392,6 +394,87 @@ class _Environment(_Object, type_prefix="en"):
392
394
  client=client,
393
395
  )
394
396
 
397
+ @property
398
+ def billing(self) -> "_EnvironmentBillingManager":
399
+ return _EnvironmentBillingManager(self)
400
+
401
+
402
+ class _EnvironmentBillingManager:
403
+ """mdmd:namespace
404
+ Namespace for Environment billing APIs
405
+ """
406
+
407
+ def __init__(self, environment: _Environment):
408
+ """mdmd:ignore"""
409
+ self._environment = environment
410
+
411
+ async def report(
412
+ self,
413
+ *,
414
+ start: datetime, # Start of the report, inclusive
415
+ end: datetime | None = None, # End of the report, exclusive
416
+ resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
417
+ tag_names: list[str] | None = None, # Optional additional metadata to include
418
+ ) -> list[BillingReportItem]:
419
+ """Return a cost report for Environment usage, broken down by object and time.
420
+
421
+ Args:
422
+ start: Start of the report, inclusive and rounded to the beginning of the interval.
423
+ Must be in UTC or timezone-naive (interpreted as UTC).
424
+ end: End of the report, exclusive. Must be in UTC or timezone-naive. Partial final
425
+ intervals will be excluded from the report.
426
+ resolution: Resolution, e.g. "d" for daily or "h" for hourly.
427
+ tag_names: List of tag names; each row will include the tag name and value in use
428
+ for that object during the relevant time interval. Pass `["*"]` to include all
429
+ tags in the report.
430
+
431
+ Returns:
432
+ A list of `BillingReportItem` dataclasses. Each item reports the cost attributed to
433
+ a specific Modal object during a given time interval. Cost is further broken down by
434
+ the resource type that generated it (e.g. CPU, Memory, specific GPU usage).
435
+ Note that the specific resource types included in the breakdown are subject to change
436
+ as Modal's billing model evolves.
437
+
438
+ See also:
439
+ - [`modal environment billing report`](https://modal.com/docs/cli/latest/environment#modal-environment-billing-report):
440
+ An environment report CLI that has convenience features around relative time range queries
441
+ and JSON/CSV output.
442
+ - [`Workspace.billing.report()`](https://modal.com/docs/sdk/py/latest/modal.Workspace#billingreport):
443
+ An analogous report API for the entire Workspace.
444
+
445
+ """
446
+ if tag_names is None:
447
+ tag_names = []
448
+
449
+ if end is None:
450
+ end = datetime.now(timezone.utc)
451
+
452
+ if start.tzinfo is None:
453
+ start = start.replace(tzinfo=timezone.utc)
454
+ elif start.tzinfo != timezone.utc:
455
+ raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
456
+
457
+ if end.tzinfo is None:
458
+ end = end.replace(tzinfo=timezone.utc)
459
+ elif end.tzinfo != timezone.utc:
460
+ raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
461
+
462
+ if not self._environment.is_hydrated:
463
+ await self._environment.hydrate()
464
+
465
+ request = api_pb2.WorkspaceBillingReportRequest(
466
+ resolution=resolution,
467
+ tag_names=tag_names,
468
+ environment_ids=[self._environment.object_id],
469
+ )
470
+ request.start_timestamp.FromDatetime(start)
471
+ request.end_timestamp.FromDatetime(end)
472
+
473
+ return [
474
+ BillingReportItem._from_proto(pb_item)
475
+ async for pb_item in self._environment.client.stub.WorkspaceBillingReport.unary_stream(request)
476
+ ]
477
+
395
478
 
396
479
  ENVIRONMENT_CACHE: dict[str, _Environment] = {}
397
480
 
@@ -661,6 +661,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
661
661
  min_containers: int | None = None,
662
662
  max_containers: int | None = None,
663
663
  buffer_containers: int | None = None,
664
+ scaleup_window: int | None = None,
664
665
  scaledown_window: int | None = None,
665
666
  max_concurrent_inputs: int | None = None,
666
667
  target_concurrent_inputs: int | None = None,
@@ -766,6 +767,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
766
767
  min_containers=min_containers,
767
768
  max_containers=max_containers,
768
769
  buffer_containers=buffer_containers,
770
+ scaleup_window=scaleup_window,
769
771
  scaledown_window=scaledown_window,
770
772
  )
771
773
 
@@ -1157,6 +1159,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1157
1159
  min_containers: int | None = None,
1158
1160
  max_containers: int | None = None,
1159
1161
  buffer_containers: int | None = None,
1162
+ scaleup_window: int | None = None,
1160
1163
  scaledown_window: int | None = None,
1161
1164
  target_concurrency: int | None = None,
1162
1165
  ) -> None:
@@ -1164,6 +1167,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1164
1167
  min_containers=min_containers,
1165
1168
  max_containers=max_containers,
1166
1169
  buffer_containers=buffer_containers,
1170
+ scaleup_window=scaleup_window,
1167
1171
  scaledown_window=scaledown_window,
1168
1172
  target_concurrency=target_concurrency,
1169
1173
  )
@@ -1191,10 +1195,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1191
1195
  its static configuration.
1192
1196
 
1193
1197
  Args:
1194
- min_containers: Minimum number of containers to keep running, or `None` to leave unchanged.
1195
- max_containers: Maximum concurrent containers, or `None` to leave unchanged.
1196
- buffer_containers: Extra containers to keep warm beyond demand, or `None` to leave unchanged.
1197
- scaledown_window: Seconds idle containers wait before scaling down, or `None` to leave unchanged.
1198
+ min_containers: Minimum number of containers to keep running.
1199
+ max_containers: Maximum concurrent containers.
1200
+ buffer_containers: Extra containers to keep warm beyond current demand.
1201
+ scaledown_window: Maximum duration (in seconds) idle containers wait before scaling down.
1198
1202
 
1199
1203
  Examples:
1200
1204
  ```python notest
@@ -1208,7 +1212,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1208
1212
 
1209
1213
  # Extend the scaledown window to increase the amount of time that idle containers stay alive
1210
1214
  f.update_autoscaler(scaledown_window=300)
1211
- ```
1215
+ ```
1212
1216
 
1213
1217
  """
1214
1218
  # Assert .update_autoscaler() is not called on a method as opposed to the Object. Applicable for Cls only.
@@ -1908,9 +1912,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1908
1912
  **kwargs: Keyword arguments forwarded to the remote function.
1909
1913
 
1910
1914
  Returns:
1911
- A [`modal.FunctionCall`](https://modal.com/docs/reference/modal.FunctionCall) object
1915
+ A [`modal.FunctionCall`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall) object
1912
1916
  that can later be polled or waited for using
1913
- [`.get(timeout=...)`](https://modal.com/docs/reference/modal.FunctionCall#get).
1917
+ [`.get(timeout=...)`](https://modal.com/docs/sdk/py/latest/modal.FunctionCall#get).
1914
1918
  """
1915
1919
  self._check_no_web_url("spawn")
1916
1920
  if self._is_generator:
@@ -2061,7 +2065,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2061
2065
  """Returns a structure representing the call graph from a given root
2062
2066
  call ID, along with the status of execution for each node.
2063
2067
 
2064
- See [`modal.call_graph`](https://modal.com/docs/reference/modal.call_graph) reference page
2068
+ See [`modal.call_graph`](https://modal.com/docs/sdk/py/latest/modal.call_graph) reference page
2065
2069
  for documentation on the structure of the returned `InputInfo` items.
2066
2070
 
2067
2071
  Returns:
@@ -2079,7 +2083,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2079
2083
  terminate_containers: bool = False,
2080
2084
  ):
2081
2085
  """Cancels the function call, which will stop its execution and mark its inputs as
2082
- [`TERMINATED`](https://modal.com/docs/reference/modal.call_graph#modalcall_graphinputstatus).
2086
+ [`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
2083
2087
 
2084
2088
  If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
2085
2089
  causing any non-cancelled inputs on those containers to be rescheduled in new containers.
@@ -1,6 +1,6 @@
1
1
  # Copyright Modal Labs 2025
2
2
  from collections.abc import Collection, Mapping
3
- from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload
3
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, Literal, TypeVar, overload
4
4
 
5
5
  import grpclib.client
6
6
  from google.protobuf.message import Message
@@ -178,9 +178,9 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
178
178
 
179
179
  async def unary_stream(
180
180
  self,
181
- request,
181
+ request: RequestType,
182
182
  metadata: Any | None = None,
183
- ):
183
+ ) -> AsyncGenerator[ResponseType, None]:
184
184
  from .client import _Client
185
185
 
186
186
  if self.client._snapshotted:
@@ -9,6 +9,7 @@ import typing
9
9
  import warnings
10
10
  from collections.abc import Callable, Collection, Sequence
11
11
  from dataclasses import dataclass, field
12
+ from functools import wraps
12
13
  from inspect import isfunction
13
14
  from pathlib import Path, PurePosixPath
14
15
  from typing import (
@@ -424,6 +425,20 @@ async def _image_await_build_result(image_id: str, client: _Client) -> api_pb2.I
424
425
  return result_response
425
426
 
426
427
 
428
+ def _requires_image_instance(method):
429
+ @wraps(method)
430
+ def wrapper(self, *args, **kwargs):
431
+ if not isinstance(self, _Image):
432
+ raise InvalidError(
433
+ "Image has not been constructed yet. "
434
+ f"Use one of the static factory methods prior to calling {method.__name__} "
435
+ "like `modal.Image.debian_slim`, `modal.Image.from_registry`, or `modal.Image.micromamba`"
436
+ )
437
+ return method(self, *args, **kwargs)
438
+
439
+ return wrapper
440
+
441
+
427
442
  class _Image(_Object, type_prefix="im"):
428
443
  """Base class for container images to run functions in.
429
444
 
@@ -755,6 +770,7 @@ class _Image(_Object, type_prefix="im"):
755
770
  context_mount_function=lambda: mount,
756
771
  )
757
772
 
773
+ @_requires_image_instance
758
774
  def add_local_file(self, local_path: str | Path, remote_path: str, *, copy: bool = False) -> "_Image":
759
775
  """Adds a local file to the image at `remote_path` within the container.
760
776
 
@@ -791,6 +807,7 @@ class _Image(_Object, type_prefix="im"):
791
807
  mount = _Mount._from_local_file(local_path, remote_path)
792
808
  return self._add_mount_layer_or_copy(mount, copy=copy)
793
809
 
810
+ @_requires_image_instance
794
811
  def add_local_dir(
795
812
  self,
796
813
  local_path: str | Path,
@@ -869,6 +886,7 @@ class _Image(_Object, type_prefix="im"):
869
886
  mount = _Mount._add_local_dir(Path(local_path), PurePosixPath(remote_path), ignore=_ignore_fn(ignore))
870
887
  return self._add_mount_layer_or_copy(mount, copy=copy)
871
888
 
889
+ @_requires_image_instance
872
890
  def add_local_python_source(
873
891
  self, *modules: str, copy: bool = False, ignore: Sequence[str] | Callable[[Path], bool] = NON_PYTHON_FILES
874
892
  ) -> "_Image":
@@ -1012,6 +1030,7 @@ class _Image(_Object, type_prefix="im"):
1012
1030
  await resolver.load(self, load_context)
1013
1031
  return self
1014
1032
 
1033
+ @_requires_image_instance
1015
1034
  def pip_install(
1016
1035
  self,
1017
1036
  *packages: str | list[str],
@@ -1098,6 +1117,7 @@ class _Image(_Object, type_prefix="im"):
1098
1117
  secrets=secrets,
1099
1118
  )
1100
1119
 
1120
+ @_requires_image_instance
1101
1121
  def pip_install_private_repos(
1102
1122
  self,
1103
1123
  *repositories: str,
@@ -1218,6 +1238,7 @@ class _Image(_Object, type_prefix="im"):
1218
1238
  force_build=self.force_build or force_build,
1219
1239
  )
1220
1240
 
1241
+ @_requires_image_instance
1221
1242
  def pip_install_from_requirements(
1222
1243
  self,
1223
1244
  requirements_txt: str,
@@ -1279,6 +1300,7 @@ class _Image(_Object, type_prefix="im"):
1279
1300
  secrets=secrets,
1280
1301
  )
1281
1302
 
1303
+ @_requires_image_instance
1282
1304
  def pip_install_from_pyproject(
1283
1305
  self,
1284
1306
  pyproject_toml: str,
@@ -1362,6 +1384,7 @@ class _Image(_Object, type_prefix="im"):
1362
1384
  gpu_config=parse_gpu_config(gpu),
1363
1385
  )
1364
1386
 
1387
+ @_requires_image_instance
1365
1388
  def uv_pip_install(
1366
1389
  self,
1367
1390
  *packages: str | list[str],
@@ -1493,6 +1516,7 @@ class _Image(_Object, type_prefix="im"):
1493
1516
  secrets=secrets,
1494
1517
  )
1495
1518
 
1519
+ @_requires_image_instance
1496
1520
  def poetry_install_from_file(
1497
1521
  self,
1498
1522
  poetry_pyproject_toml: str,
@@ -1602,6 +1626,7 @@ class _Image(_Object, type_prefix="im"):
1602
1626
  gpu_config=parse_gpu_config(gpu),
1603
1627
  )
1604
1628
 
1629
+ @_requires_image_instance
1605
1630
  def uv_sync(
1606
1631
  self,
1607
1632
  uv_project_dir: str = "./",
@@ -1785,6 +1810,7 @@ class _Image(_Object, type_prefix="im"):
1785
1810
  gpu_config=parse_gpu_config(gpu),
1786
1811
  )
1787
1812
 
1813
+ @_requires_image_instance
1788
1814
  def dockerfile_commands(
1789
1815
  self,
1790
1816
  *dockerfile_commands: str | list[str],
@@ -1873,6 +1899,7 @@ class _Image(_Object, type_prefix="im"):
1873
1899
  build_args=build_args,
1874
1900
  )
1875
1901
 
1902
+ @_requires_image_instance
1876
1903
  def entrypoint(
1877
1904
  self,
1878
1905
  entrypoint_commands: list[str],
@@ -1893,6 +1920,7 @@ class _Image(_Object, type_prefix="im"):
1893
1920
 
1894
1921
  return self.dockerfile_commands(dockerfile_cmd)
1895
1922
 
1923
+ @_requires_image_instance
1896
1924
  def shell(
1897
1925
  self,
1898
1926
  shell_commands: list[str],
@@ -1913,6 +1941,7 @@ class _Image(_Object, type_prefix="im"):
1913
1941
 
1914
1942
  return self.dockerfile_commands(dockerfile_cmd)
1915
1943
 
1944
+ @_requires_image_instance
1916
1945
  def run_commands(
1917
1946
  self,
1918
1947
  *commands: str | list[str],
@@ -1997,6 +2026,7 @@ class _Image(_Object, type_prefix="im"):
1997
2026
  _namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1998
2027
  )
1999
2028
 
2029
+ @_requires_image_instance
2000
2030
  def micromamba_install(
2001
2031
  self,
2002
2032
  *packages: str | list[str],
@@ -2528,6 +2558,7 @@ class _Image(_Object, type_prefix="im"):
2528
2558
  _namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
2529
2559
  )
2530
2560
 
2561
+ @_requires_image_instance
2531
2562
  def apt_install(
2532
2563
  self,
2533
2564
  *packages: str | list[str],
@@ -2579,6 +2610,7 @@ class _Image(_Object, type_prefix="im"):
2579
2610
  secrets=secrets,
2580
2611
  )
2581
2612
 
2613
+ @_requires_image_instance
2582
2614
  def run_function(
2583
2615
  self,
2584
2616
  raw_f: Callable[..., Any],
@@ -2697,6 +2729,7 @@ class _Image(_Object, type_prefix="im"):
2697
2729
  force_build=self.force_build or force_build,
2698
2730
  )
2699
2731
 
2732
+ @_requires_image_instance
2700
2733
  def env(self, vars: dict[str, str]) -> "_Image":
2701
2734
  """Sets the environment variables in an Image.
2702
2735
 
@@ -2727,6 +2760,7 @@ class _Image(_Object, type_prefix="im"):
2727
2760
  dockerfile_function=build_dockerfile,
2728
2761
  )
2729
2762
 
2763
+ @_requires_image_instance
2730
2764
  def workdir(self, path: str | PurePosixPath) -> "_Image":
2731
2765
  """Set the working directory for subsequent image build steps and function execution.
2732
2766
 
@@ -2756,6 +2790,7 @@ class _Image(_Object, type_prefix="im"):
2756
2790
  dockerfile_function=build_dockerfile,
2757
2791
  )
2758
2792
 
2793
+ @_requires_image_instance
2759
2794
  def cmd(self, cmd: list[str]) -> "_Image":
2760
2795
  """Set the default command (`CMD`) to run when a container is started.
2761
2796
 
@@ -2784,6 +2819,7 @@ class _Image(_Object, type_prefix="im"):
2784
2819
 
2785
2820
  return self.dockerfile_commands(dockerfile_cmd)
2786
2821
 
2822
+ @_requires_image_instance
2787
2823
  def pipe(
2788
2824
  self,
2789
2825
  func: Callable[Concatenate["modal.Image", P], "modal.Image"],
@@ -2929,17 +2965,16 @@ class _Image(_Object, type_prefix="im"):
2929
2965
  """
2930
2966
  tag = _parse_named_image_ref(name)
2931
2967
 
2932
- if not self.is_hydrated:
2968
+ if self._object_id is None:
2933
2969
  raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
2934
2970
 
2935
- _client = client or self._client
2936
- assert _client is not None
2971
+ _client = client or await _Client.from_env()
2937
2972
 
2938
2973
  resolved_env = environment_name or config.get("environment") or ""
2939
2974
 
2940
2975
  await _client.stub.ImagePublish(
2941
2976
  api_pb2.ImagePublishRequest(
2942
- image_id=self.object_id,
2977
+ image_id=self._object_id,
2943
2978
  environment_name=resolved_env,
2944
2979
  is_public=False,
2945
2980
  tag=tag,
@@ -477,7 +477,7 @@ def _wsgi_app(
477
477
  Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
478
478
  It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
479
479
  which is compatible with ASGI and supports additional functionality such as web sockets.
480
- Modal supports ASGI via [`asgi_app`](https://modal.com/docs/reference/modal.asgi_app).
480
+ Modal supports ASGI via [`asgi_app`](https://modal.com/docs/sdk/py/latest/modal.asgi_app).
481
481
 
482
482
  Examples:
483
483
  ```python
@@ -17,6 +17,11 @@ from modal.config import config, logger
17
17
 
18
18
  CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
19
19
 
20
+ # Exit code used to signal to modal-runtime that a GPU memory snapshot restore failed
21
+ # and the task should be retried without a snapshot. This is a sentinel value that must
22
+ # stay in sync with `GPU_SNAPSHOT_RESTORE_FAILED_EXIT_CODE` in runner.rs.
23
+ SNAPSHOT_RESTORE_FAILED_EXIT_CODE: int = 222
24
+
20
25
  # Maximum total duration for each individual `cuda-checkpoint` invocation.
21
26
  CUDA_CHECKPOINT_TIMEOUT: float = 3 * 60.0
22
27
 
@@ -180,10 +180,15 @@ class _TaskLifecycleManager:
180
180
  if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
181
181
  logger.debug("GPU memory snapshot enabled. Attempting to restore GPU memory.")
182
182
 
183
- assert self._cuda_checkpoint_session, (
184
- "CudaCheckpointSession not found when attempting to restore GPU memory"
185
- )
186
- self._cuda_checkpoint_session.restore()
183
+ try:
184
+ if self._cuda_checkpoint_session is None:
185
+ raise gpu_memory_snapshot.CudaCheckpointException("CudaCheckpointSession not found")
186
+ self._cuda_checkpoint_session.restore()
187
+ except gpu_memory_snapshot.CudaCheckpointException as exc:
188
+ logger.warning(f"GPU memory snapshot restore failed; retrying task without snapshot. Error: {exc}")
189
+ sys.stderr.flush()
190
+ # Exit with a sentinel code that the runtime will use to retry the task without a snapshot.
191
+ os._exit(gpu_memory_snapshot.SNAPSHOT_RESTORE_FAILED_EXIT_CODE)
187
192
 
188
193
  self._client = await _Client.from_env()
189
194