modal 1.5.5.dev1__tar.gz → 1.5.5.dev2__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.5.dev1 → modal-1.5.5.dev2}/PKG-INFO +1 -1
  2. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_container_entrypoint.py +4 -2
  3. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_logs_manager.py +67 -0
  4. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_logs_manager.pyi +167 -0
  5. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/container_io_manager.py +44 -23
  6. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/container_io_manager.pyi +10 -5
  7. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/client.pyi +2 -2
  8. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/functions.pyi +6 -6
  9. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/sandbox.py +53 -0
  10. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/sandbox.pyi +72 -0
  11. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/PKG-INFO +1 -1
  12. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_version/__init__.py +1 -1
  13. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/pyproject.toml +1 -1
  14. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/LICENSE +0 -0
  15. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/README.md +0 -0
  16. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/__init__.py +0 -0
  17. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/__main__.py +0 -0
  18. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_billing.py +0 -0
  19. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_clustered_functions.py +0 -0
  20. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_clustered_functions.pyi +0 -0
  21. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_environments.py +0 -0
  22. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_function_variants.py +0 -0
  23. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_functions.py +0 -0
  24. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_grpc_client.py +0 -0
  25. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_image.py +0 -0
  26. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_ipython.py +0 -0
  27. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_load_context.py +0 -0
  28. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_location.py +0 -0
  29. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_logs.py +0 -0
  30. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_object.py +0 -0
  31. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_output/__init__.py +0 -0
  32. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_output/manager.py +0 -0
  33. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_output/pty.py +0 -0
  34. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_output/rich.py +0 -0
  35. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_output/status.py +0 -0
  36. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_partial_function.py +0 -0
  37. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_resolver.py +0 -0
  38. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_resources.py +0 -0
  39. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/__init__.py +0 -0
  40. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/asgi.py +0 -0
  41. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/execution_context.py +0 -0
  42. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/execution_context.pyi +0 -0
  43. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  44. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/task_lifecycle_manager.py +0 -0
  45. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  46. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/telemetry.py +0 -0
  47. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/user_code_event_loop.py +0 -0
  48. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_runtime/user_code_imports.py +0 -0
  49. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_serialization.py +0 -0
  50. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_server.py +0 -0
  51. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_supports_logs.py +0 -0
  52. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_traceback.py +0 -0
  53. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_tunnel.py +0 -0
  54. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_tunnel.pyi +0 -0
  55. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_type_manager.py +0 -0
  56. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/__init__.py +0 -0
  57. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/app_utils.py +0 -0
  58. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/async_utils.py +0 -0
  59. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/auth_token_manager.py +0 -0
  60. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/blob_utils.py +0 -0
  61. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/browser_utils.py +0 -0
  62. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/bytes_io_segment_payload.py +0 -0
  63. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/curl_utils.py +0 -0
  64. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/deprecation.py +0 -0
  65. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/display_utils.py +0 -0
  66. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/docker_utils.py +0 -0
  67. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/function_utils.py +0 -0
  68. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/git_utils.py +0 -0
  69. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/grpc_testing.py +0 -0
  70. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/grpc_utils.py +0 -0
  71. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/hash_utils.py +0 -0
  72. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/http_utils.py +0 -0
  73. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/jwt_utils.py +0 -0
  74. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/logger.py +0 -0
  75. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/mount_utils.py +0 -0
  76. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/name_utils.py +0 -0
  77. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/package_utils.py +0 -0
  78. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/pattern_utils.py +0 -0
  79. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/proxy_support.py +0 -0
  80. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/rand_pb_testing.py +0 -0
  81. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/sandbox_fs_utils.py +0 -0
  82. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/shell_utils.py +0 -0
  83. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/task_command_router_client.py +0 -0
  84. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_utils/time_utils.py +0 -0
  85. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_vendor/__init__.py +0 -0
  86. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  87. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_vendor/cloudpickle.py +0 -0
  88. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_vendor/tblib.py +0 -0
  89. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_vendor/version.py +0 -0
  90. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_watcher.py +0 -0
  91. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/_workspace.py +0 -0
  92. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/app.py +0 -0
  93. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/app.pyi +0 -0
  94. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/billing.py +0 -0
  95. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/billing.pyi +0 -0
  96. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/2023.12.312.txt +0 -0
  97. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/2023.12.txt +0 -0
  98. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/2024.04.txt +0 -0
  99. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/2024.10.txt +0 -0
  100. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/2025.06.txt +0 -0
  101. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/PREVIEW.txt +0 -0
  102. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/README.md +0 -0
  103. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/builder/base-images.json +0 -0
  104. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/call_graph.py +0 -0
  105. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/__init__.py +0 -0
  106. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/_download.py +0 -0
  107. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/_help.py +0 -0
  108. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/_traceback.py +0 -0
  109. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/app.py +0 -0
  110. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/billing.py +0 -0
  111. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/bootstrap.py +0 -0
  112. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/changelog.py +0 -0
  113. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/cluster.py +0 -0
  114. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/config.py +0 -0
  115. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/container.py +0 -0
  116. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/curl.py +0 -0
  117. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/dashboard.py +0 -0
  118. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/dict.py +0 -0
  119. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/endpoint.py +0 -0
  120. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/entry_point.py +0 -0
  121. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/environment.py +0 -0
  122. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/image.py +0 -0
  123. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/import_refs.py +0 -0
  124. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/launch.py +0 -0
  125. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/logo.py +0 -0
  126. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/network_file_system.py +0 -0
  127. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/profile.py +0 -0
  128. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/programs/__init__.py +0 -0
  129. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/programs/run_jupyter.py +0 -0
  130. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/programs/vscode.py +0 -0
  131. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/queues.py +0 -0
  132. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/run.py +0 -0
  133. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/secret.py +0 -0
  134. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/selector.py +0 -0
  135. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/shell.py +0 -0
  136. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/skills.py +0 -0
  137. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/token.py +0 -0
  138. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/utils.py +0 -0
  139. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/volume.py +0 -0
  140. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cli/workspace.py +0 -0
  141. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/client.py +0 -0
  142. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cloud_bucket_mount.py +0 -0
  143. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cloud_bucket_mount.pyi +0 -0
  144. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cls.py +0 -0
  145. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/cls.pyi +0 -0
  146. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/config.py +0 -0
  147. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/container_process.py +0 -0
  148. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/container_process.pyi +0 -0
  149. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/dict.py +0 -0
  150. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/dict.pyi +0 -0
  151. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/environments.py +0 -0
  152. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/environments.pyi +0 -0
  153. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/exception.py +0 -0
  154. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/experimental/__init__.py +0 -0
  155. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/experimental/flash.py +0 -0
  156. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/experimental/flash.pyi +0 -0
  157. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/experimental/ipython.py +0 -0
  158. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/file_io.py +0 -0
  159. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/file_io.pyi +0 -0
  160. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/file_pattern_matcher.py +0 -0
  161. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/functions.py +0 -0
  162. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/image.py +0 -0
  163. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/image.pyi +0 -0
  164. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/io_streams.py +0 -0
  165. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/io_streams.pyi +0 -0
  166. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/mount.py +0 -0
  167. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/mount.pyi +0 -0
  168. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/network_file_system.py +0 -0
  169. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/network_file_system.pyi +0 -0
  170. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/object.py +0 -0
  171. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/object.pyi +0 -0
  172. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/output.py +0 -0
  173. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/parallel_map.py +0 -0
  174. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/parallel_map.pyi +0 -0
  175. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/partial_function.py +0 -0
  176. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/partial_function.pyi +0 -0
  177. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/proxy.py +0 -0
  178. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/proxy.pyi +0 -0
  179. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/py.typed +0 -0
  180. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/queue.py +0 -0
  181. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/queue.pyi +0 -0
  182. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/retries.py +0 -0
  183. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/runner.py +0 -0
  184. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/runner.pyi +0 -0
  185. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/running_app.py +0 -0
  186. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/sandbox_fs.py +0 -0
  187. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/sandbox_fs.pyi +0 -0
  188. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/schedule.py +0 -0
  189. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/scheduler_placement.py +0 -0
  190. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/secret.py +0 -0
  191. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/secret.pyi +0 -0
  192. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/server.py +0 -0
  193. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/server.pyi +0 -0
  194. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/serving.py +0 -0
  195. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/serving.pyi +0 -0
  196. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/skills/modal/SKILL.md +0 -0
  197. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/snapshot.py +0 -0
  198. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/snapshot.pyi +0 -0
  199. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/stream_type.py +0 -0
  200. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/token_flow.py +0 -0
  201. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/token_flow.pyi +0 -0
  202. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/types.py +0 -0
  203. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/volume.py +0 -0
  204. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/volume.pyi +0 -0
  205. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/workspace.py +0 -0
  206. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal/workspace.pyi +0 -0
  207. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/SOURCES.txt +0 -0
  208. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/dependency_links.txt +0 -0
  209. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/entry_points.txt +0 -0
  210. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/requires.txt +0 -0
  211. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal.egg-info/top_level.txt +0 -0
  212. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/__init__.py +0 -0
  213. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/gen_cli_docs.py +0 -0
  214. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/gen_cli_docs_main.py +0 -0
  215. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/gen_reference_docs.py +0 -0
  216. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/gen_reference_docs_main.py +0 -0
  217. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/mdmd/__init__.py +0 -0
  218. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/mdmd/mdmd.py +0 -0
  219. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/mdmd/signatures.py +0 -0
  220. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/mdmd/types.py +0 -0
  221. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_docs/reference_intro.md +0 -0
  222. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/__init__.py +0 -0
  223. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/api_grpc.py +0 -0
  224. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/api_pb2.py +0 -0
  225. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/api_pb2.pyi +0 -0
  226. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/api_pb2_grpc.py +0 -0
  227. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/api_pb2_grpc.pyi +0 -0
  228. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/modal_api_grpc.py +0 -0
  229. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/py.typed +0 -0
  230. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/task_command_router_grpc.py +0 -0
  231. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/task_command_router_pb2.py +0 -0
  232. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/task_command_router_pb2.pyi +0 -0
  233. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  234. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  235. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/modal_version/__main__.py +0 -0
  236. {modal-1.5.5.dev1 → modal-1.5.5.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.5.dev1
3
+ Version: 1.5.5.dev2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -145,7 +145,7 @@ def call_function(
145
145
  item_count += 1
146
146
 
147
147
  await container_io_manager._send_outputs.aio(
148
- io_context.output_items_generator_done(started_at, item_count)
148
+ io_context, io_context.output_items_generator_done(started_at, item_count)
149
149
  )
150
150
  else:
151
151
  value = await io_context.call_function_async()
@@ -182,7 +182,9 @@ def call_function(
182
182
  container_io_manager._queue_put(generator_queue, value)
183
183
  item_count += 1
184
184
 
185
- container_io_manager._send_outputs(io_context.output_items_generator_done(started_at, item_count))
185
+ container_io_manager._send_outputs(
186
+ io_context, io_context.output_items_generator_done(started_at, item_count)
187
+ )
186
188
  else:
187
189
  values = io_context.call_function_sync()
188
190
  container_io_manager.push_outputs(io_context, started_at, values)
@@ -93,6 +93,8 @@ def _entry_context_ids(object_id: str, item: api_pb2.TaskLogs, batch: api_pb2.Ta
93
93
  ]
94
94
  case "fc":
95
95
  context_ids = [item.input_id or batch.input_id, item.container_id or batch.task_id]
96
+ case "sb":
97
+ context_ids = [item.container_id or batch.task_id]
96
98
  case _:
97
99
  context_ids = []
98
100
  # filter for empty strings, can happen for Server which have function_ids but no input/container
@@ -802,6 +804,71 @@ class _ImageLogsManager:
802
804
  ImageLogsManager = synchronize_api(_ImageLogsManager, target_module=__name__)
803
805
 
804
806
 
807
+ class _SandboxLogsManager:
808
+ """mdmd:namespace"""
809
+
810
+ def __init__(self, source: _SupportsLogs):
811
+ """mdmd:hidden"""
812
+ self._manager = _LogsManager(source)
813
+
814
+ async def fetch(
815
+ self,
816
+ *,
817
+ since: datetime,
818
+ until: datetime | None = None,
819
+ source: LogSource | None = None,
820
+ search_text: str = "",
821
+ ) -> AsyncGenerator[LogEntry, None]:
822
+ """Fetch Sandbox logs corresponding to the date range and filters.
823
+
824
+ Args:
825
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
826
+ which is interpreted as local time.
827
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
828
+ as local time.
829
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
830
+ search_text: Filter by search text.
831
+
832
+ Yields:
833
+ `LogEntry` objects in chronological order.
834
+
835
+ Examples:
836
+
837
+ ```python notest
838
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
839
+
840
+ for entry in sandbox.logs.fetch(
841
+ since=datetime.now() - timedelta(minutes=25),
842
+ source="stdout",
843
+ ):
844
+ print(entry.message, end="")
845
+ ```
846
+ """
847
+ async for log_entry in self._manager.fetch(since=since, until=until, source=source, search_text=search_text):
848
+ yield log_entry
849
+
850
+ async def tail(
851
+ self,
852
+ entries: int = 100,
853
+ *,
854
+ source: LogSource | None = None,
855
+ ) -> AsyncGenerator[LogEntry, None]:
856
+ """Fetch the most recent Sandbox logs.
857
+
858
+ Args:
859
+ entries: The number of log entries to return.
860
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
861
+
862
+ Yields:
863
+ `LogEntry` objects in chronological order.
864
+ """
865
+ async for log_entry in self._manager.tail(entries, source=source):
866
+ yield log_entry
867
+
868
+
869
+ SandboxLogsManager = synchronize_api(_SandboxLogsManager, target_module=__name__)
870
+
871
+
805
872
  class _AppLogsManager:
806
873
  """mdmd:namespace"""
807
874
 
@@ -996,6 +996,173 @@ class ImageLogsManager:
996
996
 
997
997
  tail: __tail_spec
998
998
 
999
+ class _SandboxLogsManager:
1000
+ """mdmd:namespace"""
1001
+ def __init__(self, source: modal._supports_logs._SupportsLogs):
1002
+ """mdmd:hidden"""
1003
+ ...
1004
+
1005
+ def fetch(
1006
+ self,
1007
+ *,
1008
+ since: datetime.datetime,
1009
+ until: typing.Optional[datetime.datetime] = None,
1010
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1011
+ search_text: str = "",
1012
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1013
+ """Fetch Sandbox logs corresponding to the date range and filters.
1014
+
1015
+ Args:
1016
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1017
+ which is interpreted as local time.
1018
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1019
+ as local time.
1020
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1021
+ search_text: Filter by search text.
1022
+
1023
+ Yields:
1024
+ `LogEntry` objects in chronological order.
1025
+
1026
+ Examples:
1027
+
1028
+ ```python notest
1029
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1030
+
1031
+ for entry in sandbox.logs.fetch(
1032
+ since=datetime.now() - timedelta(minutes=25),
1033
+ source="stdout",
1034
+ ):
1035
+ print(entry.message, end="")
1036
+ ```
1037
+ """
1038
+ ...
1039
+
1040
+ def tail(
1041
+ self, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1042
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1043
+ """Fetch the most recent Sandbox logs.
1044
+
1045
+ Args:
1046
+ entries: The number of log entries to return.
1047
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1048
+
1049
+ Yields:
1050
+ `LogEntry` objects in chronological order.
1051
+ """
1052
+ ...
1053
+
1054
+ class SandboxLogsManager:
1055
+ """mdmd:namespace"""
1056
+ def __init__(self, source: modal._supports_logs._SupportsLogs):
1057
+ """mdmd:hidden"""
1058
+ ...
1059
+
1060
+ class __fetch_spec(typing_extensions.Protocol):
1061
+ def __call__(
1062
+ self,
1063
+ /,
1064
+ *,
1065
+ since: datetime.datetime,
1066
+ until: typing.Optional[datetime.datetime] = None,
1067
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1068
+ search_text: str = "",
1069
+ ) -> typing.Generator[modal.types.LogEntry, None, None]:
1070
+ """Fetch Sandbox logs corresponding to the date range and filters.
1071
+
1072
+ Args:
1073
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1074
+ which is interpreted as local time.
1075
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1076
+ as local time.
1077
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1078
+ search_text: Filter by search text.
1079
+
1080
+ Yields:
1081
+ `LogEntry` objects in chronological order.
1082
+
1083
+ Examples:
1084
+
1085
+ ```python notest
1086
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1087
+
1088
+ for entry in sandbox.logs.fetch(
1089
+ since=datetime.now() - timedelta(minutes=25),
1090
+ source="stdout",
1091
+ ):
1092
+ print(entry.message, end="")
1093
+ ```
1094
+ """
1095
+ ...
1096
+
1097
+ def aio(
1098
+ self,
1099
+ /,
1100
+ *,
1101
+ since: datetime.datetime,
1102
+ until: typing.Optional[datetime.datetime] = None,
1103
+ source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None,
1104
+ search_text: str = "",
1105
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1106
+ """Fetch Sandbox logs corresponding to the date range and filters.
1107
+
1108
+ Args:
1109
+ since: Start date to fetch logs from. Must be in UTC or timezone-naive,
1110
+ which is interpreted as local time.
1111
+ until: Defaults to current date if None. Must be in UTC or timezone-naive, which is interpreted
1112
+ as local time.
1113
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1114
+ search_text: Filter by search text.
1115
+
1116
+ Yields:
1117
+ `LogEntry` objects in chronological order.
1118
+
1119
+ Examples:
1120
+
1121
+ ```python notest
1122
+ sandbox = modal.Sandbox.from_name("my-app", "sandbox")
1123
+
1124
+ for entry in sandbox.logs.fetch(
1125
+ since=datetime.now() - timedelta(minutes=25),
1126
+ source="stdout",
1127
+ ):
1128
+ print(entry.message, end="")
1129
+ ```
1130
+ """
1131
+ ...
1132
+
1133
+ fetch: __fetch_spec
1134
+
1135
+ class __tail_spec(typing_extensions.Protocol):
1136
+ def __call__(
1137
+ self, /, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1138
+ ) -> typing.Generator[modal.types.LogEntry, None, None]:
1139
+ """Fetch the most recent Sandbox logs.
1140
+
1141
+ Args:
1142
+ entries: The number of log entries to return.
1143
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1144
+
1145
+ Yields:
1146
+ `LogEntry` objects in chronological order.
1147
+ """
1148
+ ...
1149
+
1150
+ def aio(
1151
+ self, /, entries: int = 100, *, source: typing.Optional[typing.Literal["stdout", "stderr", "system"]] = None
1152
+ ) -> collections.abc.AsyncGenerator[modal.types.LogEntry, None]:
1153
+ """Fetch the most recent Sandbox logs.
1154
+
1155
+ Args:
1156
+ entries: The number of log entries to return.
1157
+ source: Filter by source: 'stdout', 'stderr', or 'system'.
1158
+
1159
+ Yields:
1160
+ `LogEntry` objects in chronological order.
1161
+ """
1162
+ ...
1163
+
1164
+ tail: __tail_spec
1165
+
999
1166
  class _AppLogsManager:
1000
1167
  """mdmd:namespace"""
1001
1168
  def __init__(self, source: modal._supports_logs._SupportsLogs):
@@ -96,6 +96,7 @@ class IOContext:
96
96
 
97
97
  _cancel_issued: bool = False
98
98
  _cancel_callback: Callable[[], None] | None = None
99
+ _exited: bool = False
99
100
 
100
101
  def __init__(
101
102
  self,
@@ -889,25 +890,35 @@ class _ContainerIOManager:
889
890
  if item.kill_switch:
890
891
  logger.debug(f"Task {self.task_id} input kill signal input.")
891
892
  return
892
- inputs.append(
893
- (
894
- item.input_id,
895
- item.retry_count,
896
- item.function_call_id,
897
- item.attempt_token,
898
- item.input,
899
- item.function_call_invocation_type,
893
+ live_io_context = self.current_inputs.get(item.input_id)
894
+ if live_io_context is not None and (
895
+ live_io_context.retry_counts[live_io_context.input_ids.index(item.input_id)]
896
+ == item.retry_count
897
+ ):
898
+ # An expired fetch lease redelivers a still-running attempt under the same
899
+ # input id and retry count; a retry of a failed attempt gets a new retry count.
900
+ logger.warning(f"Skipping duplicate delivery of input {item.input_id}")
901
+ else:
902
+ inputs.append(
903
+ (
904
+ item.input_id,
905
+ item.retry_count,
906
+ item.function_call_id,
907
+ item.attempt_token,
908
+ item.input,
909
+ item.function_call_invocation_type,
910
+ )
900
911
  )
901
- )
902
912
  if item.input.final_input:
903
913
  if request.batch_max_size > 0:
904
914
  logger.debug(f"Task {self.task_id} Final input not expected in batch input stream")
905
915
  final_input_received = True
906
916
  break
907
917
 
908
- # If yielded, allow input slots to be released via exit_context
909
- yield inputs
910
- yielded = True
918
+ if inputs:
919
+ # If yielded, allow input slots to be released via exit_context
920
+ yield inputs
921
+ yielded = True
911
922
 
912
923
  # TODO(michael): Remove use of max_inputs after worker rollover
913
924
  single_use_container = self.function_def.single_use_containers or self.function_def.max_inputs == 1
@@ -950,7 +961,7 @@ class _ContainerIOManager:
950
961
  # collect all active input slots, meaning all inputs have wrapped up.
951
962
  await self._input_slots.close()
952
963
 
953
- async def _send_outputs(self, outputs: list[api_pb2.FunctionPutOutputsItem]) -> None:
964
+ async def _send_outputs(self, io_context: IOContext, outputs: list[api_pb2.FunctionPutOutputsItem]) -> None:
954
965
  """Send pre-built output items with retry and chunking."""
955
966
  # There are multiple outputs for a single IOContext in the case of @modal.batched.
956
967
  # Limit the batch size to 20 to stay within message size limits and buffer size limits.
@@ -963,8 +974,7 @@ class _ContainerIOManager:
963
974
  max_retries=None, # Retry indefinitely, trying every 1s.
964
975
  ),
965
976
  )
966
- input_ids = [output.input_id for output in outputs]
967
- self.exit_context(input_ids)
977
+ self.exit_context(io_context)
968
978
 
969
979
  @asynccontextmanager
970
980
  async def handle_input_exception(
@@ -984,7 +994,7 @@ class _ContainerIOManager:
984
994
  raise
985
995
  except (InputCancellation, asyncio.CancelledError):
986
996
  outputs = await io_context.output_items_cancellation(started_at)
987
- await self._send_outputs(outputs)
997
+ await self._send_outputs(io_context, outputs)
988
998
  logger.warning(f"Successfully canceled input {io_context.input_ids}")
989
999
  return
990
1000
  except BaseException as exc:
@@ -995,13 +1005,24 @@ class _ContainerIOManager:
995
1005
  # print exception so it's logged
996
1006
  print_exception(*sys.exc_info())
997
1007
  outputs = await io_context.output_items_exception(started_at, self.task_id, exc)
998
- await self._send_outputs(outputs)
1008
+ await self._send_outputs(io_context, outputs)
999
1009
 
1000
- def exit_context(self, input_ids: list[str]):
1001
- for input_id in input_ids:
1002
- self.current_inputs.pop(input_id)
1003
-
1004
- self._input_slots.release()
1010
+ def exit_context(self, io_context: IOContext):
1011
+ # A cancellation can land after outputs were already sent, re-entering here for the same
1012
+ # context; exit at most once so the input slot is not released twice.
1013
+ if io_context._exited:
1014
+ return
1015
+ io_context._exited = True
1016
+ try:
1017
+ for input_id in io_context.input_ids:
1018
+ # A retry admitted before its predecessor finished exiting takes over the
1019
+ # tracking entry, so remove it only if this context still owns it.
1020
+ if self.current_inputs.get(input_id) is io_context:
1021
+ self.current_inputs.pop(input_id)
1022
+ else:
1023
+ logger.warning(f"Input {input_id} missing from active input tracking")
1024
+ finally:
1025
+ self._input_slots.release()
1005
1026
 
1006
1027
  # skip inspection of user-generated output_data for synchronicity input translation
1007
1028
  @synchronizer.no_io_translation
@@ -1013,7 +1034,7 @@ class _ContainerIOManager:
1013
1034
  ) -> None:
1014
1035
  # The standard output encoding+sending method for successful function outputs
1015
1036
  outputs = await io_context.output_items(started_at, output_data)
1016
- await self._send_outputs(outputs)
1037
+ await self._send_outputs(io_context, outputs)
1017
1038
 
1018
1039
  @asynccontextmanager
1019
1040
  async def snapshot_context_manager(self) -> AsyncGenerator[None, None]:
@@ -33,6 +33,7 @@ class IOContext:
33
33
  finalized_function: modal._runtime.user_code_imports.FinalizedFunction
34
34
  _cancel_issued: bool
35
35
  _cancel_callback: typing.Optional[collections.abc.Callable[[], None]]
36
+ _exited: bool
36
37
 
37
38
  def __init__(
38
39
  self,
@@ -202,7 +203,9 @@ class _ContainerIOManager:
202
203
  batch_max_size: int = 0,
203
204
  batch_wait_ms: int = 0,
204
205
  ) -> collections.abc.AsyncIterator[IOContext]: ...
205
- async def _send_outputs(self, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
206
+ async def _send_outputs(
207
+ self, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]
208
+ ) -> None:
206
209
  """Send pre-built output items with retry and chunking."""
207
210
  ...
208
211
 
@@ -210,7 +213,7 @@ class _ContainerIOManager:
210
213
  """Handle an exception while processing a function input."""
211
214
  ...
212
215
 
213
- def exit_context(self, input_ids: list[str]): ...
216
+ def exit_context(self, io_context: IOContext): ...
214
217
  async def push_outputs(self, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
215
218
  def snapshot_context_manager(self) -> typing.AsyncContextManager[None]: ...
216
219
  async def interact(self, from_breakpoint: bool = False): ...
@@ -445,11 +448,13 @@ class ContainerIOManager:
445
448
  run_inputs_outputs: __run_inputs_outputs_spec
446
449
 
447
450
  class ___send_outputs_spec(typing_extensions.Protocol):
448
- def __call__(self, /, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
451
+ def __call__(self, /, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
449
452
  """Send pre-built output items with retry and chunking."""
450
453
  ...
451
454
 
452
- async def aio(self, /, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
455
+ async def aio(
456
+ self, /, io_context: IOContext, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]
457
+ ) -> None:
453
458
  """Send pre-built output items with retry and chunking."""
454
459
  ...
455
460
 
@@ -468,7 +473,7 @@ class ContainerIOManager:
468
473
 
469
474
  handle_input_exception: __handle_input_exception_spec
470
475
 
471
- def exit_context(self, input_ids: list[str]): ...
476
+ def exit_context(self, io_context: IOContext): ...
472
477
 
473
478
  class __push_outputs_spec(typing_extensions.Protocol):
474
479
  def __call__(self, /, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
@@ -38,7 +38,7 @@ class _Client:
38
38
  server_url: str,
39
39
  client_type: int,
40
40
  credentials: typing.Optional[tuple[str, str]],
41
- version: str = "1.5.5.dev1",
41
+ version: str = "1.5.5.dev2",
42
42
  ):
43
43
  """mdmd:hidden
44
44
  The Modal client object is not intended to be instantiated directly by users.
@@ -209,7 +209,7 @@ class Client:
209
209
  server_url: str,
210
210
  client_type: int,
211
211
  credentials: typing.Optional[tuple[str, str]],
212
- version: str = "1.5.5.dev1",
212
+ version: str = "1.5.5.dev2",
213
213
  ):
214
214
  """mdmd:hidden
215
215
  The Modal client object is not intended to be instantiated directly by users.
@@ -546,7 +546,7 @@ class Function(
546
546
 
547
547
  _call_generator: ___call_generator_spec
548
548
 
549
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
549
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
550
550
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
551
551
  """Calls the function remotely, executing it with the given arguments and returning the execution's result.
552
552
 
@@ -571,7 +571,7 @@ class Function(
571
571
  """
572
572
  ...
573
573
 
574
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
574
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
575
575
 
576
576
  class __remote_gen_spec(typing_extensions.Protocol):
577
577
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -621,7 +621,7 @@ class Function(
621
621
  """
622
622
  ...
623
623
 
624
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
624
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
625
625
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
626
626
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
627
627
 
@@ -654,7 +654,7 @@ class Function(
654
654
  """
655
655
  ...
656
656
 
657
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
657
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
658
658
 
659
659
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
660
660
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -662,7 +662,7 @@ class Function(
662
662
 
663
663
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
664
664
 
665
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
665
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
666
666
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
667
667
  """Calls the function with the given arguments, without waiting for the results.
668
668
 
@@ -695,7 +695,7 @@ class Function(
695
695
  """
696
696
  ...
697
697
 
698
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
698
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
699
699
 
700
700
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
701
701
  """Return the inner Python object wrapped by this Modal Function.
@@ -15,6 +15,8 @@ from dataclasses import dataclass
15
15
  from pathlib import PurePosixPath
16
16
  from typing import TYPE_CHECKING, Any, Literal, Union, overload
17
17
 
18
+ from modal._logs_manager import _SandboxLogsManager
19
+ from modal._supports_logs import LogsFilters, _LogQueryData
18
20
  from modal.secret import _split_env_dict_and_resolvable_secrets
19
21
 
20
22
  from ._output.pty import get_pty_info
@@ -1931,6 +1933,27 @@ class _Sandbox(_Object, type_prefix="sb"):
1931
1933
  await asyncio.sleep(0.5)
1932
1934
  return self._task_id
1933
1935
 
1936
+ async def _resolve_task_id_for_logs(self) -> str:
1937
+ """Try to resolve the task ID in one shot for historical log fetches.
1938
+
1939
+ For a historical log api, we should not wait for the task to be created.
1940
+ If the Sandbox created logs worth fetching, the task ID should be available.
1941
+ """
1942
+ if self._task_id:
1943
+ return self._task_id
1944
+
1945
+ req = api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
1946
+ stub = self.__client.stub
1947
+ if self._is_v2:
1948
+ assert self.__client._auth_token_manager
1949
+ auth_token = await self.__client._auth_token_manager.get_token()
1950
+ resp = await stub.SandboxGetTaskIdV2(req, metadata=[("x-modal-auth-token", auth_token)])
1951
+ else:
1952
+ resp = await stub.SandboxGetTaskId(req)
1953
+ if not resp.task_id:
1954
+ raise ExecutionError("Sandbox task_id is not available yet.")
1955
+ return resp.task_id
1956
+
1934
1957
  async def _get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
1935
1958
  if self._command_router_client is None:
1936
1959
  try:
@@ -2605,6 +2628,36 @@ class _Sandbox(_Object, type_prefix="sb"):
2605
2628
  # Fetch the next batch starting from the end of the current one.
2606
2629
  before_timestamp = resp.sandboxes[-1].created_at
2607
2630
 
2631
+ async def _get_log_query_data(self) -> _LogQueryData:
2632
+ if not self._app_id:
2633
+ if not self._attached:
2634
+ raise ExecutionError("Cannot fetch logs for a detached Sandbox without hydrated app_id")
2635
+ await self.hydrate()
2636
+
2637
+ if not self._app_id:
2638
+ raise ExecutionError("app_id should have been set during sandbox hydration")
2639
+
2640
+ task_id = await self._resolve_task_id_for_logs()
2641
+ return _LogQueryData(self.__client, self._app_id, LogsFilters(task_id=task_id), self.object_id)
2642
+
2643
+ @property
2644
+ def logs(self) -> _SandboxLogsManager:
2645
+ """Access logs for a `Sandbox` entrypoint.
2646
+
2647
+ Useful for inspecting logs after a Sandbox terminates.
2648
+ Use [`fetch()`](#logsfetch)
2649
+ to read logs from a UTC time range, [`tail()`](#logstail)
2650
+ to read the most recent logs.
2651
+
2652
+ Note that the logs from executed commands in the sandbox (via `exec()`) are not included in the
2653
+ entrypoint logs.
2654
+
2655
+ See also:
2656
+ - [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
2657
+ CLI access to logs for an App.
2658
+ """
2659
+ return _SandboxLogsManager(self)
2660
+
2608
2661
 
2609
2662
  class _SidecarContainer:
2610
2663
  """Handle to an additional container running in a Sandbox."""