modal 1.6.4.dev1__tar.gz → 1.6.4.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/PKG-INFO +1 -1
  2. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/task_lifecycle_manager.py +23 -2
  3. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/task_lifecycle_manager.pyi +6 -0
  4. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/app.py +84 -1
  5. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/app.pyi +154 -0
  6. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/endpoint.py +109 -0
  7. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/image.py +119 -1
  8. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/client.pyi +2 -2
  9. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/config.py +5 -7
  10. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/functions.pyi +6 -6
  11. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/runner.py +10 -7
  12. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/runner.pyi +5 -0
  13. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/types.py +21 -1
  14. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/PKG-INFO +1 -1
  15. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2.py +1407 -1406
  16. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2.pyi +39 -58
  17. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_version/__init__.py +1 -1
  18. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/pyproject.toml +0 -1
  19. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/LICENSE +0 -0
  20. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/README.md +0 -0
  21. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/__init__.py +0 -0
  22. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/__main__.py +0 -0
  23. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_billing.py +0 -0
  24. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_cluster.py +0 -0
  25. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_cluster.pyi +0 -0
  26. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_container_entrypoint.py +0 -0
  27. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_environments.py +0 -0
  28. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_function_variants.py +0 -0
  29. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_functions.py +0 -0
  30. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_grpc_client.py +0 -0
  31. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_image.py +0 -0
  32. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_ipython.py +0 -0
  33. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_load_context.py +0 -0
  34. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_location.py +0 -0
  35. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs.py +0 -0
  36. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs_manager.py +0 -0
  37. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_logs_manager.pyi +0 -0
  38. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_metrics.py +0 -0
  39. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_metrics.pyi +0 -0
  40. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_object.py +0 -0
  41. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_outbound_policy.py +0 -0
  42. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_outbound_policy.pyi +0 -0
  43. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/__init__.py +0 -0
  44. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/manager.py +0 -0
  45. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/pty.py +0 -0
  46. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/rich.py +0 -0
  47. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_output/status.py +0 -0
  48. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_partial_function.py +0 -0
  49. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_resolver.py +0 -0
  50. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_resources.py +0 -0
  51. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/__init__.py +0 -0
  52. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/asgi.py +0 -0
  53. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/container_io_manager.py +0 -0
  54. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/container_io_manager.pyi +0 -0
  55. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/execution_context.py +0 -0
  56. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/execution_context.pyi +0 -0
  57. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  58. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/telemetry.py +0 -0
  59. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/user_code_event_loop.py +0 -0
  60. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_runtime/user_code_imports.py +0 -0
  61. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_serialization.py +0 -0
  62. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_server.py +0 -0
  63. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_server.pyi +0 -0
  64. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_supports_logs.py +0 -0
  65. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_traceback.py +0 -0
  66. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_tunnel.py +0 -0
  67. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_tunnel.pyi +0 -0
  68. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_type_manager.py +0 -0
  69. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/__init__.py +0 -0
  70. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/app_utils.py +0 -0
  71. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/async_utils.py +0 -0
  72. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/auth_token_manager.py +0 -0
  73. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/blob_utils.py +0 -0
  74. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/browser_utils.py +0 -0
  75. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/bytes_io_segment_payload.py +0 -0
  76. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/curl_utils.py +0 -0
  77. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/deprecation.py +0 -0
  78. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/display_utils.py +0 -0
  79. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/docker_utils.py +0 -0
  80. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/function_utils.py +0 -0
  81. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/git_utils.py +0 -0
  82. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/grpc_testing.py +0 -0
  83. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/grpc_utils.py +0 -0
  84. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/idle_countdown.py +0 -0
  87. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/jwt_utils.py +0 -0
  88. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/logger.py +0 -0
  89. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/mount_utils.py +0 -0
  90. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/name_utils.py +0 -0
  91. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/oauth.py +0 -0
  92. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/package_utils.py +0 -0
  93. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/pattern_utils.py +0 -0
  94. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/proxy_support.py +0 -0
  95. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/rand_pb_testing.py +0 -0
  96. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/sandbox_fs_utils.py +0 -0
  97. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/shell_utils.py +0 -0
  98. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_utils/time_utils.py +0 -0
  99. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/__init__.py +0 -0
  100. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  101. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/cloudpickle.py +0 -0
  102. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/tblib.py +0 -0
  103. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_vendor/version.py +0 -0
  104. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_watcher.py +0 -0
  105. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/_workspace.py +0 -0
  106. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/billing.py +0 -0
  107. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/billing.pyi +0 -0
  108. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2023.12.312.txt +0 -0
  109. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2023.12.txt +0 -0
  110. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2024.04.txt +0 -0
  111. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2024.10.txt +0 -0
  112. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2025.06.txt +0 -0
  113. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/2026.10.txt +0 -0
  114. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/PREVIEW.txt +0 -0
  115. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/README.md +0 -0
  116. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/builder/base-images.json +0 -0
  117. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/call_graph.py +0 -0
  118. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/__init__.py +0 -0
  119. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_download.py +0 -0
  120. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_help.py +0 -0
  121. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_logs.py +0 -0
  122. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_stats.py +0 -0
  123. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/_traceback.py +0 -0
  124. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/app.py +0 -0
  125. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/billing.py +0 -0
  126. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/changelog.py +0 -0
  127. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/config.py +0 -0
  128. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/container.py +0 -0
  129. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/curl.py +0 -0
  130. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/dashboard.py +0 -0
  131. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/dict.py +0 -0
  132. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/entry_point.py +0 -0
  133. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/environment.py +0 -0
  134. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/function.py +0 -0
  135. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/import_refs.py +0 -0
  136. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/logo.py +0 -0
  137. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/network_file_system.py +0 -0
  138. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/profile.py +0 -0
  139. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/queues.py +0 -0
  140. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/run.py +0 -0
  141. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/secret.py +0 -0
  142. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/selector.py +0 -0
  143. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/server.py +0 -0
  144. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/shell.py +0 -0
  145. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/skills.py +0 -0
  146. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/token.py +0 -0
  147. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/utils.py +0 -0
  148. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/volume.py +0 -0
  149. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cli/workspace.py +0 -0
  150. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/client.py +0 -0
  151. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cloud_bucket_mount.py +0 -0
  152. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cloud_bucket_mount.pyi +0 -0
  153. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cls.py +0 -0
  154. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cls.pyi +0 -0
  155. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cluster.py +0 -0
  156. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/cluster.pyi +0 -0
  157. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/container_process.py +0 -0
  158. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/container_process.pyi +0 -0
  159. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/dict.py +0 -0
  160. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/dict.pyi +0 -0
  161. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/environments.py +0 -0
  162. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/environments.pyi +0 -0
  163. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/exception.py +0 -0
  164. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/__init__.py +0 -0
  165. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/_flash_container_entry.py +0 -0
  166. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash.py +0 -0
  167. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash.pyi +0 -0
  168. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash_server.py +0 -0
  169. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/flash_server.pyi +0 -0
  170. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/experimental/ipython.py +0 -0
  171. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/file_pattern_matcher.py +0 -0
  172. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/functions.py +0 -0
  173. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/image.py +0 -0
  174. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/image.pyi +0 -0
  175. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/io_streams.py +0 -0
  176. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/io_streams.pyi +0 -0
  177. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/mount.py +0 -0
  178. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/mount.pyi +0 -0
  179. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/network_file_system.py +0 -0
  180. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/network_file_system.pyi +0 -0
  181. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/object.py +0 -0
  182. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/object.pyi +0 -0
  183. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/output.py +0 -0
  184. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/parallel_map.py +0 -0
  185. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/parallel_map.pyi +0 -0
  186. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/partial_function.py +0 -0
  187. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/partial_function.pyi +0 -0
  188. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/proxy.py +0 -0
  189. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/proxy.pyi +0 -0
  190. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/py.typed +0 -0
  191. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/queue.py +0 -0
  192. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/queue.pyi +0 -0
  193. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/retries.py +0 -0
  194. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/running_app.py +0 -0
  195. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/__init__.py +0 -0
  196. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_common.py +0 -0
  197. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_filesystem.py +0 -0
  198. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_persistent.py +0 -0
  199. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_v1.py +0 -0
  200. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_impl_v2.py +0 -0
  201. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_sandbox.py +0 -0
  202. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_sidecar.py +0 -0
  203. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_snapshot.py +0 -0
  204. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/_task_command_router_client.py +0 -0
  205. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/sandbox.py +0 -0
  206. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/sandbox/sandbox.pyi +0 -0
  207. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/schedule.py +0 -0
  208. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/scheduler_placement.py +0 -0
  209. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/secret.py +0 -0
  210. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/secret.pyi +0 -0
  211. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/server.py +0 -0
  212. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/server.pyi +0 -0
  213. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/serving.py +0 -0
  214. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/serving.pyi +0 -0
  215. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/skills/modal/SKILL.md +0 -0
  216. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/stream_type.py +0 -0
  217. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/token_flow.py +0 -0
  218. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/token_flow.pyi +0 -0
  219. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/volume.py +0 -0
  220. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/volume.pyi +0 -0
  221. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/workspace.py +0 -0
  222. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal/workspace.pyi +0 -0
  223. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/SOURCES.txt +0 -0
  224. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/dependency_links.txt +0 -0
  225. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/entry_points.txt +0 -0
  226. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/requires.txt +0 -0
  227. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal.egg-info/top_level.txt +0 -0
  228. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/__init__.py +0 -0
  229. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_cli_docs.py +0 -0
  230. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_cli_docs_main.py +0 -0
  231. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_reference_docs.py +0 -0
  232. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/gen_reference_docs_main.py +0 -0
  233. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/__init__.py +0 -0
  234. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/mdmd.py +0 -0
  235. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/signatures.py +0 -0
  236. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/mdmd/types.py +0 -0
  237. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_docs/reference_intro.md +0 -0
  238. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/__init__.py +0 -0
  239. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_grpc.py +0 -0
  240. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2_grpc.py +0 -0
  241. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/api_pb2_grpc.pyi +0 -0
  242. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/modal_api_grpc.py +0 -0
  243. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/py.typed +0 -0
  244. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_grpc.py +0 -0
  245. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2.py +0 -0
  246. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2.pyi +0 -0
  247. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  248. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  249. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/modal_version/__main__.py +0 -0
  250. {modal-1.6.4.dev1 → modal-1.6.4.dev3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.4.dev1
3
+ Version: 1.6.4.dev3
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -25,9 +25,19 @@ from modal._utils.blob_utils import format_blob_data
25
25
  from modal._utils.grpc_utils import Retry
26
26
  from modal._utils.package_utils import parse_major_minor_version
27
27
  from modal.client import _Client
28
- from modal.config import config, logger
28
+ from modal.config import _env_var_name, config, logger
29
29
  from modal_proto import api_pb2
30
30
 
31
+ SPEC_ENVIRON_PATH: str = "/proc/gvisor/spec_environ"
32
+
33
+
34
+ def _read_spec_environ() -> dict[str, str]:
35
+ path = Path(SPEC_ENVIRON_PATH)
36
+ if not path.exists():
37
+ return {}
38
+ entries = (entry.partition("=") for entry in path.read_text("utf8").split("\0"))
39
+ return {key: value for key, sep, value in entries if sep}
40
+
31
41
 
32
42
  class UserException(Exception):
33
43
  """Used to shut down the task gracefully."""
@@ -42,6 +52,7 @@ class _TaskLifecycleManager:
42
52
  checkpoint_id: str | None
43
53
  _client: _Client
44
54
  _cuda_checkpoint_session: gpu_memory_snapshot.CudaCheckpointSession | None
55
+ _snapshot_spec_environ: dict[str, str]
45
56
 
46
57
  def _init(
47
58
  self,
@@ -57,6 +68,7 @@ class _TaskLifecycleManager:
57
68
  self.checkpoint_id = checkpoint_id
58
69
  self._client = client
59
70
  self._cuda_checkpoint_session = None
71
+ self._snapshot_spec_environ = {}
60
72
 
61
73
  def __new__(
62
74
  cls,
@@ -171,10 +183,18 @@ class _TaskLifecycleManager:
171
183
  setattr(self, key, restored_state[key])
172
184
 
173
185
  # Env vars and global state.
186
+ env_updates = {}
187
+ for key, value in _read_spec_environ().items():
188
+ if self._snapshot_spec_environ.get(key) != value:
189
+ env_updates[key] = value
190
+
191
+ # Values from the restore state take precedence.
174
192
  for key, value in restored_state.items():
175
193
  # Empty string indicates that value does not need to be updated.
176
194
  if value != "":
177
- config.override_locally(key, value)
195
+ env_updates[_env_var_name(key)] = value
196
+
197
+ os.environ.update(env_updates)
178
198
 
179
199
  # Restore GPU memory.
180
200
  if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
@@ -205,6 +225,7 @@ class _TaskLifecycleManager:
205
225
  self._cuda_checkpoint_session = gpu_memory_snapshot.CudaCheckpointSession()
206
226
  self._cuda_checkpoint_session.checkpoint()
207
227
 
228
+ self._snapshot_spec_environ = _read_spec_environ()
208
229
  await self._client._stub.ContainerCheckpoint(
209
230
  api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id)
210
231
  )
@@ -5,6 +5,8 @@ import synchronicity.combined_types
5
5
  import typing
6
6
  import typing_extensions
7
7
 
8
+ def _read_spec_environ() -> dict[str, str]: ...
9
+
8
10
  class UserException(Exception):
9
11
  """Used to shut down the task gracefully."""
10
12
 
@@ -18,6 +20,7 @@ class _TaskLifecycleManager:
18
20
  checkpoint_id: typing.Optional[str]
19
21
  _client: modal.client._Client
20
22
  _cuda_checkpoint_session: typing.Optional[modal._runtime.gpu_memory_snapshot.CudaCheckpointSession]
23
+ _snapshot_spec_environ: dict[str, str]
21
24
 
22
25
  def _init(
23
26
  self,
@@ -67,6 +70,7 @@ class TaskLifecycleManager:
67
70
  checkpoint_id: typing.Optional[str]
68
71
  _client: modal.client.Client
69
72
  _cuda_checkpoint_session: typing.Optional[modal._runtime.gpu_memory_snapshot.CudaCheckpointSession]
73
+ _snapshot_spec_environ: dict[str, str]
70
74
 
71
75
  def __init__(self, /, *args, **kwargs):
72
76
  """Initialize self. See help(type(self)) for accurate signature."""
@@ -136,3 +140,5 @@ def check_fastapi_pydantic_compatibility(exc: ImportError) -> None:
136
140
  building an image that specifies a more recent version only for pydantic.
137
141
  """
138
142
  ...
143
+
144
+ SPEC_ENVIRON_PATH: str
@@ -3,6 +3,7 @@ import inspect
3
3
  import typing
4
4
  from collections.abc import AsyncGenerator, Callable, Collection, Coroutine, Mapping, Sequence
5
5
  from dataclasses import dataclass
6
+ from datetime import datetime, timezone
6
7
  from pathlib import PurePosixPath
7
8
  from textwrap import dedent
8
9
  from typing import (
@@ -56,7 +57,7 @@ from .retries import Retries
56
57
  from .running_app import RunningApp
57
58
  from .schedule import Schedule
58
59
  from .secret import _Secret
59
- from .types import AppInfo
60
+ from .types import AppInfo, SandboxGroupStats
60
61
  from .volume import _Volume
61
62
 
62
63
  _default_image: _Image = _Image.debian_slim()
@@ -135,6 +136,82 @@ class _LocalAppState:
135
136
  volumes_default: dict[str | PurePosixPath, _Volume]
136
137
 
137
138
 
139
+ class _AppSandboxesManager:
140
+ """mdmd:namespace"""
141
+
142
+ def __init__(self, app: "_App"):
143
+ """mdmd:hidden"""
144
+ self._app = app
145
+
146
+ async def stats(
147
+ self,
148
+ *,
149
+ since: datetime,
150
+ until: datetime | None = None,
151
+ group_by_tags: Sequence[str] | None = None,
152
+ tags: dict[str, str] | None = None,
153
+ ) -> AsyncGenerator[SandboxGroupStats, None]:
154
+ """Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
155
+
156
+ The maximum time range is 7 days.
157
+
158
+ Args:
159
+ since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
160
+ timezone are interpreted as local time.
161
+ until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
162
+ the current time. Values without a timezone are interpreted as local time.
163
+ group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
164
+ a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
165
+ as a single group.
166
+ tags: Only include Sandboxes that have at least these tags.
167
+
168
+ Yields:
169
+ `SandboxGroupStats` objects, one per combination of tag values, with the following fields:
170
+ - `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
171
+ - `cost`: total cost in USD, the sum of `cost_by_resource`.
172
+ - `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
173
+ `H100`. The resource names are subject to change as Modal's billing model evolves.
174
+
175
+ Raises:
176
+ InvalidError: If the time range is invalid or there are more than 10,000 groups.
177
+
178
+ Examples:
179
+ ```python notest
180
+ app = modal.App.lookup("my-app")
181
+ since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
182
+ for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
183
+ print(group.tags, group.cost, group.cost_by_resource)
184
+ ```
185
+ """
186
+ if self._app._app_id is None:
187
+ raise InvalidError("`App.sandboxes.stats` requires a running, deployed or looked-up App.")
188
+ if until is not None:
189
+ if until.tzinfo is None:
190
+ until = until.astimezone()
191
+ until = until.astimezone(timezone.utc)
192
+ if since.tzinfo is None:
193
+ since = since.astimezone()
194
+ since = since.astimezone(timezone.utc)
195
+ if since >= datetime.now(timezone.utc):
196
+ raise InvalidError("`since` must be in the past.")
197
+ if until is not None and since >= until:
198
+ raise InvalidError("`since` must be before `until`.")
199
+ client = self._app._client or await _Client.from_env()
200
+ request = api_pb2.SandboxGetTimeRangeStatsRequest(
201
+ group_by_tags=list(group_by_tags or []), tags=tags or {}, app_id=self._app._app_id
202
+ )
203
+ request.since.FromDatetime(since)
204
+ if until is not None:
205
+ request.until.FromDatetime(until)
206
+ while True:
207
+ response = await client._stub.SandboxGetTimeRangeStats(request)
208
+ for group in response.groups:
209
+ yield SandboxGroupStats._from_proto(group)
210
+ if not response.next_page_token:
211
+ return
212
+ request.page_token = response.next_page_token
213
+
214
+
138
215
  class _App:
139
216
  """A Modal App is a group of functions and classes that are deployed together.
140
217
 
@@ -1679,6 +1756,11 @@ class _App:
1679
1756
 
1680
1757
  return _AppLogsManager(self)
1681
1758
 
1759
+ @property
1760
+ def sandboxes(self) -> _AppSandboxesManager:
1761
+ """Namespace for the Sandboxes belonging to this App."""
1762
+ return _AppSandboxesManager(self)
1763
+
1682
1764
  async def info(self, refresh: bool = False) -> AppInfo:
1683
1765
  """Return information for a modal `App`.
1684
1766
 
@@ -1711,4 +1793,5 @@ class _App:
1711
1793
  return self._info
1712
1794
 
1713
1795
 
1796
+ AppSandboxesManager = synchronize_api(_AppSandboxesManager)
1714
1797
  App = synchronize_api(_App)
@@ -1,4 +1,5 @@
1
1
  import collections.abc
2
+ import datetime
2
3
  import modal._functions
3
4
  import modal._image
4
5
  import modal._load_context
@@ -115,6 +116,54 @@ class _LocalAppState:
115
116
  """Return self==value."""
116
117
  ...
117
118
 
119
+ class _AppSandboxesManager:
120
+ """mdmd:namespace"""
121
+ def __init__(self, app: _App):
122
+ """mdmd:hidden"""
123
+ ...
124
+
125
+ def stats(
126
+ self,
127
+ *,
128
+ since: datetime.datetime,
129
+ until: typing.Optional[datetime.datetime] = None,
130
+ group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
131
+ tags: typing.Optional[dict[str, str]] = None,
132
+ ) -> collections.abc.AsyncGenerator[modal.types.SandboxGroupStats, None]:
133
+ """Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
134
+
135
+ The maximum time range is 7 days.
136
+
137
+ Args:
138
+ since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
139
+ timezone are interpreted as local time.
140
+ until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
141
+ the current time. Values without a timezone are interpreted as local time.
142
+ group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
143
+ a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
144
+ as a single group.
145
+ tags: Only include Sandboxes that have at least these tags.
146
+
147
+ Yields:
148
+ `SandboxGroupStats` objects, one per combination of tag values, with the following fields:
149
+ - `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
150
+ - `cost`: total cost in USD, the sum of `cost_by_resource`.
151
+ - `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
152
+ `H100`. The resource names are subject to change as Modal's billing model evolves.
153
+
154
+ Raises:
155
+ InvalidError: If the time range is invalid or there are more than 10,000 groups.
156
+
157
+ Examples:
158
+ ```python notest
159
+ app = modal.App.lookup("my-app")
160
+ since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
161
+ for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
162
+ print(group.tags, group.cost, group.cost_by_resource)
163
+ ```
164
+ """
165
+ ...
166
+
118
167
  class _App:
119
168
  """A Modal App is a group of functions and classes that are deployed together.
120
169
 
@@ -953,6 +1002,11 @@ class _App:
953
1002
  """
954
1003
  ...
955
1004
 
1005
+ @property
1006
+ def sandboxes(self) -> _AppSandboxesManager:
1007
+ """Namespace for the Sandboxes belonging to this App."""
1008
+ ...
1009
+
956
1010
  async def info(self, refresh: bool = False) -> modal.types.AppInfo:
957
1011
  """Return information for a modal `App`.
958
1012
 
@@ -971,6 +1025,101 @@ class _App:
971
1025
  """
972
1026
  ...
973
1027
 
1028
+ class AppSandboxesManager:
1029
+ """mdmd:namespace"""
1030
+ def __init__(self, app: App):
1031
+ """mdmd:hidden"""
1032
+ ...
1033
+
1034
+ class __stats_spec(typing_extensions.Protocol):
1035
+ def __call__(
1036
+ self,
1037
+ /,
1038
+ *,
1039
+ since: datetime.datetime,
1040
+ until: typing.Optional[datetime.datetime] = None,
1041
+ group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
1042
+ tags: typing.Optional[dict[str, str]] = None,
1043
+ ) -> typing.Generator[modal.types.SandboxGroupStats, None, None]:
1044
+ """Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
1045
+
1046
+ The maximum time range is 7 days.
1047
+
1048
+ Args:
1049
+ since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
1050
+ timezone are interpreted as local time.
1051
+ until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
1052
+ the current time. Values without a timezone are interpreted as local time.
1053
+ group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
1054
+ a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
1055
+ as a single group.
1056
+ tags: Only include Sandboxes that have at least these tags.
1057
+
1058
+ Yields:
1059
+ `SandboxGroupStats` objects, one per combination of tag values, with the following fields:
1060
+ - `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
1061
+ - `cost`: total cost in USD, the sum of `cost_by_resource`.
1062
+ - `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
1063
+ `H100`. The resource names are subject to change as Modal's billing model evolves.
1064
+
1065
+ Raises:
1066
+ InvalidError: If the time range is invalid or there are more than 10,000 groups.
1067
+
1068
+ Examples:
1069
+ ```python notest
1070
+ app = modal.App.lookup("my-app")
1071
+ since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
1072
+ for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
1073
+ print(group.tags, group.cost, group.cost_by_resource)
1074
+ ```
1075
+ """
1076
+ ...
1077
+
1078
+ def aio(
1079
+ self,
1080
+ /,
1081
+ *,
1082
+ since: datetime.datetime,
1083
+ until: typing.Optional[datetime.datetime] = None,
1084
+ group_by_tags: typing.Optional[collections.abc.Sequence[str]] = None,
1085
+ tags: typing.Optional[dict[str, str]] = None,
1086
+ ) -> collections.abc.AsyncGenerator[modal.types.SandboxGroupStats, None]:
1087
+ """Return statistics for this App's Sandboxes over a time range, optionally grouped by tag values.
1088
+
1089
+ The maximum time range is 7 days.
1090
+
1091
+ Args:
1092
+ since: The beginning of the time range, inclusive, on a UTC hour boundary. Values without a
1093
+ timezone are interpreted as local time.
1094
+ until: The end of the time range, exclusive, on a UTC hour boundary. If omitted, this defaults to
1095
+ the current time. Values without a timezone are interpreted as local time.
1096
+ group_by_tags: Tag names to group by. Sandboxes with the same values for these names form one group;
1097
+ a name not present on a Sandbox counts as `None`. If omitted, all matching Sandboxes are returned
1098
+ as a single group.
1099
+ tags: Only include Sandboxes that have at least these tags.
1100
+
1101
+ Yields:
1102
+ `SandboxGroupStats` objects, one per combination of tag values, with the following fields:
1103
+ - `tags`: the value of each name in `group_by_tags` for the group's Sandboxes, `None` where not present.
1104
+ - `cost`: total cost in USD, the sum of `cost_by_resource`.
1105
+ - `cost_by_resource`: cost in USD by billed resource: `CPU`, `Memory`, and GPU types such as
1106
+ `H100`. The resource names are subject to change as Modal's billing model evolves.
1107
+
1108
+ Raises:
1109
+ InvalidError: If the time range is invalid or there are more than 10,000 groups.
1110
+
1111
+ Examples:
1112
+ ```python notest
1113
+ app = modal.App.lookup("my-app")
1114
+ since = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) - timedelta(days=1)
1115
+ for group in app.sandboxes.stats(since=since, group_by_tags=["team"]):
1116
+ print(group.tags, group.cost, group.cost_by_resource)
1117
+ ```
1118
+ """
1119
+ ...
1120
+
1121
+ stats: __stats_spec
1122
+
974
1123
  SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
975
1124
 
976
1125
  class App:
@@ -2073,6 +2222,11 @@ class App:
2073
2222
  """
2074
2223
  ...
2075
2224
 
2225
+ @property
2226
+ def sandboxes(self) -> AppSandboxesManager:
2227
+ """Namespace for the Sandboxes belonging to this App."""
2228
+ ...
2229
+
2076
2230
  class __info_spec(typing_extensions.Protocol):
2077
2231
  def __call__(self, /, refresh: bool = False) -> modal.types.AppInfo:
2078
2232
  """Return information for a modal `App`.
@@ -5,6 +5,7 @@ from json import dumps
5
5
  from typing import Optional
6
6
 
7
7
  import click
8
+ from rich.box import SIMPLE
8
9
  from rich.table import Column, Table
9
10
  from rich.text import Text
10
11
 
@@ -21,6 +22,7 @@ from modal_proto import api_pb2
21
22
 
22
23
  from ._help import ModalGroup
23
24
  from ._logs import _validate_logs_args
25
+ from ._stats import STATS_METADATA_STYLE
24
26
  from .server import _run_server_logs, _run_server_stats, _server_stats_time_range
25
27
 
26
28
  _ENDPOINT_HELP = """
@@ -41,6 +43,11 @@ _SERVING_MODE_LABELS = {
41
43
  api_pb2.ENDPOINT_SERVING_MODE_DEDICATED: "dedicated",
42
44
  api_pb2.ENDPOINT_SERVING_MODE_SHARED: "shared",
43
45
  }
46
+ _INPUT_MODALITY_LABELS = {
47
+ api_pb2.ENDPOINT_INPUT_MODALITY_TEXT: "text",
48
+ api_pb2.ENDPOINT_INPUT_MODALITY_IMAGE: "image",
49
+ api_pb2.ENDPOINT_INPUT_MODALITY_AUDIO: "audio",
50
+ }
44
51
  _ENDPOINT_STATUS_LABELS = {
45
52
  api_pb2.EndpointGetInfoResponse.ENDPOINT_STATUS_PROVISIONING: "provisioning",
46
53
  api_pb2.EndpointGetInfoResponse.ENDPOINT_STATUS_LIVE: "live",
@@ -502,6 +509,108 @@ async def stop(
502
509
  output.print(f"[green]✓[/green] Stopped Endpoint {endpoint_id}.")
503
510
 
504
511
 
512
+ @endpoint_cli.command("library", panel="Inspection")
513
+ @click.option(
514
+ "--input-modality",
515
+ type=click.Choice(["text", "image", "audio"]),
516
+ multiple=True,
517
+ help="Only show models that accept any of these inputs. Can be repeated.",
518
+ )
519
+ @click.option("--json", "json_output", is_flag=True, help="Output models as JSON.")
520
+ @synchronizer.create_blocking
521
+ async def library(*, input_modality: tuple[str, ...] = (), json_output: bool = False) -> None:
522
+ """List models you can serve with Modal Endpoints.
523
+
524
+ Models are listed by their Hugging Face repo ID, with the input types they
525
+ accept and whether they're available as shared endpoints, dedicated
526
+ endpoints, or both.
527
+
528
+ This is Modal's catalog of supported models, not the Endpoints in your
529
+ workspace. To see those, run `modal endpoint list`.
530
+
531
+ Examples:
532
+
533
+ List models that support text input modality:
534
+
535
+ ```
536
+ modal endpoint library --input-modality text
537
+ ```
538
+
539
+ Pick a model ID and then create a shared endpoint:
540
+
541
+ ```
542
+ modal endpoint create --model Qwen/Qwen3.8-27B --shared
543
+ ```
544
+ """
545
+ client = await _Client.from_env()
546
+ response = await client._stub.EndpointGetLibrary(api_pb2.EndpointGetLibraryRequest())
547
+ modalities = {modality for modality, label in _INPUT_MODALITY_LABELS.items() if label in input_modality}
548
+
549
+ models = sorted(
550
+ (
551
+ model
552
+ for model in response.models
553
+ if (not modalities or modalities.intersection(model.supported_input_modalities))
554
+ ),
555
+ key=lambda model: model.base_model_repo_id,
556
+ )
557
+ output = OutputManager.get()
558
+ if json_output:
559
+ output.print_json(
560
+ dumps(
561
+ [
562
+ {
563
+ "base_model_repo_id": model.base_model_repo_id,
564
+ "input_modalities": [
565
+ _INPUT_MODALITY_LABELS.get(modality, "unknown")
566
+ for modality in model.supported_input_modalities
567
+ ],
568
+ "shared": api_pb2.ENDPOINT_SERVING_MODE_SHARED in model.available_serving_modes,
569
+ "dedicated": api_pb2.ENDPOINT_SERVING_MODE_DEDICATED in model.available_serving_modes,
570
+ }
571
+ for model in models
572
+ ]
573
+ )
574
+ )
575
+ return
576
+ output.print(
577
+ Text(
578
+ f"Displaying {len(models):,} {'model' if len(models) == 1 else 'models'}",
579
+ style=STATS_METADATA_STYLE,
580
+ )
581
+ )
582
+ if not models:
583
+ output.print(
584
+ "No endpoint-compatible models match the filters."
585
+ if input_modality
586
+ else "No endpoint-compatible models are available."
587
+ )
588
+ return
589
+ display_table(
590
+ [
591
+ Column("Model ID", overflow="fold"),
592
+ "Input Modalities",
593
+ Column("Shared", justify="center"),
594
+ Column("Dedicated", justify="center"),
595
+ ],
596
+ [
597
+ (
598
+ Text.assemble(
599
+ (model.base_model_repo_id, f"link https://huggingface.co/{model.base_model_repo_id}"),
600
+ (" ↗", "bright_blue not underline"),
601
+ ),
602
+ ", ".join(
603
+ _INPUT_MODALITY_LABELS.get(modality, "unknown") for modality in model.supported_input_modalities
604
+ ),
605
+ "✓" if api_pb2.ENDPOINT_SERVING_MODE_SHARED in model.available_serving_modes else "—",
606
+ "✓" if api_pb2.ENDPOINT_SERVING_MODE_DEDICATED in model.available_serving_modes else "—",
607
+ )
608
+ for model in models
609
+ ],
610
+ table_box=SIMPLE,
611
+ )
612
+
613
+
505
614
  @endpoint_cli.command("info", panel="Inspection", no_args_is_help=True)
506
615
  @click.argument("endpoint_identifier")
507
616
  @env_option
@@ -4,21 +4,27 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import json as json_module
7
+ import os
7
8
  from collections.abc import AsyncIterator
8
9
  from typing import Optional
9
10
 
10
11
  import click
12
+ from rich.syntax import Syntax
11
13
 
12
14
  from modal._environments import ensure_env
13
- from modal._image import _upgrade_image_name
15
+ from modal._image import _Image, _infer_registry_auth_type, _upgrade_image_name
14
16
  from modal._logs_manager import _resolve_source
15
17
  from modal._utils.async_utils import synchronizer
16
18
  from modal._utils.time_utils import timestamp_to_localized_str
19
+ from modal.app import _App
17
20
  from modal.cli.utils import display_table, env_option
18
21
  from modal.client import _Client
19
22
  from modal.config import config
23
+ from modal.exception import InvalidError
20
24
  from modal.image import Image
21
25
  from modal.output import OutputManager
26
+ from modal.runner import _run_app
27
+ from modal.secret import _Secret
22
28
  from modal_proto import api_pb2
23
29
 
24
30
  from ._help import ModalGroup
@@ -36,6 +42,118 @@ def _print_result_summary(count: int) -> None:
36
42
  OutputManager.get().print(f"Showing {count} {result_label}")
37
43
 
38
44
 
45
+ async def _registry_secret(
46
+ secret_name: Optional[str], secret_vars: Optional[str], env: str
47
+ ) -> tuple[Optional[_Secret], set[str]]:
48
+ if secret_name and secret_vars:
49
+ raise click.UsageError("--registry-secret and --registry-secret-from-env cannot be used together.")
50
+ if secret_name:
51
+ secret = _Secret.from_name(secret_name, environment_name=env)
52
+ return secret, await secret._get_keys()
53
+ keys = [key.strip() for key in (secret_vars or "").split(",") if key.strip()]
54
+ if not keys:
55
+ return None, set()
56
+ return _Secret.from_local_environ(keys), set(keys)
57
+
58
+
59
+ @image_cli.command("build", no_args_is_help=True)
60
+ @click.argument("context_dir", metavar="CONTEXT_DIR")
61
+ @click.option(
62
+ "-t",
63
+ "--name",
64
+ "name",
65
+ metavar="[ENV/]NAME[:TAG]",
66
+ required=True,
67
+ help="Name to publish the Image as, e.g. `my-image`, `my-image:v2`, or `my-env/my-image`.",
68
+ )
69
+ @click.option(
70
+ "-f",
71
+ "--file",
72
+ "dockerfile",
73
+ type=click.Path(exists=True, dir_okay=False),
74
+ default=None,
75
+ help="Path to the Dockerfile. Defaults to `CONTEXT_DIR/Dockerfile`.",
76
+ )
77
+ @click.option(
78
+ "--registry-secret",
79
+ "registry_secret_name",
80
+ metavar="SECRET_NAME",
81
+ default=None,
82
+ help="Name of a Secret with credentials for pulling the base image from a private registry.",
83
+ )
84
+ @click.option(
85
+ "--registry-secret-from-env",
86
+ "registry_secret_vars",
87
+ default=None,
88
+ metavar="VAR[,VAR...]",
89
+ help=(
90
+ "Comma-separated local environment variable names holding registry credentials. "
91
+ "Cannot be combined with --registry-secret."
92
+ ),
93
+ )
94
+ @click.option("--force-build", is_flag=True, help="Rebuild every layer of the Image, ignoring cached builds.")
95
+ @env_option
96
+ @synchronizer.create_blocking
97
+ async def build(
98
+ context_dir: str,
99
+ name: str,
100
+ dockerfile: Optional[str],
101
+ registry_secret_name: Optional[str],
102
+ registry_secret_vars: Optional[str],
103
+ force_build: bool,
104
+ env: Optional[str],
105
+ ) -> None:
106
+ """Build an Image from a Dockerfile and publish it as NAME.
107
+
108
+ CONTEXT_DIR is the build context used to resolve relative COPY paths. Use `.` for the current directory.
109
+
110
+ NAME defaults to the `latest` tag. `--env` sets the environment the build runs in and where
111
+ `--registry-secret` is looked up. The Image is published into that environment unless NAME has an
112
+ environment prefix, e.g. `my-env/my-image`.
113
+
114
+ Examples:
115
+
116
+ ```
117
+ modal image build -t my-image .
118
+ modal image build --name my-image:v2 -f docker/Dockerfile .
119
+ modal image build -t my-env/my-image .
120
+ modal image build -t my-image --registry-secret my-registry-creds .
121
+ modal image build -t my-image --registry-secret-from-env REGISTRY_USERNAME,REGISTRY_PASSWORD .
122
+ ```
123
+ """
124
+ if not os.path.isdir(context_dir):
125
+ raise click.UsageError(f"CONTEXT_DIR {context_dir!r} is not a directory.")
126
+ if dockerfile is None:
127
+ dockerfile = os.path.join(context_dir, "Dockerfile")
128
+ if not os.path.isfile(dockerfile):
129
+ raise click.UsageError(f"No Dockerfile found at {dockerfile}. Use --file to specify one.")
130
+ env = ensure_env(env)
131
+ try:
132
+ tag = _upgrade_image_name(name, fallback_environment_name=env)
133
+ except InvalidError as exc:
134
+ raise click.UsageError(str(exc))
135
+
136
+ secret, keys = await _registry_secret(registry_secret_name, registry_secret_vars, env)
137
+ if secret is not None:
138
+ # Fail before creating the app; from_dockerfile runs the same check during the build
139
+ try:
140
+ _infer_registry_auth_type(keys)
141
+ except InvalidError as exc:
142
+ raise click.UsageError(str(exc)) from exc
143
+ image = _Image.from_dockerfile(dockerfile, context_dir=context_dir, registry_secret=secret, force_build=force_build)
144
+ app = _App("modal-image-build")
145
+ # Only show the Image build output, which includes its logs
146
+ async with _run_app(app, environment_name=env, stream_output=False):
147
+ await image.build(app)
148
+ await image.publish(tag)
149
+
150
+ output_mgr = OutputManager.get()
151
+ output_mgr.step_completed(f"Image built: [bold]{image.object_id}[/bold]")
152
+ output_mgr.step_completed(f"Image published as [bold magenta]{tag}[/bold magenta]! 🎉")
153
+ output_mgr.print("\nUse it with:\n")
154
+ output_mgr.print(Syntax(f'modal.Image.from_name("{tag}")', "python"))
155
+
156
+
39
157
  @image_cli.command("logs", no_args_is_help=True)
40
158
  @click.argument("image_id")
41
159
  @click.option(