modAL 1.2.3.dev9__tar.gz → 1.2.3.dev11__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 (200) hide show
  1. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/PKG-INFO +1 -1
  2. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/grpc_utils.py +57 -4
  3. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/client.pyi +2 -2
  4. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/io_streams.py +101 -32
  5. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/io_streams.pyi +30 -5
  6. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/sandbox.py +2 -6
  7. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/PKG-INFO +1 -1
  8. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api.proto +13 -0
  9. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api_grpc.py +1 -0
  10. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api_pb2.py +1032 -1021
  11. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api_pb2.pyi +29 -0
  12. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_version/__init__.py +1 -1
  13. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/LICENSE +0 -0
  14. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/README.md +0 -0
  15. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/__init__.py +0 -0
  16. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/__main__.py +0 -0
  17. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_billing.py +0 -0
  18. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_clustered_functions.py +0 -0
  19. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_clustered_functions.pyi +0 -0
  20. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_container_entrypoint.py +0 -0
  21. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_functions.py +0 -0
  22. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_grpc_client.py +0 -0
  23. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_ipython.py +0 -0
  24. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_load_context.py +0 -0
  25. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_location.py +0 -0
  26. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_object.py +0 -0
  27. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_output.py +0 -0
  28. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_partial_function.py +0 -0
  29. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_pty.py +0 -0
  30. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_resolver.py +0 -0
  31. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_resources.py +0 -0
  32. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/__init__.py +0 -0
  33. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/asgi.py +0 -0
  34. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/container_io_manager.py +0 -0
  35. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/container_io_manager.pyi +0 -0
  36. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/execution_context.py +0 -0
  37. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/execution_context.pyi +0 -0
  38. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  39. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/telemetry.py +0 -0
  40. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_runtime/user_code_imports.py +0 -0
  41. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_serialization.py +0 -0
  42. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_traceback.py +0 -0
  43. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_tunnel.py +0 -0
  44. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_tunnel.pyi +0 -0
  45. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_type_manager.py +0 -0
  46. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/__init__.py +0 -0
  47. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/app_utils.py +0 -0
  48. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/async_utils.py +0 -0
  49. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/auth_token_manager.py +0 -0
  50. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/blob_utils.py +0 -0
  51. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/bytes_io_segment_payload.py +0 -0
  52. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/deprecation.py +0 -0
  53. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/docker_utils.py +0 -0
  54. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/function_utils.py +0 -0
  55. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/git_utils.py +0 -0
  56. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/grpc_testing.py +0 -0
  57. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/hash_utils.py +0 -0
  58. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/http_utils.py +0 -0
  59. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/jwt_utils.py +0 -0
  60. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/logger.py +0 -0
  61. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/mount_utils.py +0 -0
  62. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/name_utils.py +0 -0
  63. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/package_utils.py +0 -0
  64. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/pattern_utils.py +0 -0
  65. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/rand_pb_testing.py +0 -0
  66. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/shell_utils.py +0 -0
  67. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/task_command_router_client.py +0 -0
  68. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_utils/time_utils.py +0 -0
  69. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_vendor/__init__.py +0 -0
  70. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  71. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_vendor/cloudpickle.py +0 -0
  72. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_vendor/tblib.py +0 -0
  73. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/_watcher.py +0 -0
  74. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/app.py +0 -0
  75. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/app.pyi +0 -0
  76. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/billing.py +0 -0
  77. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/2023.12.312.txt +0 -0
  78. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/2023.12.txt +0 -0
  79. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/2024.04.txt +0 -0
  80. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/2024.10.txt +0 -0
  81. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/2025.06.txt +0 -0
  82. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/PREVIEW.txt +0 -0
  83. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/README.md +0 -0
  84. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/builder/base-images.json +0 -0
  85. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/call_graph.py +0 -0
  86. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/__init__.py +0 -0
  87. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/_download.py +0 -0
  88. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/_traceback.py +0 -0
  89. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/app.py +0 -0
  90. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/cluster.py +0 -0
  91. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/config.py +0 -0
  92. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/container.py +0 -0
  93. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/dict.py +0 -0
  94. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/entry_point.py +0 -0
  95. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/environment.py +0 -0
  96. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/import_refs.py +0 -0
  97. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/launch.py +0 -0
  98. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/network_file_system.py +0 -0
  99. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/profile.py +0 -0
  100. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/programs/__init__.py +0 -0
  101. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/programs/launch_instance_ssh.py +0 -0
  102. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/programs/run_jupyter.py +0 -0
  103. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/programs/run_marimo.py +0 -0
  104. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/programs/vscode.py +0 -0
  105. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/queues.py +0 -0
  106. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/run.py +0 -0
  107. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/secret.py +0 -0
  108. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/token.py +0 -0
  109. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/utils.py +0 -0
  110. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cli/volume.py +0 -0
  111. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/client.py +0 -0
  112. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cloud_bucket_mount.py +0 -0
  113. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cloud_bucket_mount.pyi +0 -0
  114. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cls.py +0 -0
  115. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/cls.pyi +0 -0
  116. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/config.py +0 -0
  117. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/container_process.py +0 -0
  118. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/container_process.pyi +0 -0
  119. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/dict.py +0 -0
  120. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/dict.pyi +0 -0
  121. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/environments.py +0 -0
  122. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/environments.pyi +0 -0
  123. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/exception.py +0 -0
  124. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/experimental/__init__.py +0 -0
  125. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/experimental/flash.py +0 -0
  126. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/experimental/flash.pyi +0 -0
  127. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/experimental/ipython.py +0 -0
  128. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/file_io.py +0 -0
  129. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/file_io.pyi +0 -0
  130. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/file_pattern_matcher.py +0 -0
  131. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/functions.py +0 -0
  132. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/functions.pyi +0 -0
  133. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/gpu.py +0 -0
  134. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/image.py +0 -0
  135. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/image.pyi +0 -0
  136. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/mount.py +0 -0
  137. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/mount.pyi +0 -0
  138. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/network_file_system.py +0 -0
  139. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/network_file_system.pyi +0 -0
  140. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/object.py +0 -0
  141. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/object.pyi +0 -0
  142. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/output.py +0 -0
  143. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/parallel_map.py +0 -0
  144. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/parallel_map.pyi +0 -0
  145. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/partial_function.py +0 -0
  146. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/partial_function.pyi +0 -0
  147. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/proxy.py +0 -0
  148. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/proxy.pyi +0 -0
  149. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/py.typed +0 -0
  150. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/queue.py +0 -0
  151. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/queue.pyi +0 -0
  152. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/retries.py +0 -0
  153. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/runner.py +0 -0
  154. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/runner.pyi +0 -0
  155. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/running_app.py +0 -0
  156. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/sandbox.pyi +0 -0
  157. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/schedule.py +0 -0
  158. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/scheduler_placement.py +0 -0
  159. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/secret.py +0 -0
  160. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/secret.pyi +0 -0
  161. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/serving.py +0 -0
  162. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/serving.pyi +0 -0
  163. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/snapshot.py +0 -0
  164. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/snapshot.pyi +0 -0
  165. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/stream_type.py +0 -0
  166. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/token_flow.py +0 -0
  167. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/token_flow.pyi +0 -0
  168. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/volume.py +0 -0
  169. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal/volume.pyi +0 -0
  170. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/SOURCES.txt +0 -0
  171. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/dependency_links.txt +0 -0
  172. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/entry_points.txt +0 -0
  173. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/requires.txt +0 -0
  174. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal.egg-info/top_level.txt +0 -0
  175. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/__init__.py +0 -0
  176. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/gen_cli_docs.py +0 -0
  177. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/gen_reference_docs.py +0 -0
  178. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/mdmd/__init__.py +0 -0
  179. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/mdmd/mdmd.py +0 -0
  180. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_docs/mdmd/signatures.py +0 -0
  181. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/__init__.py +0 -0
  182. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api_pb2_grpc.py +0 -0
  183. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/api_pb2_grpc.pyi +0 -0
  184. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/modal_api_grpc.py +0 -0
  185. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/py.typed +0 -0
  186. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router.proto +0 -0
  187. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router_grpc.py +0 -0
  188. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router_pb2.py +0 -0
  189. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router_pb2.pyi +0 -0
  190. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  191. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  192. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router.proto +0 -0
  193. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router_grpc.py +0 -0
  194. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router_pb2.py +0 -0
  195. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router_pb2.pyi +0 -0
  196. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  197. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  198. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/modal_version/__main__.py +0 -0
  199. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/pyproject.toml +0 -0
  200. {modal-1.2.3.dev9 → modal-1.2.3.dev11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3.dev9
3
+ Version: 1.2.3.dev11
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -9,7 +9,8 @@ import urllib.parse
9
9
  import uuid
10
10
  from collections.abc import AsyncIterator
11
11
  from dataclasses import dataclass, field
12
- from typing import Any, Optional, TypeVar
12
+ from functools import cache
13
+ from typing import Any, Optional, Sequence, TypeVar
13
14
 
14
15
  import grpclib.client
15
16
  import grpclib.config
@@ -17,11 +18,14 @@ import grpclib.events
17
18
  import grpclib.protocol
18
19
  import grpclib.stream
19
20
  from google.protobuf.message import Message
21
+ from google.protobuf.symbol_database import SymbolDatabase
20
22
  from grpclib import GRPCError, Status
23
+ from grpclib.encoding.base import StatusDetailsCodecBase
21
24
  from grpclib.exceptions import StreamTerminatedError
22
25
  from grpclib.protocol import H2Protocol
23
26
 
24
27
  from modal.exception import AuthError, ConnectionError
28
+ from modal_proto import api_pb2
25
29
  from modal_version import __version__
26
30
 
27
31
  from .._traceback import suppress_tb_frames
@@ -107,6 +111,56 @@ class ConnectionManager:
107
111
  self._channels.clear()
108
112
 
109
113
 
114
+ @cache
115
+ def _sym_db() -> SymbolDatabase:
116
+ from google.protobuf.symbol_database import Default
117
+
118
+ return Default()
119
+
120
+
121
+ class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
122
+ """grpclib compatible details codec.
123
+
124
+ The server can encode the details using `google.rpc.Status` using grpclib's default codec and this custom codec
125
+ can decode it into a `api_pb2.RPCStatus`.
126
+ """
127
+
128
+ def encode(
129
+ self,
130
+ status: Status,
131
+ message: Optional[str],
132
+ details: Optional[Sequence[Message]],
133
+ ) -> bytes:
134
+ details_proto = api_pb2.RPCStatus(code=status.value, message=message or "")
135
+ if details is not None:
136
+ for detail in details:
137
+ detail_container = details_proto.details.add()
138
+ detail_container.Pack(detail)
139
+ return details_proto.SerializeToString()
140
+
141
+ def decode(
142
+ self,
143
+ status: Status,
144
+ message: Optional[str],
145
+ data: bytes,
146
+ ) -> Any:
147
+ sym_db = _sym_db()
148
+ details_proto = api_pb2.RPCStatus.FromString(data)
149
+
150
+ details = []
151
+ for detail_container in details_proto.details:
152
+ # If we do not know how to decode an emssage, we'll ignore it.
153
+ with contextlib.suppress(Exception):
154
+ msg_type = sym_db.GetSymbol(detail_container.TypeName())
155
+ detail = msg_type()
156
+ detail_container.Unpack(detail)
157
+ details.append(detail)
158
+ return details
159
+
160
+
161
+ custom_detail_codec = CustomProtoStatusDetailsCodec()
162
+
163
+
110
164
  def create_channel(
111
165
  server_url: str,
112
166
  metadata: dict[str, str] = {},
@@ -125,7 +179,7 @@ def create_channel(
125
179
  )
126
180
 
127
181
  if o.scheme == "unix":
128
- channel = grpclib.client.Channel(path=o.path, config=config) # probably pointless to use a pool ever
182
+ channel = grpclib.client.Channel(path=o.path, config=config, status_details_codec=custom_detail_codec)
129
183
  elif o.scheme in ("http", "https"):
130
184
  target = o.netloc
131
185
  parts = target.split(":")
@@ -133,7 +187,7 @@ def create_channel(
133
187
  ssl = o.scheme.endswith("s")
134
188
  host = parts[0]
135
189
  port = int(parts[1]) if len(parts) == 2 else 443 if ssl else 80
136
- channel = grpclib.client.Channel(host, port, ssl=ssl, config=config)
190
+ channel = grpclib.client.Channel(host, port, ssl=ssl, config=config, status_details_codec=custom_detail_codec)
137
191
  else:
138
192
  raise Exception(f"Unknown scheme: {o.scheme}")
139
193
 
@@ -258,7 +312,6 @@ async def _retry_transient_errors(
258
312
  raise AuthError(exc.message)
259
313
  else:
260
314
  raise exc
261
-
262
315
  if retry.max_retries is not None and n_retries >= retry.max_retries:
263
316
  final_attempt = True
264
317
  elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.2.3.dev9",
36
+ version: str = "1.2.3.dev11",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.2.3.dev9",
167
+ version: str = "1.2.3.dev11",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,6 +1,8 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
3
  import codecs
4
+ import contextlib
5
+ import sys
4
6
  import time
5
7
  from collections.abc import AsyncGenerator, AsyncIterator
6
8
  from dataclasses import dataclass
@@ -377,7 +379,7 @@ async def _stdio_stream_from_command_router(
377
379
  return
378
380
 
379
381
 
380
- class _BytesStreamReaderThroughCommandRouter(Generic[T]):
382
+ class _BytesStreamReaderThroughCommandRouter:
381
383
  """
382
384
  StreamReader implementation that will read directly from the worker that
383
385
  hosts the sandbox.
@@ -396,27 +398,27 @@ class _BytesStreamReaderThroughCommandRouter(Generic[T]):
396
398
  def file_descriptor(self) -> int:
397
399
  return self._params.file_descriptor
398
400
 
399
- async def read(self) -> T:
401
+ async def read(self) -> bytes:
400
402
  data_bytes = b""
401
403
  async for part in self:
402
404
  data_bytes += cast(bytes, part)
403
- return cast(T, data_bytes)
405
+ return data_bytes
404
406
 
405
- def __aiter__(self) -> AsyncIterator[T]:
407
+ def __aiter__(self) -> AsyncIterator[bytes]:
406
408
  return self
407
409
 
408
- async def __anext__(self) -> T:
410
+ async def __anext__(self) -> bytes:
409
411
  if self._stream is None:
410
412
  self._stream = _stdio_stream_from_command_router(self._params)
411
413
  # This raises StopAsyncIteration if the stream is at EOF.
412
- return cast(T, await self._stream.__anext__())
414
+ return await self._stream.__anext__()
413
415
 
414
416
  async def aclose(self):
415
417
  if self._stream:
416
418
  await self._stream.aclose()
417
419
 
418
420
 
419
- class _TextStreamReaderThroughCommandRouter(Generic[T]):
421
+ class _TextStreamReaderThroughCommandRouter:
420
422
  """
421
423
  StreamReader implementation that will read directly from the worker
422
424
  that hosts the sandbox.
@@ -437,16 +439,16 @@ class _TextStreamReaderThroughCommandRouter(Generic[T]):
437
439
  def file_descriptor(self) -> int:
438
440
  return self._params.file_descriptor
439
441
 
440
- async def read(self) -> T:
442
+ async def read(self) -> str:
441
443
  data_str = ""
442
444
  async for part in self:
443
445
  data_str += cast(str, part)
444
- return cast(T, data_str)
446
+ return data_str
445
447
 
446
- def __aiter__(self) -> AsyncIterator[T]:
448
+ def __aiter__(self) -> AsyncIterator[str]:
447
449
  return self
448
450
 
449
- async def __anext__(self) -> T:
451
+ async def __anext__(self) -> str:
450
452
  if self._stream is None:
451
453
  bytes_stream = _stdio_stream_from_command_router(self._params)
452
454
  if self._by_line:
@@ -454,13 +456,88 @@ class _TextStreamReaderThroughCommandRouter(Generic[T]):
454
456
  else:
455
457
  self._stream = _decode_bytes_stream_to_str(bytes_stream)
456
458
  # This raises StopAsyncIteration if the stream is at EOF.
457
- return cast(T, await self._stream.__anext__())
459
+ return await self._stream.__anext__()
458
460
 
459
461
  async def aclose(self):
460
462
  if self._stream:
461
463
  await self._stream.aclose()
462
464
 
463
465
 
466
+ class _StdoutPrintingStreamReaderThroughCommandRouter(Generic[T]):
467
+ """
468
+ StreamReader implementation for StreamType.STDOUT when using the task command router.
469
+
470
+ This mirrors the behavior from the server-backed implementation: the stream is printed to
471
+ the local stdout immediately and is not readable via StreamReader methods.
472
+ """
473
+
474
+ _reader: Union[_TextStreamReaderThroughCommandRouter, _BytesStreamReaderThroughCommandRouter]
475
+
476
+ def __init__(
477
+ self,
478
+ reader: Union[_TextStreamReaderThroughCommandRouter, _BytesStreamReaderThroughCommandRouter],
479
+ ) -> None:
480
+ self._reader = reader
481
+ self._task: Optional[asyncio.Task[None]] = None
482
+ self._closed = False
483
+ # Kick off a background task that reads from the underlying text stream and prints to stdout.
484
+ self._start_printing_task()
485
+
486
+ @property
487
+ def file_descriptor(self) -> int:
488
+ return self._reader.file_descriptor
489
+
490
+ def _start_printing_task(self) -> None:
491
+ async def _run():
492
+ try:
493
+
494
+ def print_text_part(part: Union[str, bytes]) -> None:
495
+ assert isinstance(part, str)
496
+ print(cast(str, part), end="")
497
+
498
+ def print_bytes_part(part: Union[str, bytes]) -> None:
499
+ assert isinstance(part, bytes)
500
+ sys.stdout.buffer.write(cast(bytes, part))
501
+ sys.stdout.buffer.flush()
502
+
503
+ if isinstance(self._reader, _BytesStreamReaderThroughCommandRouter):
504
+ print_part = print_bytes_part
505
+ elif isinstance(self._reader, _TextStreamReaderThroughCommandRouter):
506
+ print_part = print_text_part
507
+ else:
508
+ raise ValueError("Unsupported reader type")
509
+
510
+ async for part in self._reader:
511
+ print_part(part)
512
+ except Exception as e:
513
+ logger.exception(f"Error printing stream: {e}")
514
+ finally:
515
+ closed, self._closed = self._closed, True
516
+ if not closed:
517
+ await self._reader.aclose()
518
+
519
+ self._task = asyncio.create_task(_run())
520
+
521
+ async def read(self) -> T:
522
+ raise InvalidError("Output can only be retrieved using the PIPE stream type.")
523
+
524
+ def __aiter__(self) -> AsyncIterator[T]:
525
+ raise InvalidError("Output can only be retrieved using the PIPE stream type.")
526
+
527
+ async def __anext__(self) -> T:
528
+ raise InvalidError("Output can only be retrieved using the PIPE stream type.")
529
+
530
+ async def aclose(self):
531
+ if self._task is not None:
532
+ self._task.cancel()
533
+ with contextlib.suppress(asyncio.CancelledError):
534
+ await self._task
535
+ self._task = None
536
+ closed, self._closed = self._closed, True
537
+ if not closed:
538
+ await self._reader.aclose()
539
+
540
+
464
541
  class _DevnullStreamReader(Generic[T]):
465
542
  """StreamReader implementation for a stream configured with
466
543
  StreamType.DEVNULL. Throws an error if read or any other method is
@@ -499,6 +576,7 @@ class _StreamReader(Generic[T]):
499
576
  _DevnullStreamReader,
500
577
  _TextStreamReaderThroughCommandRouter,
501
578
  _BytesStreamReaderThroughCommandRouter,
579
+ _StdoutPrintingStreamReaderThroughCommandRouter,
502
580
  ]
503
581
 
504
582
  def __init__(
@@ -523,35 +601,26 @@ class _StreamReader(Generic[T]):
523
601
  file_descriptor, object_id, object_type, client, stream_type, text, by_line, deadline
524
602
  )
525
603
  else:
526
- # The only reason task_id is optional is because StreamReader is
527
- # also used for sandbox logs, which don't have a task ID available
528
- # when the StreamReader is created.
604
+ # The only reason task_id is optional is because StreamReader is also used for sandbox
605
+ # logs, which don't have a task ID available when the StreamReader is created.
529
606
  assert task_id is not None
530
607
  assert object_type == "container_process"
531
608
  if stream_type == StreamType.DEVNULL:
532
609
  self._impl = _DevnullStreamReader(file_descriptor)
533
610
  else:
534
611
  assert stream_type == StreamType.PIPE or stream_type == StreamType.STDOUT
535
- # TODO(saltzm): The original implementation of STDOUT StreamType in
536
- # _StreamReaderThroughServer prints to stdout immediately. This doesn't match
537
- # python subprocess.run, which uses None to print to stdout immediately, and uses
538
- # STDOUT as an argument to stderr to redirect stderr to the stdout stream. We should
539
- # implement the old behavior here before moving out of beta, but after that
540
- # we should consider changing the API to match python subprocess.run. I don't expect
541
- # many customers are using this in any case, so I think it's fine to leave this
542
- # unimplemented for now.
543
- if stream_type == StreamType.STDOUT:
544
- raise NotImplementedError(
545
- "Currently the STDOUT stream type is not supported when using exec "
546
- "through a task command router, which is currently in beta."
547
- )
548
612
  params = _StreamReaderThroughCommandRouterParams(
549
613
  file_descriptor, task_id, object_id, command_router_client, deadline
550
614
  )
551
615
  if text:
552
- self._impl = _TextStreamReaderThroughCommandRouter(params, by_line)
616
+ reader = _TextStreamReaderThroughCommandRouter(params, by_line)
617
+ else:
618
+ reader = _BytesStreamReaderThroughCommandRouter(params)
619
+
620
+ if stream_type == StreamType.STDOUT:
621
+ self._impl = _StdoutPrintingStreamReaderThroughCommandRouter(reader)
553
622
  else:
554
- self._impl = _BytesStreamReaderThroughCommandRouter(params)
623
+ self._impl = reader
555
624
 
556
625
  @property
557
626
  def file_descriptor(self) -> int:
@@ -560,7 +629,7 @@ class _StreamReader(Generic[T]):
560
629
 
561
630
  async def read(self) -> T:
562
631
  """Fetch the entire contents of the stream until EOF."""
563
- return await self._impl.read()
632
+ return cast(T, await self._impl.read())
564
633
 
565
634
  # TODO(saltzm): I'd prefer to have the implementation classes only implement __aiter__
566
635
  # and have them return generator functions directly, but synchronicity doesn't let us
@@ -572,7 +641,7 @@ class _StreamReader(Generic[T]):
572
641
 
573
642
  async def __anext__(self) -> T:
574
643
  """mdmd:hidden"""
575
- return await self._impl.__anext__()
644
+ return cast(T, await self._impl.__anext__())
576
645
 
577
646
  async def aclose(self):
578
647
  """mdmd:hidden"""
@@ -127,7 +127,7 @@ def _stdio_stream_from_command_router(
127
127
  """Stream raw bytes from the router client."""
128
128
  ...
129
129
 
130
- class _BytesStreamReaderThroughCommandRouter(typing.Generic[T]):
130
+ class _BytesStreamReaderThroughCommandRouter:
131
131
  """StreamReader implementation that will read directly from the worker that
132
132
  hosts the sandbox.
133
133
 
@@ -139,12 +139,12 @@ class _BytesStreamReaderThroughCommandRouter(typing.Generic[T]):
139
139
 
140
140
  @property
141
141
  def file_descriptor(self) -> int: ...
142
- async def read(self) -> T: ...
143
- def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
144
- async def __anext__(self) -> T: ...
142
+ async def read(self) -> bytes: ...
143
+ def __aiter__(self) -> collections.abc.AsyncIterator[bytes]: ...
144
+ async def __anext__(self) -> bytes: ...
145
145
  async def aclose(self): ...
146
146
 
147
- class _TextStreamReaderThroughCommandRouter(typing.Generic[T]):
147
+ class _TextStreamReaderThroughCommandRouter:
148
148
  """StreamReader implementation that will read directly from the worker
149
149
  that hosts the sandbox.
150
150
 
@@ -156,6 +156,29 @@ class _TextStreamReaderThroughCommandRouter(typing.Generic[T]):
156
156
 
157
157
  @property
158
158
  def file_descriptor(self) -> int: ...
159
+ async def read(self) -> str: ...
160
+ def __aiter__(self) -> collections.abc.AsyncIterator[str]: ...
161
+ async def __anext__(self) -> str: ...
162
+ async def aclose(self): ...
163
+
164
+ class _StdoutPrintingStreamReaderThroughCommandRouter(typing.Generic[T]):
165
+ """StreamReader implementation for StreamType.STDOUT when using the task command router.
166
+
167
+ This mirrors the behavior from the server-backed implementation: the stream is printed to
168
+ the local stdout immediately and is not readable via StreamReader methods.
169
+ """
170
+
171
+ _reader: typing.Union[_TextStreamReaderThroughCommandRouter, _BytesStreamReaderThroughCommandRouter]
172
+
173
+ def __init__(
174
+ self, reader: typing.Union[_TextStreamReaderThroughCommandRouter, _BytesStreamReaderThroughCommandRouter]
175
+ ) -> None:
176
+ """Initialize self. See help(type(self)) for accurate signature."""
177
+ ...
178
+
179
+ @property
180
+ def file_descriptor(self) -> int: ...
181
+ def _start_printing_task(self) -> None: ...
159
182
  async def read(self) -> T: ...
160
183
  def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
161
184
  async def __anext__(self) -> T: ...
@@ -189,6 +212,7 @@ class _StreamReader(typing.Generic[T]):
189
212
  _DevnullStreamReader,
190
213
  _TextStreamReaderThroughCommandRouter,
191
214
  _BytesStreamReaderThroughCommandRouter,
215
+ _StdoutPrintingStreamReaderThroughCommandRouter,
192
216
  ]
193
217
 
194
218
  def __init__(
@@ -357,6 +381,7 @@ class StreamReader(typing.Generic[T]):
357
381
  _DevnullStreamReader,
358
382
  _TextStreamReaderThroughCommandRouter,
359
383
  _BytesStreamReaderThroughCommandRouter,
384
+ _StdoutPrintingStreamReaderThroughCommandRouter,
360
385
  ]
361
386
 
362
387
  def __init__(
@@ -928,12 +928,8 @@ class _Sandbox(_Object, type_prefix="sb"):
928
928
  elif stdout == StreamType.DEVNULL:
929
929
  stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_DEVNULL
930
930
  elif stdout == StreamType.STDOUT:
931
- # TODO(saltzm): This is a behavior change from the old implementation. We should
932
- # probably implement the old behavior of printing to stdout before moving out of beta.
933
- raise NotImplementedError(
934
- "Currently the STDOUT stream type is not supported when using exec "
935
- "through a task command router, which is currently in beta."
936
- )
931
+ # Stream stdout to the client so that it can be printed locally in the reader.
932
+ stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_PIPE
937
933
  else:
938
934
  raise ValueError("Unsupported StreamType for stdout")
939
935
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3.dev9
3
+ Version: 1.2.3.dev11
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -4,6 +4,7 @@ option go_package = "github.com/modal-labs/modal/go/proto";
4
4
 
5
5
  package modal.client;
6
6
 
7
+ import "google/protobuf/any.proto";
7
8
  import "google/protobuf/empty.proto";
8
9
  import "google/protobuf/struct.proto";
9
10
  import "google/protobuf/timestamp.proto";
@@ -2045,6 +2046,7 @@ message GPUConfig {
2045
2046
  string gpu_type = 4;
2046
2047
  }
2047
2048
 
2049
+
2048
2050
  message GeneratorDone { // Sent as the output when a generator finishes running.
2049
2051
  uint64 items_total = 1;
2050
2052
  }
@@ -2086,6 +2088,7 @@ message GenericResult { // Used for both tasks and function outputs
2086
2088
  string propagation_reason = 13; // (?)
2087
2089
  }
2088
2090
 
2091
+
2089
2092
  message Image {
2090
2093
  repeated BaseImage base_images = 5;
2091
2094
  repeated string dockerfile_commands = 6;
@@ -2631,6 +2634,16 @@ message QueuePutRequest {
2631
2634
  int32 partition_ttl_seconds = 6;
2632
2635
  }
2633
2636
 
2637
+ // A copy google.rpc.Status for GRPCError.details:
2638
+ // https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
2639
+ // RPCStatus is compatible with google.rpc.Status, so one can encode messages using
2640
+ // google.rpc.Status. The `details` field can be a list of any message, but for client
2641
+ // to decode it, the messages should be defined here (`modal_proto`).
2642
+ message RPCStatus {
2643
+ int32 code = 1;
2644
+ string message = 2;
2645
+ repeated google.protobuf.Any details = 3;
2646
+ }
2634
2647
 
2635
2648
  message RateLimit {
2636
2649
  int32 limit = 1;
@@ -9,6 +9,7 @@ import grpclib.client
9
9
  if typing.TYPE_CHECKING:
10
10
  import grpclib.server
11
11
 
12
+ import google.protobuf.any_pb2
12
13
  import google.protobuf.empty_pb2
13
14
  import google.protobuf.struct_pb2
14
15
  import google.protobuf.timestamp_pb2