modal 1.1.1.dev16__tar.gz → 1.1.1.dev18__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (188) hide show
  1. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/PKG-INFO +1 -1
  2. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_functions.py +34 -16
  3. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/auth_token_manager.py +1 -1
  4. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/function_utils.py +1 -0
  5. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/client.py +8 -1
  6. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/client.pyi +9 -2
  7. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/parallel_map.py +289 -0
  8. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/parallel_map.pyi +21 -0
  9. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/PKG-INFO +1 -1
  10. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api.proto +8 -0
  11. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api_pb2.py +180 -172
  12. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api_pb2.pyi +23 -1
  13. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_version/__init__.py +1 -1
  14. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/LICENSE +0 -0
  15. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/README.md +0 -0
  16. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/__init__.py +0 -0
  17. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/__main__.py +0 -0
  18. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_clustered_functions.py +0 -0
  19. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_clustered_functions.pyi +0 -0
  20. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_container_entrypoint.py +0 -0
  21. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_ipython.py +0 -0
  22. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_location.py +0 -0
  23. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_object.py +0 -0
  24. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_output.py +0 -0
  25. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_partial_function.py +0 -0
  26. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_pty.py +0 -0
  27. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_resolver.py +0 -0
  28. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_resources.py +0 -0
  29. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/__init__.py +0 -0
  30. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/asgi.py +0 -0
  31. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/container_io_manager.py +0 -0
  32. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/container_io_manager.pyi +0 -0
  33. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/execution_context.py +0 -0
  34. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/execution_context.pyi +0 -0
  35. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  36. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/telemetry.py +0 -0
  37. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_runtime/user_code_imports.py +0 -0
  38. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_serialization.py +0 -0
  39. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_traceback.py +0 -0
  40. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_tunnel.py +0 -0
  41. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_tunnel.pyi +0 -0
  42. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_type_manager.py +0 -0
  43. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/__init__.py +0 -0
  44. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/app_utils.py +0 -0
  45. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/async_utils.py +0 -0
  46. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/blob_utils.py +0 -0
  47. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/bytes_io_segment_payload.py +0 -0
  48. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/deprecation.py +0 -0
  49. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/docker_utils.py +0 -0
  50. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/git_utils.py +0 -0
  51. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/grpc_testing.py +0 -0
  52. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/grpc_utils.py +0 -0
  53. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/hash_utils.py +0 -0
  54. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/http_utils.py +0 -0
  55. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/jwt_utils.py +0 -0
  56. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/logger.py +0 -0
  57. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/mount_utils.py +0 -0
  58. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/name_utils.py +0 -0
  59. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/package_utils.py +0 -0
  60. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/pattern_utils.py +0 -0
  61. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/rand_pb_testing.py +0 -0
  62. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/shell_utils.py +0 -0
  63. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_utils/time_utils.py +0 -0
  64. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_vendor/__init__.py +0 -0
  65. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  66. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_vendor/cloudpickle.py +0 -0
  67. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_vendor/tblib.py +0 -0
  68. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/_watcher.py +0 -0
  69. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/app.py +0 -0
  70. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/app.pyi +0 -0
  71. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/call_graph.py +0 -0
  72. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/__init__.py +0 -0
  73. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/_download.py +0 -0
  74. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/_traceback.py +0 -0
  75. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/app.py +0 -0
  76. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/cluster.py +0 -0
  77. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/config.py +0 -0
  78. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/container.py +0 -0
  79. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/dict.py +0 -0
  80. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/entry_point.py +0 -0
  81. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/environment.py +0 -0
  82. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/import_refs.py +0 -0
  83. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/launch.py +0 -0
  84. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/network_file_system.py +0 -0
  85. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/profile.py +0 -0
  86. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/programs/__init__.py +0 -0
  87. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/programs/run_jupyter.py +0 -0
  88. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/programs/vscode.py +0 -0
  89. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/queues.py +0 -0
  90. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/run.py +0 -0
  91. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/secret.py +0 -0
  92. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/token.py +0 -0
  93. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/utils.py +0 -0
  94. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cli/volume.py +0 -0
  95. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cloud_bucket_mount.py +0 -0
  96. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cloud_bucket_mount.pyi +0 -0
  97. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cls.py +0 -0
  98. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/cls.pyi +0 -0
  99. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/config.py +0 -0
  100. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/container_process.py +0 -0
  101. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/container_process.pyi +0 -0
  102. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/dict.py +0 -0
  103. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/dict.pyi +0 -0
  104. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/environments.py +0 -0
  105. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/environments.pyi +0 -0
  106. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/exception.py +0 -0
  107. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/experimental/__init__.py +0 -0
  108. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/experimental/flash.py +0 -0
  109. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/experimental/flash.pyi +0 -0
  110. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/experimental/ipython.py +0 -0
  111. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/file_io.py +0 -0
  112. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/file_io.pyi +0 -0
  113. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/file_pattern_matcher.py +0 -0
  114. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/functions.py +0 -0
  115. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/functions.pyi +0 -0
  116. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/gpu.py +0 -0
  117. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/image.py +0 -0
  118. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/image.pyi +0 -0
  119. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/io_streams.py +0 -0
  120. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/io_streams.pyi +0 -0
  121. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/mount.py +0 -0
  122. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/mount.pyi +0 -0
  123. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/network_file_system.py +0 -0
  124. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/network_file_system.pyi +0 -0
  125. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/object.py +0 -0
  126. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/object.pyi +0 -0
  127. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/output.py +0 -0
  128. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/partial_function.py +0 -0
  129. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/partial_function.pyi +0 -0
  130. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/proxy.py +0 -0
  131. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/proxy.pyi +0 -0
  132. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/py.typed +0 -0
  133. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/queue.py +0 -0
  134. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/queue.pyi +0 -0
  135. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/2023.12.312.txt +0 -0
  136. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/2023.12.txt +0 -0
  137. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/2024.04.txt +0 -0
  138. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/2024.10.txt +0 -0
  139. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/2025.06.txt +0 -0
  140. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/PREVIEW.txt +0 -0
  141. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/README.md +0 -0
  142. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/requirements/base-images.json +0 -0
  143. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/retries.py +0 -0
  144. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/runner.py +0 -0
  145. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/runner.pyi +0 -0
  146. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/running_app.py +0 -0
  147. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/sandbox.py +0 -0
  148. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/sandbox.pyi +0 -0
  149. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/schedule.py +0 -0
  150. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/scheduler_placement.py +0 -0
  151. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/secret.py +0 -0
  152. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/secret.pyi +0 -0
  153. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/serving.py +0 -0
  154. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/serving.pyi +0 -0
  155. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/snapshot.py +0 -0
  156. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/snapshot.pyi +0 -0
  157. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/stream_type.py +0 -0
  158. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/token_flow.py +0 -0
  159. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/token_flow.pyi +0 -0
  160. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/volume.py +0 -0
  161. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal/volume.pyi +0 -0
  162. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/SOURCES.txt +0 -0
  163. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/dependency_links.txt +0 -0
  164. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/entry_points.txt +0 -0
  165. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/requires.txt +0 -0
  166. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal.egg-info/top_level.txt +0 -0
  167. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/__init__.py +0 -0
  168. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/gen_cli_docs.py +0 -0
  169. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/gen_reference_docs.py +0 -0
  170. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/mdmd/__init__.py +0 -0
  171. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/mdmd/mdmd.py +0 -0
  172. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_docs/mdmd/signatures.py +0 -0
  173. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/__init__.py +0 -0
  174. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api_grpc.py +0 -0
  175. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api_pb2_grpc.py +0 -0
  176. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/api_pb2_grpc.pyi +0 -0
  177. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/modal_api_grpc.py +0 -0
  178. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/modal_options_grpc.py +0 -0
  179. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options.proto +0 -0
  180. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options_grpc.py +0 -0
  181. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options_pb2.py +0 -0
  182. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options_pb2.pyi +0 -0
  183. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options_pb2_grpc.py +0 -0
  184. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/options_pb2_grpc.pyi +0 -0
  185. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_proto/py.typed +0 -0
  186. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/modal_version/__main__.py +0 -0
  187. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/pyproject.toml +0 -0
  188. {modal-1.1.1.dev16 → modal-1.1.1.dev18}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1.dev16
3
+ Version: 1.1.1.dev18
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -75,6 +75,7 @@ from .parallel_map import (
75
75
  _for_each_sync,
76
76
  _map_async,
77
77
  _map_invocation,
78
+ _map_invocation_inputplane,
78
79
  _map_sync,
79
80
  _spawn_map_async,
80
81
  _spawn_map_sync,
@@ -399,7 +400,8 @@ class _InputPlaneInvocation:
399
400
  parent_input_id=current_input_id() or "",
400
401
  input=input_item,
401
402
  )
402
- metadata = await _InputPlaneInvocation._get_metadata(input_plane_region, client)
403
+
404
+ metadata = await client.get_input_plane_metadata(input_plane_region)
403
405
  response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
404
406
  attempt_token = response.attempt_token
405
407
 
@@ -415,7 +417,7 @@ class _InputPlaneInvocation:
415
417
  timeout_secs=OUTPUTS_TIMEOUT,
416
418
  requested_at=time.time(),
417
419
  )
418
- metadata = await self._get_metadata(self.input_plane_region, self.client)
420
+ metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
419
421
  await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
420
422
  self.stub.AttemptAwait,
421
423
  await_request,
@@ -1514,20 +1516,36 @@ Use the `Function.get_web_url()` method instead.
1514
1516
  else:
1515
1517
  count_update_callback = None
1516
1518
 
1517
- async with aclosing(
1518
- _map_invocation(
1519
- self,
1520
- input_queue,
1521
- self.client,
1522
- order_outputs,
1523
- return_exceptions,
1524
- wrap_returned_exceptions,
1525
- count_update_callback,
1526
- api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1527
- )
1528
- ) as stream:
1529
- async for item in stream:
1530
- yield item
1519
+ # TODO(ben-okeefe): Feature gating for input plane map until feature is enabled.
1520
+ if self._input_plane_url and False:
1521
+ async with aclosing(
1522
+ _map_invocation_inputplane(
1523
+ self,
1524
+ input_queue,
1525
+ self.client,
1526
+ order_outputs,
1527
+ return_exceptions,
1528
+ wrap_returned_exceptions,
1529
+ count_update_callback,
1530
+ )
1531
+ ) as stream:
1532
+ async for item in stream:
1533
+ yield item
1534
+ else:
1535
+ async with aclosing(
1536
+ _map_invocation(
1537
+ self,
1538
+ input_queue,
1539
+ self.client,
1540
+ order_outputs,
1541
+ return_exceptions,
1542
+ wrap_returned_exceptions,
1543
+ count_update_callback,
1544
+ api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1545
+ )
1546
+ ) as stream:
1547
+ async for item in stream:
1548
+ yield item
1531
1549
 
1532
1550
  async def _call_function(self, args, kwargs) -> ReturnType:
1533
1551
  invocation: Union[_Invocation, _InputPlaneInvocation]
@@ -27,7 +27,7 @@ class _AuthTokenManager:
27
27
  self._expiry = 0.0
28
28
  self._lock: typing.Union[asyncio.Lock, None] = None
29
29
 
30
- async def get_token(self):
30
+ async def get_token(self) -> str:
31
31
  """
32
32
  When called, the AuthTokenManager can be in one of three states:
33
33
  1. Has a valid cached token. It is returned to the caller.
@@ -542,6 +542,7 @@ def should_upload(
542
542
  )
543
543
 
544
544
 
545
+ # This must be called against the client stub, not the input-plane stub.
545
546
  async def _create_input(
546
547
  args,
547
548
  kwargs,
@@ -268,6 +268,14 @@ class _Client:
268
268
  # Just used from tests.
269
269
  cls._client_from_env = client
270
270
 
271
+ async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]:
272
+ assert self._auth_token_manager, "Client must have an instance of auth token manager."
273
+ token = await self._auth_token_manager.get_token()
274
+ return [
275
+ ("x-modal-input-plane-region", input_plane_region),
276
+ ("x-modal-auth-token", token),
277
+ ]
278
+
271
279
  async def _call_safely(self, coro, readable_method: str):
272
280
  """Runs coroutine wrapped in a task that's part of the client's task context
273
281
 
@@ -456,4 +464,3 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
456
464
  self.wrapped_method.channel = await self.client._get_channel(self.server_url)
457
465
  async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
458
466
  yield response
459
-
@@ -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.1.1.dev16",
36
+ version: str = "1.1.1.dev18",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -112,6 +112,7 @@ class _Client:
112
112
  """mdmd:hidden"""
113
113
  ...
114
114
 
115
+ async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]: ...
115
116
  async def _call_safely(self, coro, readable_method: str):
116
117
  """Runs coroutine wrapped in a task that's part of the client's task context
117
118
 
@@ -163,7 +164,7 @@ class Client:
163
164
  server_url: str,
164
165
  client_type: int,
165
166
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.1.1.dev16",
167
+ version: str = "1.1.1.dev18",
167
168
  ):
168
169
  """mdmd:hidden
169
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -275,6 +276,12 @@ class Client:
275
276
  """mdmd:hidden"""
276
277
  ...
277
278
 
279
+ class __get_input_plane_metadata_spec(typing_extensions.Protocol[SUPERSELF]):
280
+ def __call__(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
281
+ async def aio(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
282
+
283
+ get_input_plane_metadata: __get_input_plane_metadata_spec[typing_extensions.Self]
284
+
278
285
  class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
279
286
  def __call__(self, /, coro, readable_method: str):
280
287
  """Runs coroutine wrapped in a task that's part of the client's task context
@@ -424,6 +424,295 @@ async def _map_invocation(
424
424
  await log_debug_stats_task
425
425
 
426
426
 
427
+ async def _map_invocation_inputplane(
428
+ function: "modal.functions._Function",
429
+ raw_input_queue: _SynchronizedQueue,
430
+ client: "modal.client._Client",
431
+ order_outputs: bool,
432
+ return_exceptions: bool,
433
+ wrap_returned_exceptions: bool,
434
+ count_update_callback: Optional[Callable[[int, int], None]],
435
+ ) -> typing.AsyncGenerator[Any, None]:
436
+ """Input-plane implementation of a function map invocation.
437
+
438
+ This is analogous to `_map_invocation`, but instead of the control-plane
439
+ `FunctionMap` / `FunctionPutInputs` / `FunctionGetOutputs` RPCs it speaks
440
+ the input-plane protocol consisting of `MapStartOrContinue` and `MapAwait`.
441
+
442
+ The implementation purposefully ignores retry handling for now - a stub is
443
+ left in place so that a future change can add support for the retry path
444
+ without re-structuring the surrounding code.
445
+ """
446
+
447
+ assert function._input_plane_url, "_map_invocation_inputplane should only be used for input-plane backed functions"
448
+
449
+ input_plane_stub = await client.get_stub(function._input_plane_url)
450
+
451
+ assert client.stub, "Client must be hydrated with a stub for _map_invocation_inputplane"
452
+
453
+ # ------------------------------------------------------------
454
+ # Invocation-wide state
455
+ # ------------------------------------------------------------
456
+
457
+ have_all_inputs = False
458
+ map_done_event = asyncio.Event()
459
+
460
+ inputs_created = 0
461
+ outputs_completed = 0
462
+
463
+ # The input-plane server returns this after the first request.
464
+ function_call_id: str | None = None
465
+ function_call_id_received = asyncio.Event()
466
+
467
+ # Map of idx -> attempt_token returned by the server. This will be needed
468
+ # for a future client-side retry implementation.
469
+ attempt_tokens: dict[int, str] = {}
470
+
471
+ # Single priority queue that holds *both* fresh inputs (timestamp == now)
472
+ # and future retries (timestamp > now).
473
+ queue: TimestampPriorityQueue[api_pb2.MapStartOrContinueItem] = TimestampPriorityQueue()
474
+
475
+ # Maximum number of inputs that may be in-flight (the server sends this in
476
+ # the first response – fall back to the default if we never receive it for
477
+ # any reason).
478
+ max_inputs_outstanding = MAX_INPUTS_OUTSTANDING_DEFAULT
479
+
480
+ # ------------------------------------------------------------
481
+ # Helper functions
482
+ # ------------------------------------------------------------
483
+
484
+ def update_counters(created_delta: int = 0, completed_delta: int = 0, set_have_all_inputs: bool | None = None):
485
+ nonlocal inputs_created, outputs_completed, have_all_inputs
486
+
487
+ if created_delta:
488
+ inputs_created += created_delta
489
+ if completed_delta:
490
+ outputs_completed += completed_delta
491
+ if set_have_all_inputs is not None:
492
+ have_all_inputs = set_have_all_inputs
493
+
494
+ if count_update_callback is not None:
495
+ count_update_callback(outputs_completed, inputs_created)
496
+
497
+ if have_all_inputs and outputs_completed >= inputs_created:
498
+ map_done_event.set()
499
+
500
+ async def create_input(argskwargs):
501
+ idx = inputs_created + 1 # 1-indexed map call idx
502
+ update_counters(created_delta=1)
503
+ (args, kwargs) = argskwargs
504
+ put_item: api_pb2.FunctionPutInputsItem = await _create_input(
505
+ args,
506
+ kwargs,
507
+ client.stub,
508
+ max_object_size_bytes=function._max_object_size_bytes,
509
+ idx=idx,
510
+ method_name=function._use_method_name,
511
+ )
512
+ return api_pb2.MapStartOrContinueItem(input=put_item)
513
+
514
+ # ------------------------------------------------------------
515
+ # Coroutine: drain user input iterator, upload blobs, enqueue for sending
516
+ # ------------------------------------------------------------
517
+
518
+ async def input_iter():
519
+ while True:
520
+ raw_input = await raw_input_queue.get()
521
+ if raw_input is None: # end of input sentinel
522
+ break
523
+ yield raw_input # args, kwargs
524
+
525
+ async def drain_input_generator():
526
+ async with aclosing(
527
+ async_map_ordered(input_iter(), create_input, concurrency=BLOB_MAX_PARALLELISM)
528
+ ) as streamer:
529
+ async for q_item in streamer:
530
+ await queue.put(time.time(), q_item)
531
+
532
+ # All inputs have been read.
533
+ await queue.close()
534
+ update_counters(set_have_all_inputs=True)
535
+ yield
536
+
537
+ # ------------------------------------------------------------
538
+ # Coroutine: send queued items to the input-plane server
539
+ # ------------------------------------------------------------
540
+
541
+ async def pump_inputs():
542
+ nonlocal function_call_id, max_inputs_outstanding
543
+
544
+ async for batch in queue_batch_iterator(queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
545
+ # Convert the queued items into the proto format expected by the RPC.
546
+ request_items: list[api_pb2.MapStartOrContinueItem] = [
547
+ api_pb2.MapStartOrContinueItem(input=qi.input, attempt_token=qi.attempt_token) for qi in batch
548
+ ]
549
+ # Build request
550
+ request = api_pb2.MapStartOrContinueRequest(
551
+ function_id=function.object_id,
552
+ function_call_id=function_call_id,
553
+ parent_input_id=current_input_id() or "",
554
+ items=request_items,
555
+ )
556
+
557
+ metadata = await client.get_input_plane_metadata(function._input_plane_region)
558
+
559
+ response: api_pb2.MapStartOrContinueResponse = await retry_transient_errors(
560
+ input_plane_stub.MapStartOrContinue, request, metadata=metadata
561
+ )
562
+
563
+ # TODO(ben-okeefe): Understand if an input could be lost at this step and not registered
564
+
565
+ if function_call_id is None:
566
+ function_call_id = response.function_call_id
567
+ function_call_id_received.set()
568
+ max_inputs_outstanding = response.max_inputs_outstanding or MAX_INPUTS_OUTSTANDING_DEFAULT
569
+
570
+ # Record attempt tokens for future retries; also release semaphore slots now that the
571
+ # inputs are officially registered on the server.
572
+ for idx, attempt_token in enumerate(response.attempt_tokens):
573
+ # Client expects the server to return the attempt tokens in the same order as the inputs we sent.
574
+ attempt_tokens[request_items[idx].input.idx] = attempt_token
575
+
576
+ yield
577
+
578
+ # ------------------------------------------------------------
579
+ # Coroutine: **stub** – retry handling will be added in the future
580
+ # ------------------------------------------------------------
581
+
582
+ async def retry_inputs():
583
+ """Temporary stub for retrying inputs. Retry handling will be added in the future."""
584
+
585
+ try:
586
+ while not map_done_event.is_set():
587
+ await asyncio.sleep(1)
588
+ if False:
589
+ yield
590
+ except asyncio.CancelledError:
591
+ pass
592
+
593
+ # ------------------------------------------------------------
594
+ # Coroutine: stream outputs via MapAwait
595
+ # ------------------------------------------------------------
596
+
597
+ async def get_all_outputs():
598
+ """Continuously fetch outputs until the map is complete."""
599
+ last_entry_id = ""
600
+ while not map_done_event.is_set():
601
+ if function_call_id is None:
602
+ await function_call_id_received.wait()
603
+ continue
604
+
605
+ request = api_pb2.MapAwaitRequest(
606
+ function_call_id=function_call_id,
607
+ last_entry_id=last_entry_id,
608
+ requested_at=time.time(),
609
+ timeout=OUTPUTS_TIMEOUT,
610
+ )
611
+ metadata = await client.get_input_plane_metadata(function._input_plane_region)
612
+ response: api_pb2.MapAwaitResponse = await retry_transient_errors(
613
+ input_plane_stub.MapAwait,
614
+ request,
615
+ max_retries=20,
616
+ attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
617
+ metadata=metadata,
618
+ )
619
+ last_entry_id = response.last_entry_id
620
+
621
+ for output_item in response.outputs:
622
+ yield output_item
623
+
624
+ update_counters(completed_delta=1)
625
+
626
+ # The loop condition will exit when map_done_event is set from update_counters.
627
+
628
+ async def get_all_outputs_and_clean_up():
629
+ try:
630
+ async with aclosing(get_all_outputs()) as stream:
631
+ async for item in stream:
632
+ yield item
633
+ finally:
634
+ # We could signal server we are done with outputs so it can clean up.
635
+ pass
636
+
637
+ # ------------------------------------------------------------
638
+ # Coroutine: convert FunctionGetOutputsItem → actual result value
639
+ # ------------------------------------------------------------
640
+
641
+ async def fetch_output(item: api_pb2.FunctionGetOutputsItem) -> tuple[int, Any]:
642
+ try:
643
+ output_val = await _process_result(item.result, item.data_format, input_plane_stub, client)
644
+ except Exception as exc:
645
+ if return_exceptions:
646
+ output_val = exc
647
+ else:
648
+ raise exc
649
+
650
+ return (item.idx, output_val)
651
+
652
+ async def poll_outputs():
653
+ # map to store out-of-order outputs received
654
+ received_outputs = {}
655
+ output_idx = 1 # 1-indexed map call idx
656
+
657
+ async with aclosing(
658
+ async_map_ordered(get_all_outputs_and_clean_up(), fetch_output, concurrency=BLOB_MAX_PARALLELISM)
659
+ ) as streamer:
660
+ async for idx, output in streamer:
661
+ if not order_outputs:
662
+ yield _OutputValue(output)
663
+ else:
664
+ # hold on to outputs for function maps, so we can reorder them correctly.
665
+ received_outputs[idx] = output
666
+
667
+ while True:
668
+ if output_idx not in received_outputs:
669
+ # we haven't received the output for the current index yet.
670
+ # stop returning outputs to the caller and instead wait for
671
+ # the next output to arrive from the server.
672
+ break
673
+
674
+ output = received_outputs.pop(output_idx)
675
+ yield _OutputValue(output)
676
+ output_idx += 1
677
+
678
+ assert len(received_outputs) == 0
679
+
680
+ # ------------------------------------------------------------
681
+ # Debug-logging helper
682
+ # ------------------------------------------------------------
683
+ async def log_debug_stats():
684
+ def log_stats():
685
+ logger.debug(
686
+ "Map-IP stats: have_all_inputs=%s inputs_created=%d outputs_completed=%d queue_size=%d",
687
+ have_all_inputs,
688
+ inputs_created,
689
+ outputs_completed,
690
+ queue.qsize(),
691
+ )
692
+
693
+ while True:
694
+ log_stats()
695
+ try:
696
+ await asyncio.sleep(10)
697
+ except asyncio.CancelledError:
698
+ # Log final stats before exiting
699
+ log_stats()
700
+ break
701
+
702
+ # ------------------------------------------------------------
703
+ # Run the four coroutines concurrently and yield results as they arrive
704
+ # ------------------------------------------------------------
705
+
706
+ log_task = asyncio.create_task(log_debug_stats())
707
+
708
+ async with aclosing(async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())) as merged:
709
+ async for maybe_output in merged:
710
+ if maybe_output is not None: # ignore None sentinels
711
+ yield maybe_output.value
712
+
713
+ log_task.cancel()
714
+
715
+
427
716
  async def _map_helper(
428
717
  self: "modal.functions.Function",
429
718
  async_input_gen: typing.AsyncGenerator[Any, None],
@@ -70,6 +70,27 @@ def _map_invocation(
70
70
  count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
71
71
  function_call_invocation_type: int,
72
72
  ): ...
73
+ def _map_invocation_inputplane(
74
+ function: modal._functions._Function,
75
+ raw_input_queue: _SynchronizedQueue,
76
+ client: modal.client._Client,
77
+ order_outputs: bool,
78
+ return_exceptions: bool,
79
+ wrap_returned_exceptions: bool,
80
+ count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
81
+ ) -> typing.AsyncGenerator[typing.Any, None]:
82
+ """Input-plane implementation of a function map invocation.
83
+
84
+ This is analogous to `_map_invocation`, but instead of the control-plane
85
+ `FunctionMap` / `FunctionPutInputs` / `FunctionGetOutputs` RPCs it speaks
86
+ the input-plane protocol consisting of `MapStartOrContinue` and `MapAwait`.
87
+
88
+ The implementation purposefully ignores retry handling for now - a stub is
89
+ left in place so that a future change can add support for the retry path
90
+ without re-structuring the surrounding code.
91
+ """
92
+ ...
93
+
73
94
  def _map_helper(
74
95
  self: modal.functions.Function,
75
96
  async_input_gen: typing.AsyncGenerator[typing.Any, None],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1.dev16
3
+ Version: 1.1.1.dev18
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -240,6 +240,13 @@ enum SystemErrorCode {
240
240
  SYSTEM_ERROR_CODE_NOSPC = 28; // ENOSPC: No space left on device
241
241
  }
242
242
 
243
+ enum TaskSnapshotBehavior {
244
+ TASK_SNAPSHOT_BEHAVIOR_UNSPECIFIED = 0;
245
+ TASK_SNAPSHOT_BEHAVIOR_SNAPSHOT = 1;
246
+ TASK_SNAPSHOT_BEHAVIOR_RESTORE = 2;
247
+ TASK_SNAPSHOT_BEHAVIOR_NONE = 3;
248
+ }
249
+
243
250
  enum TaskState {
244
251
  TASK_STATE_UNSPECIFIED = 0;
245
252
  TASK_STATE_CREATED = 6;
@@ -2961,6 +2968,7 @@ message TaskInfo {
2961
2968
  double enqueued_at = 5;
2962
2969
  string gpu_type = 6;
2963
2970
  string sandbox_id = 7;
2971
+ TaskSnapshotBehavior snapshot_behavior = 8;
2964
2972
  }
2965
2973
 
2966
2974
  message TaskListRequest {