modal 1.5.6.dev5__tar.gz → 1.5.6.dev7__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 (236) hide show
  1. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_function_variants.py +77 -0
  3. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_functions.py +74 -8
  4. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_object.py +2 -0
  5. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/function_utils.py +29 -0
  6. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/grpc_utils.py +17 -1
  7. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/task_command_router_client.py +1 -1
  8. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/app.py +35 -0
  9. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/app.pyi +60 -0
  10. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/app.py +77 -0
  11. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/client.pyi +2 -2
  12. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cloud_bucket_mount.py +6 -4
  13. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cloud_bucket_mount.pyi +3 -1
  14. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cls.py +15 -0
  15. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/functions.pyi +43 -10
  16. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox.py +1 -1
  17. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/schedule.py +1 -1
  18. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/types.py +270 -0
  19. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/PKG-INFO +1 -1
  20. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2.py +815 -815
  21. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2.pyi +6 -2
  22. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_version/__init__.py +1 -1
  23. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/LICENSE +0 -0
  24. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/README.md +0 -0
  25. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/__init__.py +0 -0
  26. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/__main__.py +0 -0
  27. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_billing.py +0 -0
  28. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_clustered_functions.py +0 -0
  29. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_clustered_functions.pyi +0 -0
  30. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_container_entrypoint.py +0 -0
  31. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_environments.py +0 -0
  32. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_grpc_client.py +0 -0
  33. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_image.py +0 -0
  34. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_ipython.py +0 -0
  35. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_load_context.py +0 -0
  36. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_location.py +0 -0
  37. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs.py +0 -0
  38. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs_manager.py +0 -0
  39. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_logs_manager.pyi +0 -0
  40. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/__init__.py +0 -0
  41. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/manager.py +0 -0
  42. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/pty.py +0 -0
  43. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/rich.py +0 -0
  44. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_output/status.py +0 -0
  45. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_partial_function.py +0 -0
  46. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_resolver.py +0 -0
  47. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_resources.py +0 -0
  48. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/__init__.py +0 -0
  49. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/asgi.py +0 -0
  50. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/container_io_manager.py +0 -0
  51. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/container_io_manager.pyi +0 -0
  52. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/execution_context.py +0 -0
  53. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/execution_context.pyi +0 -0
  54. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  55. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/task_lifecycle_manager.py +0 -0
  56. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  57. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/telemetry.py +0 -0
  58. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/user_code_event_loop.py +0 -0
  59. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_runtime/user_code_imports.py +0 -0
  60. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_serialization.py +0 -0
  61. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_server.py +0 -0
  62. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_supports_logs.py +0 -0
  63. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_traceback.py +0 -0
  64. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_tunnel.py +0 -0
  65. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_tunnel.pyi +0 -0
  66. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_type_manager.py +0 -0
  67. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/__init__.py +0 -0
  68. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/app_utils.py +0 -0
  69. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/async_utils.py +0 -0
  70. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/auth_token_manager.py +0 -0
  71. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/blob_utils.py +0 -0
  72. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/browser_utils.py +0 -0
  73. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/bytes_io_segment_payload.py +0 -0
  74. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/curl_utils.py +0 -0
  75. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/deprecation.py +0 -0
  76. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/display_utils.py +0 -0
  77. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/docker_utils.py +0 -0
  78. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/git_utils.py +0 -0
  79. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/grpc_testing.py +0 -0
  80. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/hash_utils.py +0 -0
  81. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/http_utils.py +0 -0
  82. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/jwt_utils.py +0 -0
  83. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/logger.py +0 -0
  84. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/mount_utils.py +0 -0
  85. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/name_utils.py +0 -0
  86. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/package_utils.py +0 -0
  87. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/pattern_utils.py +0 -0
  88. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/proxy_support.py +0 -0
  89. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/rand_pb_testing.py +0 -0
  90. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/sandbox_fs_utils.py +0 -0
  91. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/shell_utils.py +0 -0
  92. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_utils/time_utils.py +0 -0
  93. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/__init__.py +0 -0
  94. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  95. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/cloudpickle.py +0 -0
  96. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/tblib.py +0 -0
  97. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_vendor/version.py +0 -0
  98. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_watcher.py +0 -0
  99. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/_workspace.py +0 -0
  100. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/billing.py +0 -0
  101. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/billing.pyi +0 -0
  102. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2023.12.312.txt +0 -0
  103. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2023.12.txt +0 -0
  104. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2024.04.txt +0 -0
  105. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2024.10.txt +0 -0
  106. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/2025.06.txt +0 -0
  107. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/PREVIEW.txt +0 -0
  108. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/README.md +0 -0
  109. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/builder/base-images.json +0 -0
  110. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/call_graph.py +0 -0
  111. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/__init__.py +0 -0
  112. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_download.py +0 -0
  113. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_help.py +0 -0
  114. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/_traceback.py +0 -0
  115. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/billing.py +0 -0
  116. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/bootstrap.py +0 -0
  117. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/changelog.py +0 -0
  118. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/cluster.py +0 -0
  119. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/config.py +0 -0
  120. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/container.py +0 -0
  121. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/curl.py +0 -0
  122. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/dashboard.py +0 -0
  123. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/dict.py +0 -0
  124. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/endpoint.py +0 -0
  125. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/entry_point.py +0 -0
  126. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/environment.py +0 -0
  127. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/image.py +0 -0
  128. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/import_refs.py +0 -0
  129. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/launch.py +0 -0
  130. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/logo.py +0 -0
  131. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/network_file_system.py +0 -0
  132. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/profile.py +0 -0
  133. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/__init__.py +0 -0
  134. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/run_jupyter.py +0 -0
  135. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/programs/vscode.py +0 -0
  136. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/queues.py +0 -0
  137. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/run.py +0 -0
  138. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/secret.py +0 -0
  139. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/selector.py +0 -0
  140. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/shell.py +0 -0
  141. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/skills.py +0 -0
  142. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/token.py +0 -0
  143. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/utils.py +0 -0
  144. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/volume.py +0 -0
  145. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cli/workspace.py +0 -0
  146. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/client.py +0 -0
  147. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/cls.pyi +0 -0
  148. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/config.py +0 -0
  149. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/container_process.py +0 -0
  150. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/container_process.pyi +0 -0
  151. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/dict.py +0 -0
  152. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/dict.pyi +0 -0
  153. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/environments.py +0 -0
  154. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/environments.pyi +0 -0
  155. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/exception.py +0 -0
  156. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/__init__.py +0 -0
  157. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/flash.py +0 -0
  158. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/flash.pyi +0 -0
  159. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/experimental/ipython.py +0 -0
  160. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_io.py +0 -0
  161. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_io.pyi +0 -0
  162. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/file_pattern_matcher.py +0 -0
  163. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/functions.py +0 -0
  164. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/image.py +0 -0
  165. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/image.pyi +0 -0
  166. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/io_streams.py +0 -0
  167. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/io_streams.pyi +0 -0
  168. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/mount.py +0 -0
  169. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/mount.pyi +0 -0
  170. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/network_file_system.py +0 -0
  171. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/network_file_system.pyi +0 -0
  172. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/object.py +0 -0
  173. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/object.pyi +0 -0
  174. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/output.py +0 -0
  175. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/parallel_map.py +0 -0
  176. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/parallel_map.pyi +0 -0
  177. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/partial_function.py +0 -0
  178. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/partial_function.pyi +0 -0
  179. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/proxy.py +0 -0
  180. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/proxy.pyi +0 -0
  181. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/py.typed +0 -0
  182. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/queue.py +0 -0
  183. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/queue.pyi +0 -0
  184. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/retries.py +0 -0
  185. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/runner.py +0 -0
  186. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/runner.pyi +0 -0
  187. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/running_app.py +0 -0
  188. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox.pyi +0 -0
  189. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox_fs.py +0 -0
  190. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/sandbox_fs.pyi +0 -0
  191. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/scheduler_placement.py +0 -0
  192. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/secret.py +0 -0
  193. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/secret.pyi +0 -0
  194. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/server.py +0 -0
  195. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/server.pyi +0 -0
  196. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/serving.py +0 -0
  197. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/serving.pyi +0 -0
  198. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/skills/modal/SKILL.md +0 -0
  199. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/snapshot.py +0 -0
  200. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/snapshot.pyi +0 -0
  201. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/stream_type.py +0 -0
  202. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/token_flow.py +0 -0
  203. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/token_flow.pyi +0 -0
  204. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/volume.py +0 -0
  205. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/volume.pyi +0 -0
  206. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/workspace.py +0 -0
  207. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal/workspace.pyi +0 -0
  208. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/SOURCES.txt +0 -0
  209. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/dependency_links.txt +0 -0
  210. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/entry_points.txt +0 -0
  211. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/requires.txt +0 -0
  212. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal.egg-info/top_level.txt +0 -0
  213. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/__init__.py +0 -0
  214. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_cli_docs.py +0 -0
  215. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_cli_docs_main.py +0 -0
  216. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_reference_docs.py +0 -0
  217. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/gen_reference_docs_main.py +0 -0
  218. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/__init__.py +0 -0
  219. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/mdmd.py +0 -0
  220. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/signatures.py +0 -0
  221. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/mdmd/types.py +0 -0
  222. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_docs/reference_intro.md +0 -0
  223. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/__init__.py +0 -0
  224. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_grpc.py +0 -0
  225. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2_grpc.py +0 -0
  226. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/api_pb2_grpc.pyi +0 -0
  227. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/modal_api_grpc.py +0 -0
  228. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/py.typed +0 -0
  229. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_grpc.py +0 -0
  230. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2.py +0 -0
  231. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2.pyi +0 -0
  232. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  233. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  234. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/modal_version/__main__.py +0 -0
  235. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/pyproject.toml +0 -0
  236. {modal-1.5.6.dev5 → modal-1.5.6.dev7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev5
3
+ Version: 1.5.6.dev7
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -4,6 +4,7 @@ from collections.abc import Collection, Sequence, Sized
4
4
  from pathlib import PurePosixPath
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from modal.types import CloudBucketMountInfo, FunctionInfo, VolumeMountInfo
7
8
  from modal_proto import api_pb2
8
9
 
9
10
  from ._resources import convert_fn_config_to_resources_config
@@ -248,6 +249,7 @@ def _make_function_variant(
248
249
  ):
249
250
  if not base_function._is_hydrated:
250
251
  await base_function.hydrate(load_context.client)
252
+
251
253
  assert base_function._client and base_function._client.stub
252
254
 
253
255
  if parameter_schema is None:
@@ -278,6 +280,12 @@ def _make_function_variant(
278
280
  response = await _function_bind_params_cached(base_function, req)
279
281
  function_variant._hydrate(response.bound_function_id, base_function._client, response.handle_metadata)
280
282
 
283
+ if base_function._function_info is not None:
284
+ if options is not None:
285
+ function_variant._function_info = _get_function_info_with_options(base_function._function_info, options)
286
+ else:
287
+ function_variant._function_info = base_function._function_info._get_copy()
288
+
281
289
  def _deps():
282
290
  if options:
283
291
  return options._unhydrated_object_deps()
@@ -292,4 +300,73 @@ def _make_function_variant(
292
300
  )
293
301
  fun._source_info = base_function._source_info
294
302
  fun._spec = base_function._spec # TODO (elias): fix - this is incorrect when using with_options
303
+
304
+ # If we already have a ._function_info for the base function, we can pass it down - otherwise this
305
+ # will be filled in during hydration
306
+ if base_function._function_info is not None:
307
+ if options is not None:
308
+ fun._function_info = _get_function_info_with_options(base_function._function_info, options)
309
+ else:
310
+ fun._function_info = base_function._function_info._get_copy()
311
+
295
312
  return fun
313
+
314
+
315
+ def _get_function_info_with_options(info: FunctionInfo, options: _FunctionOptions):
316
+ new_info = info._get_copy()
317
+
318
+ if options.resources is not None:
319
+ res = options.resources
320
+ if res.milli_cpu_max > 0:
321
+ new_info.cpu = (res.milli_cpu / 1000, res.milli_cpu_max / 1000)
322
+ elif res.milli_cpu > 0:
323
+ new_info.cpu = res.milli_cpu / 1000
324
+
325
+ if res.memory_mb_max > 0:
326
+ new_info.memory_mib = (res.memory_mb, res.memory_mb_max)
327
+ elif res.memory_mb > 0:
328
+ new_info.memory_mib = res.memory_mb
329
+
330
+ if res.HasField("gpu_config") and res.gpu_config.count > 0:
331
+ new_info.gpus = [(res.gpu_config.count, res.gpu_config.gpu_type)]
332
+
333
+ if res.ephemeral_disk_mb > 0:
334
+ new_info.ephemeral_disk_mib = res.ephemeral_disk_mb
335
+
336
+ if options.scheduler_placement is not None:
337
+ sp = options.scheduler_placement
338
+ # Note: not putting `nonpreemptible` here as that is not currently possible to set via
339
+ # `.with_options(...)`
340
+ if len(sp.regions) > 0:
341
+ new_info.regions = list(sp.regions)
342
+
343
+ if options.cloud is not None:
344
+ new_info.cloud = options.cloud
345
+
346
+ if len(options.validated_volumes) > 0:
347
+ new_info.volumes = {
348
+ mount_path: VolumeMountInfo(vol._name, vol._object_id, False, None)
349
+ if vol._mount_options is None
350
+ else VolumeMountInfo(
351
+ vol._name,
352
+ vol._object_id,
353
+ vol._mount_options.read_only,
354
+ vol._mount_options.sub_path,
355
+ )
356
+ for mount_path, vol in options.validated_volumes
357
+ }
358
+
359
+ if len(options.cloud_bucket_mounts) > 0:
360
+ new_info.cloud_bucket_mounts = {}
361
+ protos, _ = cloud_bucket_mounts_to_proto(options.cloud_bucket_mounts, include_secrets=False)
362
+
363
+ for proto in protos:
364
+ new_info.cloud_bucket_mounts[proto.mount_path] = CloudBucketMountInfo._from_proto(proto)
365
+
366
+ if len(options.secrets) > 0:
367
+ new_info.secrets = [repr(s) for s in options.secrets]
368
+
369
+ if options.routing_region:
370
+ new_info.routing_region = options.routing_region
371
+
372
+ return new_info
@@ -16,6 +16,7 @@ from google.protobuf.message import Message
16
16
  from grpclib import Status
17
17
  from synchronicity.combined_types import MethodWithAio
18
18
 
19
+ from modal.types import CloudBucketMountInfo, FunctionInfo, HttpInfo, VolumeMountInfo
19
20
  from modal_proto import api_pb2
20
21
  from modal_proto.modal_api_grpc import ModalClientModal
21
22
 
@@ -56,6 +57,7 @@ from ._utils.function_utils import (
56
57
  _process_result,
57
58
  _stream_function_call_data,
58
59
  get_function_type,
60
+ get_schedule_str,
59
61
  is_async,
60
62
  normalize_fractional_target_concurrency,
61
63
  parse_gpu_config,
@@ -636,6 +638,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
636
638
  _options: _FunctionOptions
637
639
  _base_function: "_Function | None" = None
638
640
  _app_id: str | None = None
641
+ _function_info: FunctionInfo | None = None
639
642
 
640
643
  async def _get_log_query_data(self) -> _LogQueryData:
641
644
  await self.hydrate()
@@ -1158,6 +1161,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1158
1161
  serve_mounts = {m for m in all_mounts if m.is_local()}
1159
1162
  serve_mounts |= image._serve_mounts
1160
1163
  obj._serve_mounts = frozenset(serve_mounts)
1164
+ obj._function_info = FunctionInfo._from_function_proto(response.function_data)
1165
+
1161
1166
  self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1162
1167
 
1163
1168
  rep = f"Function({tag})"
@@ -1193,6 +1198,36 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1193
1198
  obj._build_args["cloud"] = repr(cloud)
1194
1199
  obj._build_args["scheduler_placement"] = repr(scheduler_placement)
1195
1200
 
1201
+ obj._function_info = FunctionInfo(
1202
+ cpu=cpu,
1203
+ memory_mib=memory,
1204
+ gpus=[(parsed.count, parsed.gpu_type) for parsed in (parse_gpu_config(g) for g in gpus)],
1205
+ ephemeral_disk_mib=ephemeral_disk,
1206
+ nonpreemptible=nonpreemptible,
1207
+ regions=None if region is None else [region] if isinstance(region, str) else list(region),
1208
+ routing_region=routing_region,
1209
+ cloud=cloud,
1210
+ schedule=None if schedule is None else get_schedule_str(schedule.proto_message),
1211
+ volumes={
1212
+ mount_path: VolumeMountInfo(vol._name, vol._object_id, False, None)
1213
+ if vol._mount_options is None
1214
+ else VolumeMountInfo(
1215
+ vol._name,
1216
+ vol._object_id,
1217
+ vol._mount_options.read_only,
1218
+ vol._mount_options.sub_path,
1219
+ )
1220
+ for mount_path, vol in validated_volumes_no_cloud_buckets
1221
+ },
1222
+ cloud_bucket_mounts={
1223
+ cbm.mount_path: CloudBucketMountInfo._from_proto(cbm)
1224
+ for cbm in cloud_bucket_mounts_to_proto(cloud_bucket_mounts, include_secrets=False)[0]
1225
+ },
1226
+ # We don't have any secret IDs yet as we are dehydrated, so we return the reprs instead
1227
+ secrets=[repr(s) for s in secrets],
1228
+ http_info=HttpInfo._from_proto(http_config) if http_config else None,
1229
+ )
1230
+
1196
1231
  return obj
1197
1232
 
1198
1233
  from_local = with_deprecation_warning(
@@ -1332,6 +1367,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1332
1367
  print_server_warnings(response.server_warnings)
1333
1368
 
1334
1369
  self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1370
+ self._function_info = FunctionInfo._from_function_proto(response.function)
1335
1371
 
1336
1372
  environment_rep = (
1337
1373
  f", environment_name={load_context_overrides.environment_name!r}"
@@ -1424,14 +1460,44 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1424
1460
  # Deprecated soon, only for backwards compatibility
1425
1461
  return self.app
1426
1462
 
1427
- @property
1428
- @with_deprecation_warning(
1429
- (2026, 8, 26),
1430
- "`Function.info` is deprecated and will be removed in `modal` version 1.6.0",
1431
- )
1432
- def info(self) -> FunctionSourceInfo:
1433
- """mdmd:hidden"""
1434
- return self._source_info_
1463
+ async def info(self, *, refresh: bool = False) -> FunctionInfo:
1464
+ """Get an overview of a Function's resource requests, associated mounts, etc.
1465
+
1466
+ This method performs a network request to populate this information if the Function handle is
1467
+ a remote lookup whose information has not yet been fetched (e.g. from `Function.from_name(...)`),
1468
+ or if `refresh=True`.
1469
+
1470
+ Args:
1471
+ refresh: Always perform a network request. Pass `refresh=True` to ensure that this method
1472
+ returns the most up to date information.
1473
+
1474
+ Returns:
1475
+ This returns a [`modal.types.FunctionInfo`](https://modal.com/docs/sdk/py/latest/types#FunctionInfo)
1476
+ dataclass.
1477
+ """
1478
+
1479
+ if self._function_info is not None and not refresh:
1480
+ return self._function_info
1481
+
1482
+ # For some constructors, e.g. Function.from_name(), hydration will set self._function_info
1483
+ if not self._is_hydrated:
1484
+ await self.hydrate()
1485
+
1486
+ # Other constructors, such as `Function._new_hydrated()`, don't set self._function_info on
1487
+ # hydration, so we have to do an RPC to get that information
1488
+ if refresh or self._function_info is None:
1489
+ # Accessing `.client` or `.object_id` here can't throw since we only get to this branch on a
1490
+ # hydrated handle
1491
+ response = await self.client.stub.FunctionGetById(
1492
+ api_pb2.FunctionGetByIdRequest(
1493
+ function_id=self.object_id,
1494
+ )
1495
+ )
1496
+
1497
+ self._function_info = FunctionInfo._from_function_proto(response.function)
1498
+
1499
+ assert self._function_info is not None
1500
+ return self._function_info._get_copy()
1435
1501
 
1436
1502
  @property
1437
1503
  def _source_info_(self) -> FunctionSourceInfo:
@@ -358,6 +358,7 @@ class _Object:
358
358
 
359
359
  *Added in v0.72.39*: This method replaces the deprecated `.resolve()` method.
360
360
  """
361
+
361
362
  # TODO: add deprecation for the client argument here - should be added in constructors instead
362
363
  if self._is_hydrated:
363
364
  if self.client._snapshotted and not self._is_rehydrated:
@@ -388,4 +389,5 @@ class _Object:
388
389
  root_load_context = LoadContext(client=client, task_context=tc)
389
390
  with suppress_tb_frame(): # skip this frame by default
390
391
  await resolver.load(self, root_load_context)
392
+
391
393
  return self
@@ -668,3 +668,32 @@ def parse_gpu_config(value: str | None) -> api_pb2.GPUConfig:
668
668
  return api_pb2.GPUConfig(gpu_type=gpu_type, count=count)
669
669
  else:
670
670
  raise InvalidError(f"Invalid GPU config: {value!r}. Value must be a string or `None`")
671
+
672
+
673
+ def get_schedule_str(schedule: api_pb2.Schedule) -> str | None:
674
+ if schedule.WhichOneof("schedule_oneof") is None:
675
+ return None
676
+
677
+ if schedule.WhichOneof("schedule_oneof") == "cron":
678
+ cron = schedule.cron
679
+ return f"Cron({cron.cron_string!r}, {cron.timezone})"
680
+
681
+ period = schedule.period
682
+
683
+ period_args = []
684
+ if period.years > 0:
685
+ period_args.append(f"years={period.years}")
686
+ if period.months > 0:
687
+ period_args.append(f"months={period.months}")
688
+ if period.weeks > 0:
689
+ period_args.append(f"weeks={period.weeks}")
690
+ if period.days > 0:
691
+ period_args.append(f"days={period.days}")
692
+ if period.hours > 0:
693
+ period_args.append(f"hours={period.hours}")
694
+ if period.minutes > 0:
695
+ period_args.append(f"minutes={period.minutes}")
696
+ if period.seconds > 0:
697
+ period_args.append(f"seconds={period.seconds}")
698
+
699
+ return f"Period({', '.join(period_args)})"
@@ -272,18 +272,34 @@ class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
272
272
  custom_detail_codec = CustomProtoStatusDetailsCodec()
273
273
 
274
274
 
275
- def create_channel_config() -> grpclib.config.Configuration:
275
+ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.config.Configuration:
276
276
  """Shared grpclib channel settings for Modal client connections.
277
277
 
278
278
  HTTP/2 keepalive probes keep idle transports warm through stateful
279
279
  middleboxes and surface dead connections before retries reuse them.
280
+
281
+ `sustained_keepalive` clears the two grpclib defaults such that a probe
282
+ actually goes out every `_keepalive_time`, for as long as the connection is
283
+ open.
280
284
  """
285
+ ping_throttle_overrides: dict[str, Any] = {}
286
+ if sustained_keepalive:
287
+ ping_throttle_overrides = {
288
+ # Default 2: probing stops after two probes with no request sent in
289
+ # between. Requests reset the count, acks don't. 0 = no cap.
290
+ "_http2_max_pings_without_data": 0,
291
+ # Default 300s floor between probes, applied regardless of data
292
+ # despite the name, so it has to stay under `_keepalive_time`.
293
+ "_http2_min_sent_ping_interval_without_data": GRPC_KEEPALIVE_TIME_SECS / 2,
294
+ }
295
+
281
296
  return grpclib.config.Configuration(
282
297
  _keepalive_time=GRPC_KEEPALIVE_TIME_SECS,
283
298
  _keepalive_timeout=GRPC_KEEPALIVE_TIMEOUT_SECS,
284
299
  _keepalive_permit_without_calls=True,
285
300
  http2_connection_window_size=GRPC_WINDOW_SIZE,
286
301
  http2_stream_window_size=GRPC_WINDOW_SIZE,
302
+ **ping_throttle_overrides,
287
303
  )
288
304
 
289
305
 
@@ -251,7 +251,7 @@ class TaskCommandRouterClient:
251
251
  host,
252
252
  port,
253
253
  ssl=ssl_context,
254
- config=create_channel_config(),
254
+ config=create_channel_config(sustained_keepalive=True),
255
255
  closed_error_message="Unable to perform operation on a detached sandbox",
256
256
  )
257
257
 
@@ -56,6 +56,7 @@ from .retries import Retries
56
56
  from .running_app import RunningApp
57
57
  from .schedule import Schedule
58
58
  from .secret import _Secret
59
+ from .types import AppInfo
59
60
  from .volume import _Volume
60
61
 
61
62
  _default_image: _Image = _Image.debian_slim()
@@ -182,6 +183,7 @@ class _App:
182
183
  # Metadata for loading objects within this app
183
184
  # passed by reference to functions and classes so it can be updated by run()/deploy()
184
185
  _root_load_context: LoadContext
186
+ _info: AppInfo | None
185
187
 
186
188
  @property
187
189
  def _local_state(self) -> _LocalAppState:
@@ -366,6 +368,7 @@ class _App:
366
368
  app._client = client
367
369
  app._root_load_context = LoadContext(client=client, environment_name=environment_name, app_id=response.app_id)
368
370
  app._running_app = RunningApp(response.app_id, interactive=False)
371
+ app._info = AppInfo._from_proto(response.handle_metadata, response.app_id)
369
372
  return app
370
373
 
371
374
  async def get_dashboard_url(self) -> str:
@@ -1655,5 +1658,37 @@ class _App:
1655
1658
 
1656
1659
  return _AppLogsManager(self)
1657
1660
 
1661
+ async def info(self, refresh: bool = False) -> AppInfo:
1662
+ """Return information for a modal `App`.
1663
+
1664
+ The information returned includes the App's ID, member functions and servers,
1665
+ as well as basic lifecycle information, e.g. who created the app and when.
1666
+
1667
+ Args:
1668
+ refresh: Whether to fetch the latest info. By default, false, so
1669
+ the info corresponds the App state at the time of the previous lookup.
1670
+
1671
+ See also:
1672
+ - [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
1673
+
1674
+ Returns:
1675
+ `AppInfo` object.
1676
+ """
1677
+ if hasattr(self, "_info") and self._info and not refresh:
1678
+ return AppInfo(
1679
+ app_id=self._info.app_id,
1680
+ description=self._info.description,
1681
+ functions=self._info.functions,
1682
+ servers=self._info.servers,
1683
+ lifecycle=self._info.lifecycle,
1684
+ )
1685
+ if not self._app_id:
1686
+ raise InvalidError("`app.info` requires a running or stopped app.")
1687
+ request = api_pb2.AppGetInfoRequest(app_id=self._app_id)
1688
+ client = self._client or await _Client.from_env()
1689
+ resp = await client.stub.AppGetInfo(request)
1690
+ self._info = AppInfo._from_proto(resp.info, self._app_id)
1691
+ return self._info
1692
+
1658
1693
 
1659
1694
  App = synchronize_api(_App)
@@ -20,6 +20,7 @@ import modal.running_app
20
20
  import modal.schedule
21
21
  import modal.secret
22
22
  import modal.server
23
+ import modal.types
23
24
  import modal.volume
24
25
  import pathlib
25
26
  import synchronicity.combined_types
@@ -155,6 +156,7 @@ class _App:
155
156
  _running_app: typing.Optional[modal.running_app.RunningApp]
156
157
  _client: typing.Optional[modal.client._Client]
157
158
  _root_load_context: modal._load_context.LoadContext
159
+ _info: typing.Optional[modal.types.AppInfo]
158
160
 
159
161
  @property
160
162
  def _local_state(self) -> _LocalAppState:
@@ -940,6 +942,24 @@ class _App:
940
942
  """
941
943
  ...
942
944
 
945
+ async def info(self, refresh: bool = False) -> modal.types.AppInfo:
946
+ """Return information for a modal `App`.
947
+
948
+ The information returned includes the App's ID, member functions and servers,
949
+ as well as basic lifecycle information, e.g. who created the app and when.
950
+
951
+ Args:
952
+ refresh: Whether to fetch the latest info. By default, false, so
953
+ the info corresponds the App state at the time of the previous lookup.
954
+
955
+ See also:
956
+ - [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
957
+
958
+ Returns:
959
+ `AppInfo` object.
960
+ """
961
+ ...
962
+
943
963
  SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
944
964
 
945
965
  class App:
@@ -983,6 +1003,7 @@ class App:
983
1003
  _running_app: typing.Optional[modal.running_app.RunningApp]
984
1004
  _client: typing.Optional[modal.client.Client]
985
1005
  _root_load_context: modal._load_context.LoadContext
1006
+ _info: typing.Optional[modal.types.AppInfo]
986
1007
 
987
1008
  def __init__(
988
1009
  self,
@@ -2012,4 +2033,43 @@ class App:
2012
2033
  """
2013
2034
  ...
2014
2035
 
2036
+ class __info_spec(typing_extensions.Protocol):
2037
+ def __call__(self, /, refresh: bool = False) -> modal.types.AppInfo:
2038
+ """Return information for a modal `App`.
2039
+
2040
+ The information returned includes the App's ID, member functions and servers,
2041
+ as well as basic lifecycle information, e.g. who created the app and when.
2042
+
2043
+ Args:
2044
+ refresh: Whether to fetch the latest info. By default, false, so
2045
+ the info corresponds the App state at the time of the previous lookup.
2046
+
2047
+ See also:
2048
+ - [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
2049
+
2050
+ Returns:
2051
+ `AppInfo` object.
2052
+ """
2053
+ ...
2054
+
2055
+ async def aio(self, /, refresh: bool = False) -> modal.types.AppInfo:
2056
+ """Return information for a modal `App`.
2057
+
2058
+ The information returned includes the App's ID, member functions and servers,
2059
+ as well as basic lifecycle information, e.g. who created the app and when.
2060
+
2061
+ Args:
2062
+ refresh: Whether to fetch the latest info. By default, false, so
2063
+ the info corresponds the App state at the time of the previous lookup.
2064
+
2065
+ See also:
2066
+ - [`AppInfo`](https://modal.com/docs/sdk/py/latest/types#appinfo)
2067
+
2068
+ Returns:
2069
+ `AppInfo` object.
2070
+ """
2071
+ ...
2072
+
2073
+ info: __info_spec
2074
+
2015
2075
  _default_image: modal._image._Image
@@ -4,6 +4,7 @@ import sys
4
4
  import time
5
5
  import warnings
6
6
  from datetime import datetime, timedelta, timezone
7
+ from json import dumps
7
8
  from typing import Literal, get_args
8
9
 
9
10
  import click
@@ -709,3 +710,79 @@ async def dashboard(
709
710
  app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
710
711
  url = f"https://modal.com/id/{app_id}"
711
712
  open_url_and_display(url, "App dashboard")
713
+
714
+
715
+ @app_cli.command("info", no_args_is_help=True)
716
+ @click.argument("app_identifier")
717
+ @env_option
718
+ @click.option("--json", is_flag=True, default=False)
719
+ @synchronizer.create_blocking
720
+ async def info(app_identifier: str, *, env: str | None = None, json: bool = False):
721
+ """Show an App's lifecycle, Functions, and Servers.
722
+
723
+ Examples:
724
+
725
+ Get info based on an App ID:
726
+
727
+ ```
728
+ modal app info ap-123456
729
+ ```
730
+
731
+ Get info for a currently deployed App based on its name:
732
+
733
+ ```
734
+ modal app info my-app
735
+ ```
736
+
737
+ """
738
+ env = ensure_env(env)
739
+ client = await _Client.from_env()
740
+ app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
741
+ request = api_pb2.AppGetInfoRequest(app_id=app_id)
742
+ resp: api_pb2.AppGetInfoResponse = await client.stub.AppGetInfo(request)
743
+ app_info = resp.info
744
+ lifecycle = app_info.lifecycle
745
+ output = OutputManager.get()
746
+ state = APP_STATE_TO_MESSAGE.get(lifecycle.app_state, Text("unknown", style="gray"))
747
+
748
+ if json:
749
+ output.print_json(
750
+ dumps(
751
+ {
752
+ "description": app_info.description,
753
+ "app_id": app_info.app_id,
754
+ "lifecycle": {
755
+ "state": state.plain,
756
+ "created_at": timestamp_to_localized_str(lifecycle.created_at, json),
757
+ "created_by": lifecycle.created_by or None,
758
+ "deployed_at": timestamp_to_localized_str(lifecycle.deployed_at, json),
759
+ "deployed_by": lifecycle.deployed_by or None,
760
+ "version": lifecycle.version or None,
761
+ "stopped_at": timestamp_to_localized_str(lifecycle.stopped_at, json),
762
+ "stopped_by": lifecycle.stopped_by or None,
763
+ },
764
+ "functions": dict(sorted(app_info.functions.items())),
765
+ "servers": dict(sorted(app_info.servers.items())),
766
+ }
767
+ )
768
+ )
769
+ return
770
+
771
+ output.print(
772
+ Text.assemble("App: ", (app_info.app_id, "bold"), " - ", (app_info.description, "bold"), " (", state, ")")
773
+ )
774
+
775
+ if app_info.functions:
776
+ display_table(["Name", "Function ID"], sorted(app_info.functions.items()), title="Functions")
777
+ if app_info.servers:
778
+ display_table(["Name", "Function ID"], sorted(app_info.servers.items()), title="Servers")
779
+
780
+ events: list[list[Text | str | None]] = [
781
+ ["Created", timestamp_to_localized_str(lifecycle.created_at, json), lifecycle.created_by]
782
+ ]
783
+ if lifecycle.deployed_at:
784
+ label = f"Deployed (v{lifecycle.version})" if lifecycle.version else "Deployed"
785
+ events.append([label, timestamp_to_localized_str(lifecycle.deployed_at, json), lifecycle.deployed_by])
786
+ if lifecycle.stopped_at:
787
+ events.append(["Stopped", timestamp_to_localized_str(lifecycle.stopped_at, json), lifecycle.stopped_by])
788
+ display_table(["Event", "Time", "By"], events, title="Lifecycle")
@@ -77,7 +77,7 @@ class _Client:
77
77
  server_url: str,
78
78
  client_type: int,
79
79
  credentials: typing.Optional[tuple[str, str]],
80
- version: str = "1.5.6.dev5",
80
+ version: str = "1.5.6.dev7",
81
81
  *,
82
82
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
83
83
  ):
@@ -277,7 +277,7 @@ class Client:
277
277
  server_url: str,
278
278
  client_type: int,
279
279
  credentials: typing.Optional[tuple[str, str]],
280
- version: str = "1.5.6.dev5",
280
+ version: str = "1.5.6.dev7",
281
281
  *,
282
282
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
283
283
  ):
@@ -122,7 +122,9 @@ class _CloudBucketMount:
122
122
 
123
123
 
124
124
  def cloud_bucket_mounts_to_proto(
125
- mounts: Sequence[tuple[str, _CloudBucketMount]], split_ephemeral_credentials: bool = False
125
+ mounts: Sequence[tuple[str, _CloudBucketMount]],
126
+ split_ephemeral_credentials: bool = False,
127
+ include_secrets: bool = True,
126
128
  ) -> tuple[list[api_pb2.CloudBucketMount], dict[str, api_pb2.StringMap]]:
127
129
  """mdmd:hidden
128
130
  Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
@@ -158,11 +160,11 @@ def cloud_bucket_mounts_to_proto(
158
160
  key_prefix = mount.key_prefix
159
161
 
160
162
  credentials_secret_id = ""
161
- if secret := mount.secret:
163
+ if include_secrets and (secret := mount.secret):
162
164
  if split_ephemeral_credentials and secret._load_env_dict:
163
165
  env_dict = secret._load_env_dict()
164
- credientials = api_pb2.StringMap(contents=env_dict)
165
- cloud_bucket_credentials[path] = credientials
166
+ credentials = api_pb2.StringMap(contents=env_dict)
167
+ cloud_bucket_credentials[path] = credentials
166
168
  else:
167
169
  credentials_secret_id = secret.object_id
168
170
 
@@ -126,7 +126,9 @@ class _CloudBucketMount:
126
126
  ...
127
127
 
128
128
  def cloud_bucket_mounts_to_proto(
129
- mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]], split_ephemeral_credentials: bool = False
129
+ mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]],
130
+ split_ephemeral_credentials: bool = False,
131
+ include_secrets: bool = True,
130
132
  ) -> tuple[list[modal_proto.api_pb2.CloudBucketMount], dict[str, modal_proto.api_pb2.StringMap]]:
131
133
  """mdmd:hidden
132
134
  Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.