modAL 1.2.7.dev6__tar.gz → 1.2.7.dev9__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 (194) hide show
  1. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/PKG-INFO +1 -1
  2. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_functions.py +3 -10
  3. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/container_io_manager.py +3 -2
  4. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/blob_utils.py +5 -4
  5. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/app.py +34 -8
  6. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/app.pyi +8 -4
  7. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/client.pyi +2 -2
  8. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/file_io.py +22 -10
  9. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/functions.pyi +1 -1
  10. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/io_streams.py +10 -7
  11. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/PKG-INFO +1 -1
  12. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api.proto +5 -0
  13. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api_pb2.py +656 -656
  14. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api_pb2.pyi +8 -1
  15. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_version/__init__.py +1 -1
  16. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/LICENSE +0 -0
  17. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/README.md +0 -0
  18. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/__init__.py +0 -0
  19. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/__main__.py +0 -0
  20. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_billing.py +0 -0
  21. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_clustered_functions.py +0 -0
  22. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_clustered_functions.pyi +0 -0
  23. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_container_entrypoint.py +0 -0
  24. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_grpc_client.py +0 -0
  25. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_ipython.py +0 -0
  26. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_load_context.py +0 -0
  27. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_location.py +0 -0
  28. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_object.py +0 -0
  29. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_output.py +0 -0
  30. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_partial_function.py +0 -0
  31. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_pty.py +0 -0
  32. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_resolver.py +0 -0
  33. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_resources.py +0 -0
  34. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/__init__.py +0 -0
  35. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/asgi.py +0 -0
  36. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/container_io_manager.pyi +0 -0
  37. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/execution_context.py +0 -0
  38. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/execution_context.pyi +0 -0
  39. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  40. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/telemetry.py +0 -0
  41. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/user_code_event_loop.py +0 -0
  42. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_runtime/user_code_imports.py +0 -0
  43. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_serialization.py +0 -0
  44. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_traceback.py +0 -0
  45. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_tunnel.py +0 -0
  46. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_tunnel.pyi +0 -0
  47. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_type_manager.py +0 -0
  48. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/__init__.py +0 -0
  49. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/app_utils.py +0 -0
  50. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/async_utils.py +0 -0
  51. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/auth_token_manager.py +0 -0
  52. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/bytes_io_segment_payload.py +0 -0
  53. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/deprecation.py +0 -0
  54. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/docker_utils.py +0 -0
  55. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/function_utils.py +0 -0
  56. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/git_utils.py +0 -0
  57. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/grpc_testing.py +0 -0
  58. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/grpc_utils.py +0 -0
  59. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/hash_utils.py +0 -0
  60. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/http_utils.py +0 -0
  61. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/jwt_utils.py +0 -0
  62. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/logger.py +0 -0
  63. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/mount_utils.py +0 -0
  64. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/name_utils.py +0 -0
  65. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/package_utils.py +0 -0
  66. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/pattern_utils.py +0 -0
  67. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/rand_pb_testing.py +0 -0
  68. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/shell_utils.py +0 -0
  69. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/task_command_router_client.py +0 -0
  70. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_utils/time_utils.py +0 -0
  71. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_vendor/__init__.py +0 -0
  72. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  73. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_vendor/cloudpickle.py +0 -0
  74. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_vendor/tblib.py +0 -0
  75. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/_watcher.py +0 -0
  76. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/billing.py +0 -0
  77. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/2023.12.312.txt +0 -0
  78. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/2023.12.txt +0 -0
  79. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/2024.04.txt +0 -0
  80. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/2024.10.txt +0 -0
  81. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/2025.06.txt +0 -0
  82. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/PREVIEW.txt +0 -0
  83. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/README.md +0 -0
  84. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/builder/base-images.json +0 -0
  85. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/call_graph.py +0 -0
  86. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/__init__.py +0 -0
  87. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/_download.py +0 -0
  88. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/_traceback.py +0 -0
  89. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/app.py +0 -0
  90. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/cluster.py +0 -0
  91. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/config.py +0 -0
  92. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/container.py +0 -0
  93. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/dict.py +0 -0
  94. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/entry_point.py +0 -0
  95. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/environment.py +0 -0
  96. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/import_refs.py +0 -0
  97. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/launch.py +0 -0
  98. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/network_file_system.py +0 -0
  99. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/profile.py +0 -0
  100. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/programs/__init__.py +0 -0
  101. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/programs/run_jupyter.py +0 -0
  102. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/programs/vscode.py +0 -0
  103. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/queues.py +0 -0
  104. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/run.py +0 -0
  105. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/secret.py +0 -0
  106. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/shell.py +0 -0
  107. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/token.py +0 -0
  108. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/utils.py +0 -0
  109. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cli/volume.py +0 -0
  110. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/client.py +0 -0
  111. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cloud_bucket_mount.py +0 -0
  112. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cloud_bucket_mount.pyi +0 -0
  113. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cls.py +0 -0
  114. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/cls.pyi +0 -0
  115. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/config.py +0 -0
  116. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/container_process.py +0 -0
  117. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/container_process.pyi +0 -0
  118. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/dict.py +0 -0
  119. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/dict.pyi +0 -0
  120. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/environments.py +0 -0
  121. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/environments.pyi +0 -0
  122. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/exception.py +0 -0
  123. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/experimental/__init__.py +0 -0
  124. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/experimental/flash.py +0 -0
  125. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/experimental/flash.pyi +0 -0
  126. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/experimental/ipython.py +0 -0
  127. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/file_io.pyi +0 -0
  128. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/file_pattern_matcher.py +0 -0
  129. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/functions.py +0 -0
  130. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/gpu.py +0 -0
  131. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/image.py +0 -0
  132. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/image.pyi +0 -0
  133. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/io_streams.pyi +0 -0
  134. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/mount.py +0 -0
  135. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/mount.pyi +0 -0
  136. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/network_file_system.py +0 -0
  137. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/network_file_system.pyi +0 -0
  138. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/object.py +0 -0
  139. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/object.pyi +0 -0
  140. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/output.py +0 -0
  141. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/parallel_map.py +0 -0
  142. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/parallel_map.pyi +0 -0
  143. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/partial_function.py +0 -0
  144. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/partial_function.pyi +0 -0
  145. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/proxy.py +0 -0
  146. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/proxy.pyi +0 -0
  147. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/py.typed +0 -0
  148. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/queue.py +0 -0
  149. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/queue.pyi +0 -0
  150. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/retries.py +0 -0
  151. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/runner.py +0 -0
  152. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/runner.pyi +0 -0
  153. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/running_app.py +0 -0
  154. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/sandbox.py +0 -0
  155. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/sandbox.pyi +0 -0
  156. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/schedule.py +0 -0
  157. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/scheduler_placement.py +0 -0
  158. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/secret.py +0 -0
  159. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/secret.pyi +0 -0
  160. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/serving.py +0 -0
  161. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/serving.pyi +0 -0
  162. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/snapshot.py +0 -0
  163. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/snapshot.pyi +0 -0
  164. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/stream_type.py +0 -0
  165. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/token_flow.py +0 -0
  166. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/token_flow.pyi +0 -0
  167. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/volume.py +0 -0
  168. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal/volume.pyi +0 -0
  169. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/SOURCES.txt +0 -0
  170. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/dependency_links.txt +0 -0
  171. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/entry_points.txt +0 -0
  172. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/requires.txt +0 -0
  173. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal.egg-info/top_level.txt +0 -0
  174. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/__init__.py +0 -0
  175. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/gen_cli_docs.py +0 -0
  176. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/gen_reference_docs.py +0 -0
  177. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/mdmd/__init__.py +0 -0
  178. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/mdmd/mdmd.py +0 -0
  179. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_docs/mdmd/signatures.py +0 -0
  180. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/__init__.py +0 -0
  181. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api_grpc.py +0 -0
  182. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api_pb2_grpc.py +0 -0
  183. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/api_pb2_grpc.pyi +0 -0
  184. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/modal_api_grpc.py +0 -0
  185. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/py.typed +0 -0
  186. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router.proto +0 -0
  187. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router_grpc.py +0 -0
  188. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router_pb2.py +0 -0
  189. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router_pb2.pyi +0 -0
  190. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  191. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  192. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/modal_version/__main__.py +0 -0
  193. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/pyproject.toml +0 -0
  194. {modal-1.2.7.dev6 → modal-1.2.7.dev9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.7.dev6
3
+ Version: 1.2.7.dev9
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -694,7 +694,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
694
694
  # Experimental: Clustered functions
695
695
  cluster_size: Optional[int] = None,
696
696
  rdma: Optional[bool] = None,
697
- max_inputs: Optional[int] = None,
697
+ single_use_containers: bool = False,
698
698
  ephemeral_disk: Optional[int] = None,
699
699
  include_source: bool = True,
700
700
  experimental_options: Optional[dict[str, str]] = None,
@@ -810,14 +810,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
810
810
  if arg.default is not inspect.Parameter.empty:
811
811
  raise InvalidError(f"Modal batched function {func_name} does not accept default arguments.")
812
812
 
813
- if max_inputs is not None:
814
- if not isinstance(max_inputs, int):
815
- raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
816
- if max_inputs <= 0:
817
- raise InvalidError("`max_inputs` must be positive")
818
- if max_inputs > 1:
819
- raise InvalidError("Only `max_inputs=1` is currently supported")
820
-
821
813
  # Validate volumes
822
814
  validated_volumes = validate_volumes(volumes)
823
815
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
@@ -1024,7 +1016,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1024
1016
  object_dependencies=object_dependencies,
1025
1017
  block_network=block_network,
1026
1018
  untrusted=restrict_modal_access,
1027
- max_inputs=max_inputs or 0,
1019
+ single_use_containers=single_use_containers,
1020
+ max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
1028
1021
  cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1029
1022
  scheduler_placement=scheduler_placement,
1030
1023
  is_class=info.is_service_class(),
@@ -845,8 +845,9 @@ class _ContainerIOManager:
845
845
  yield inputs
846
846
  yielded = True
847
847
 
848
- # We only support max_inputs = 1 at the moment
849
- if final_input_received or self.function_def.max_inputs == 1:
848
+ # TODO(michael): Remove use of max_inputs after worker rollover
849
+ single_use_container = self.function_def.single_use_containers or self.function_def.max_inputs == 1
850
+ if final_input_received or single_use_container:
850
851
  return
851
852
  finally:
852
853
  if not yielded:
@@ -152,12 +152,13 @@ async def perform_multipart_upload(
152
152
  part_etags = await TaskContext.gather(*upload_coros)
153
153
 
154
154
  # The body of the complete_multipart_upload command needs some data in xml format:
155
- completion_body = "<CompleteMultipartUpload>\n"
155
+ completion_parts = ["<CompleteMultipartUpload>"]
156
156
  for part_number, etag in enumerate(part_etags, 1):
157
- completion_body += f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>\n"""
158
- completion_body += "</CompleteMultipartUpload>"
157
+ completion_parts.append(f"""<Part>\n<PartNumber>{part_number}</PartNumber>\n<ETag>"{etag}"</ETag>\n</Part>""")
158
+ completion_parts.append("</CompleteMultipartUpload>")
159
+ completion_body = "\n".join(completion_parts)
159
160
 
160
- # etag of combined object should be md5 hex of concatendated md5 *bytes* from parts + `-{num_parts}`
161
+ # etag of combined object should be md5 hex of concatenated md5 *bytes* from parts + `-{num_parts}`
161
162
  bin_hash_parts = [bytes.fromhex(etag) for etag in part_etags]
162
163
 
163
164
  expected_multipart_etag = hashlib.md5(b"".join(bin_hash_parts)).hexdigest() + f"-{len(part_etags)}"
@@ -725,9 +725,7 @@ class _App:
725
725
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
726
726
  block_network: bool = False, # Whether to block network access
727
727
  restrict_modal_access: bool = False, # Whether to allow this function access to other Modal resources
728
- # Maximum number of inputs a container should handle before shutting down.
729
- # With `max_inputs = 1`, containers will be single-use.
730
- max_inputs: Optional[int] = None,
728
+ single_use_containers: bool = False, # When True, containers will shut down after handling a single input
731
729
  i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
732
730
  # Whether the file or directory containing the Function's source should automatically be included
733
731
  # in the container. When unset, falls back to the App-level configuration, or is otherwise True by default.
@@ -742,6 +740,7 @@ class _App:
742
740
  concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
743
741
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
744
742
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
743
+ max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
745
744
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
746
745
  _experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
747
746
  # using `region` and `nonpreemptible`
@@ -764,6 +763,20 @@ class _App:
764
763
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
765
764
  )
766
765
 
766
+ if max_inputs is not None:
767
+ if not isinstance(max_inputs, int):
768
+ raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
769
+ if max_inputs <= 0:
770
+ raise InvalidError("`max_inputs` must be positive")
771
+ if max_inputs > 1:
772
+ raise InvalidError("Only `max_inputs=1` is currently supported")
773
+ deprecation_warning(
774
+ (2025, 12, 16),
775
+ "The `max_inputs` parameter is deprecated. Please set `single_use_containers=True` instead.",
776
+ pending=True,
777
+ )
778
+ single_use_containers = max_inputs == 1
779
+
767
780
  if _experimental_scheduler_placement is not None:
768
781
  deprecation_warning(
769
782
  (2025, 11, 17),
@@ -912,7 +925,7 @@ class _App:
912
925
  enable_memory_snapshot=enable_memory_snapshot,
913
926
  block_network=block_network,
914
927
  restrict_modal_access=restrict_modal_access,
915
- max_inputs=max_inputs,
928
+ single_use_containers=single_use_containers,
916
929
  i6pn_enabled=i6pn_enabled,
917
930
  cluster_size=cluster_size, # Experimental: Clustered functions
918
931
  rdma=rdma,
@@ -969,9 +982,7 @@ class _App:
969
982
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
970
983
  block_network: bool = False, # Whether to block network access
971
984
  restrict_modal_access: bool = False, # Whether to allow this class access to other Modal resources
972
- # Limits the number of inputs a container handles before shutting down.
973
- # Use `max_inputs = 1` for single-use containers.
974
- max_inputs: Optional[int] = None,
985
+ single_use_containers: bool = False, # When True, containers will shut down after handling a single input
975
986
  i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
976
987
  include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
977
988
  experimental_options: Optional[dict[str, Any]] = None,
@@ -984,6 +995,7 @@ class _App:
984
995
  concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
985
996
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
986
997
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
998
+ max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
987
999
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
988
1000
  _experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
989
1001
  # using `region` and `nonpreemptible`
@@ -1002,6 +1014,20 @@ class _App:
1002
1014
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
1003
1015
  )
1004
1016
 
1017
+ if max_inputs is not None:
1018
+ if not isinstance(max_inputs, int):
1019
+ raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
1020
+ if max_inputs <= 0:
1021
+ raise InvalidError("`max_inputs` must be positive")
1022
+ if max_inputs > 1:
1023
+ raise InvalidError("Only `max_inputs=1` is currently supported")
1024
+ deprecation_warning(
1025
+ (2025, 12, 16),
1026
+ "The `max_inputs` parameter is deprecated. Please set `single_use_containers=True` instead.",
1027
+ pending=True,
1028
+ )
1029
+ single_use_containers = max_inputs == 1
1030
+
1005
1031
  if _experimental_scheduler_placement is not None:
1006
1032
  deprecation_warning(
1007
1033
  (2025, 11, 17),
@@ -1139,7 +1165,7 @@ class _App:
1139
1165
  enable_memory_snapshot=enable_memory_snapshot,
1140
1166
  block_network=block_network,
1141
1167
  restrict_modal_access=restrict_modal_access,
1142
- max_inputs=max_inputs,
1168
+ single_use_containers=single_use_containers,
1143
1169
  http_config=http_config,
1144
1170
  i6pn_enabled=i6pn_enabled,
1145
1171
  cluster_size=cluster_size,
@@ -491,7 +491,7 @@ class _App:
491
491
  enable_memory_snapshot: bool = False,
492
492
  block_network: bool = False,
493
493
  restrict_modal_access: bool = False,
494
- max_inputs: typing.Optional[int] = None,
494
+ single_use_containers: bool = False,
495
495
  i6pn: typing.Optional[bool] = None,
496
496
  include_source: typing.Optional[bool] = None,
497
497
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
@@ -502,6 +502,7 @@ class _App:
502
502
  concurrency_limit: typing.Optional[int] = None,
503
503
  container_idle_timeout: typing.Optional[int] = None,
504
504
  allow_concurrent_inputs: typing.Optional[int] = None,
505
+ max_inputs: typing.Optional[int] = None,
505
506
  _experimental_buffer_containers: typing.Optional[int] = None,
506
507
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
507
508
  ) -> _FunctionDecoratorType:
@@ -545,7 +546,7 @@ class _App:
545
546
  enable_memory_snapshot: bool = False,
546
547
  block_network: bool = False,
547
548
  restrict_modal_access: bool = False,
548
- max_inputs: typing.Optional[int] = None,
549
+ single_use_containers: bool = False,
549
550
  i6pn: typing.Optional[bool] = None,
550
551
  include_source: typing.Optional[bool] = None,
551
552
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
@@ -556,6 +557,7 @@ class _App:
556
557
  concurrency_limit: typing.Optional[int] = None,
557
558
  container_idle_timeout: typing.Optional[int] = None,
558
559
  allow_concurrent_inputs: typing.Optional[int] = None,
560
+ max_inputs: typing.Optional[int] = None,
559
561
  _experimental_buffer_containers: typing.Optional[int] = None,
560
562
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
561
563
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
@@ -1154,7 +1156,7 @@ class App:
1154
1156
  enable_memory_snapshot: bool = False,
1155
1157
  block_network: bool = False,
1156
1158
  restrict_modal_access: bool = False,
1157
- max_inputs: typing.Optional[int] = None,
1159
+ single_use_containers: bool = False,
1158
1160
  i6pn: typing.Optional[bool] = None,
1159
1161
  include_source: typing.Optional[bool] = None,
1160
1162
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
@@ -1165,6 +1167,7 @@ class App:
1165
1167
  concurrency_limit: typing.Optional[int] = None,
1166
1168
  container_idle_timeout: typing.Optional[int] = None,
1167
1169
  allow_concurrent_inputs: typing.Optional[int] = None,
1170
+ max_inputs: typing.Optional[int] = None,
1168
1171
  _experimental_buffer_containers: typing.Optional[int] = None,
1169
1172
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1170
1173
  ) -> _FunctionDecoratorType:
@@ -1208,7 +1211,7 @@ class App:
1208
1211
  enable_memory_snapshot: bool = False,
1209
1212
  block_network: bool = False,
1210
1213
  restrict_modal_access: bool = False,
1211
- max_inputs: typing.Optional[int] = None,
1214
+ single_use_containers: bool = False,
1212
1215
  i6pn: typing.Optional[bool] = None,
1213
1216
  include_source: typing.Optional[bool] = None,
1214
1217
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
@@ -1219,6 +1222,7 @@ class App:
1219
1222
  concurrency_limit: typing.Optional[int] = None,
1220
1223
  container_idle_timeout: typing.Optional[int] = None,
1221
1224
  allow_concurrent_inputs: typing.Optional[int] = None,
1225
+ max_inputs: typing.Optional[int] = None,
1222
1226
  _experimental_buffer_containers: typing.Optional[int] = None,
1223
1227
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
1224
1228
  ) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.7.dev6",
35
+ version: str = "1.2.7.dev9",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.7.dev6",
166
+ version: str = "1.2.7.dev9",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -190,7 +190,7 @@ class _FileIO(Generic[T]):
190
190
 
191
191
  async def _wait(self, exec_id: str) -> bytes:
192
192
  # The logic here is similar to how output is read from `exec`
193
- output = b""
193
+ output_buffer = io.BytesIO()
194
194
  completed = False
195
195
  retries_remaining = 10
196
196
  while not completed:
@@ -201,7 +201,7 @@ class _FileIO(Generic[T]):
201
201
  break
202
202
  if isinstance(data, Exception):
203
203
  raise data
204
- output += data
204
+ output_buffer.write(data)
205
205
  except (GRPCError, StreamTerminatedError) as exc:
206
206
  if retries_remaining > 0:
207
207
  retries_remaining -= 1
@@ -212,7 +212,7 @@ class _FileIO(Generic[T]):
212
212
  elif isinstance(exc, StreamTerminatedError):
213
213
  continue
214
214
  raise
215
- return output
215
+ return output_buffer.getvalue()
216
216
 
217
217
  def _validate_type(self, data: Union[bytes, str]) -> None:
218
218
  if self._binary and isinstance(data, str):
@@ -421,10 +421,22 @@ class _FileIO(Generic[T]):
421
421
  task_id=self._task_id,
422
422
  ),
423
423
  )
424
+
425
+ def end_of_event(item: bytes, buffer: io.BytesIO, boundary_token: bytes) -> bool:
426
+ if not item.endswith(b"\n"):
427
+ return False
428
+ boundary_token_size = len(boundary_token)
429
+ if buffer.tell() < boundary_token_size:
430
+ return False
431
+ buffer.seek(-boundary_token_size, io.SEEK_END)
432
+ if buffer.read(boundary_token_size) == boundary_token:
433
+ return True
434
+ return False
435
+
424
436
  async with TaskContext() as tc:
425
437
  tc.create_task(self._consume_watch_output(resp.exec_id))
426
438
 
427
- buffer = b""
439
+ item_buffer = io.BytesIO()
428
440
  while True:
429
441
  if len(self._watch_output_buffer) > 0:
430
442
  item = self._watch_output_buffer.pop(0)
@@ -432,12 +444,12 @@ class _FileIO(Generic[T]):
432
444
  break
433
445
  if isinstance(item, Exception):
434
446
  raise item
435
- buffer += item
436
- # a single event may be split across multiple messages
437
- # the end of an event is marked by two newlines
438
- if buffer.endswith(b"\n\n"):
447
+ item_buffer.write(item)
448
+ assert isinstance(item, bytes)
449
+ # Single events may span multiple messages so we need to check for a special event boundary token
450
+ if end_of_event(item, item_buffer, boundary_token=b"\n\n"):
439
451
  try:
440
- event_json = json.loads(buffer.strip().decode())
452
+ event_json = json.loads(item_buffer.getvalue().strip().decode())
441
453
  event = FileWatchEvent(
442
454
  type=FileWatchEventType(event_json["event_type"]),
443
455
  paths=event_json["paths"],
@@ -447,7 +459,7 @@ class _FileIO(Generic[T]):
447
459
  except (json.JSONDecodeError, KeyError, ValueError):
448
460
  # skip invalid events
449
461
  pass
450
- buffer = b""
462
+ item_buffer = io.BytesIO()
451
463
  else:
452
464
  await asyncio.sleep(0.1)
453
465
 
@@ -106,7 +106,7 @@ class Function(
106
106
  i6pn_enabled: bool = False,
107
107
  cluster_size: typing.Optional[int] = None,
108
108
  rdma: typing.Optional[bool] = None,
109
- max_inputs: typing.Optional[int] = None,
109
+ single_use_containers: bool = False,
110
110
  ephemeral_disk: typing.Optional[int] = None,
111
111
  include_source: bool = True,
112
112
  experimental_options: typing.Optional[dict[str, str]] = None,
@@ -23,7 +23,7 @@ from grpclib.exceptions import GRPCError, StreamTerminatedError
23
23
  from modal.exception import ClientClosed, ExecTimeoutError, InvalidError
24
24
  from modal_proto import api_pb2
25
25
 
26
- from ._utils.async_utils import aclosing, synchronize_api
26
+ from ._utils.async_utils import aclosing, synchronize_api, synchronizer
27
27
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
28
28
  from ._utils.task_command_router_client import TaskCommandRouterClient
29
29
  from .client import _Client
@@ -153,17 +153,17 @@ class _StreamReaderThroughServer(Generic[T]):
153
153
  """Fetch the entire contents of the stream until EOF."""
154
154
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read starting")
155
155
  if self._text:
156
- data_str = ""
156
+ buffer = io.StringIO()
157
157
  async for message in _decode_bytes_stream_to_str(self._get_logs()):
158
- data_str += message
158
+ buffer.write(message)
159
159
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
160
- return cast(T, data_str)
160
+ return cast(T, buffer.getvalue())
161
161
  else:
162
- data_bytes = b""
162
+ buffer = io.BytesIO()
163
163
  async for message in self._get_logs():
164
- data_bytes += message
164
+ buffer.write(message)
165
165
  logger.debug(f"{self._object_id} StreamReader fd={self._file_descriptor} read completed after EOF")
166
- return cast(T, data_bytes)
166
+ return cast(T, buffer.getvalue())
167
167
 
168
168
  async def _consume_container_process_stream(self):
169
169
  """Consume the container process stream and store messages in the buffer."""
@@ -566,6 +566,9 @@ class _StreamReader(Generic[T]):
566
566
  task_id: Optional[str] = None,
567
567
  ) -> None:
568
568
  """mdmd:hidden"""
569
+ # we can remove this once we ensure no constructors use async code
570
+ assert asyncio.get_running_loop() == synchronizer._get_loop(start=False)
571
+
569
572
  if by_line and not text:
570
573
  raise ValueError("line-buffering is only supported when text=True")
571
574
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.7.dev6
3
+ Version: 1.2.7.dev9
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1469,6 +1469,8 @@ message Function {
1469
1469
 
1470
1470
  bool block_network = 44;
1471
1471
 
1472
+ // In the SDK, we've deprecated `max_inputs` (which only every implemented `max_inputs=1`)
1473
+ // in favor of a boolean `single_use_containers` parameter.
1472
1474
  uint32 max_inputs = 46;
1473
1475
 
1474
1476
  repeated S3Mount s3_mounts = 47;
@@ -1556,6 +1558,9 @@ message Function {
1556
1558
  // Attribute on the module with the implementation, which may differ from function_name
1557
1559
  // when the user provided a custom name= for the Function inside the Application namespace
1558
1560
  string implementation_name = 90;
1561
+
1562
+ bool single_use_containers = 91; // When True, containers will shut down after handling a single input
1563
+
1559
1564
  }
1560
1565
 
1561
1566
  message FunctionAsyncInvokeRequest {