modal 1.1.1.dev15__tar.gz → 1.1.1.dev17__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.dev15 → modal-1.1.1.dev17}/PKG-INFO +1 -1
  2. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_functions.py +34 -16
  3. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/auth_token_manager.py +1 -1
  4. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/function_utils.py +1 -0
  5. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/client.py +8 -1
  6. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/client.pyi +9 -2
  7. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/dict.py +1 -1
  8. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/functions.pyi +6 -6
  9. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/parallel_map.py +289 -0
  10. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/parallel_map.pyi +21 -0
  11. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/queue.py +1 -1
  12. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/secret.py +3 -3
  13. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/PKG-INFO +1 -1
  14. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api.proto +30 -1
  15. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api_pb2.py +745 -705
  16. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api_pb2.pyi +108 -4
  17. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_version/__init__.py +1 -1
  18. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/LICENSE +0 -0
  19. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/README.md +0 -0
  20. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/__init__.py +0 -0
  21. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/__main__.py +0 -0
  22. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_clustered_functions.py +0 -0
  23. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_clustered_functions.pyi +0 -0
  24. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_container_entrypoint.py +0 -0
  25. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_ipython.py +0 -0
  26. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_location.py +0 -0
  27. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_object.py +0 -0
  28. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_output.py +0 -0
  29. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_partial_function.py +0 -0
  30. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_pty.py +0 -0
  31. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_resolver.py +0 -0
  32. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_resources.py +0 -0
  33. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/__init__.py +0 -0
  34. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/asgi.py +0 -0
  35. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/container_io_manager.py +0 -0
  36. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/container_io_manager.pyi +0 -0
  37. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/execution_context.py +0 -0
  38. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/execution_context.pyi +0 -0
  39. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  40. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/telemetry.py +0 -0
  41. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_runtime/user_code_imports.py +0 -0
  42. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_serialization.py +0 -0
  43. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_traceback.py +0 -0
  44. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_tunnel.py +0 -0
  45. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_tunnel.pyi +0 -0
  46. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_type_manager.py +0 -0
  47. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/__init__.py +0 -0
  48. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/app_utils.py +0 -0
  49. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/async_utils.py +0 -0
  50. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/blob_utils.py +0 -0
  51. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/bytes_io_segment_payload.py +0 -0
  52. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/deprecation.py +0 -0
  53. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/docker_utils.py +0 -0
  54. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/git_utils.py +0 -0
  55. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/grpc_testing.py +0 -0
  56. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/grpc_utils.py +0 -0
  57. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/hash_utils.py +0 -0
  58. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/http_utils.py +0 -0
  59. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/jwt_utils.py +0 -0
  60. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/logger.py +0 -0
  61. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/mount_utils.py +0 -0
  62. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/name_utils.py +0 -0
  63. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/package_utils.py +0 -0
  64. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/pattern_utils.py +0 -0
  65. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/rand_pb_testing.py +0 -0
  66. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/shell_utils.py +0 -0
  67. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_utils/time_utils.py +0 -0
  68. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_vendor/__init__.py +0 -0
  69. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  70. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_vendor/cloudpickle.py +0 -0
  71. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_vendor/tblib.py +0 -0
  72. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/_watcher.py +0 -0
  73. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/app.py +0 -0
  74. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/app.pyi +0 -0
  75. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/call_graph.py +0 -0
  76. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/__init__.py +0 -0
  77. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/_download.py +0 -0
  78. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/_traceback.py +0 -0
  79. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/app.py +0 -0
  80. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/cluster.py +0 -0
  81. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/config.py +0 -0
  82. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/container.py +0 -0
  83. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/dict.py +0 -0
  84. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/entry_point.py +0 -0
  85. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/environment.py +0 -0
  86. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/import_refs.py +0 -0
  87. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/launch.py +0 -0
  88. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/network_file_system.py +0 -0
  89. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/profile.py +0 -0
  90. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/programs/__init__.py +0 -0
  91. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/programs/run_jupyter.py +0 -0
  92. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/programs/vscode.py +0 -0
  93. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/queues.py +0 -0
  94. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/run.py +0 -0
  95. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/secret.py +0 -0
  96. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/token.py +0 -0
  97. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/utils.py +0 -0
  98. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cli/volume.py +0 -0
  99. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cloud_bucket_mount.py +0 -0
  100. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cloud_bucket_mount.pyi +0 -0
  101. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cls.py +0 -0
  102. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/cls.pyi +0 -0
  103. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/config.py +0 -0
  104. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/container_process.py +0 -0
  105. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/container_process.pyi +0 -0
  106. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/dict.pyi +0 -0
  107. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/environments.py +0 -0
  108. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/environments.pyi +0 -0
  109. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/exception.py +0 -0
  110. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/experimental/__init__.py +0 -0
  111. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/experimental/flash.py +0 -0
  112. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/experimental/flash.pyi +0 -0
  113. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/experimental/ipython.py +0 -0
  114. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/file_io.py +0 -0
  115. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/file_io.pyi +0 -0
  116. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/file_pattern_matcher.py +0 -0
  117. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/functions.py +0 -0
  118. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/gpu.py +0 -0
  119. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/image.py +0 -0
  120. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/image.pyi +0 -0
  121. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/io_streams.py +0 -0
  122. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/io_streams.pyi +0 -0
  123. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/mount.py +0 -0
  124. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/mount.pyi +0 -0
  125. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/network_file_system.py +0 -0
  126. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/network_file_system.pyi +0 -0
  127. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/object.py +0 -0
  128. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/object.pyi +0 -0
  129. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/output.py +0 -0
  130. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/partial_function.py +0 -0
  131. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/partial_function.pyi +0 -0
  132. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/proxy.py +0 -0
  133. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/proxy.pyi +0 -0
  134. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/py.typed +0 -0
  135. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/queue.pyi +0 -0
  136. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/2023.12.312.txt +0 -0
  137. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/2023.12.txt +0 -0
  138. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/2024.04.txt +0 -0
  139. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/2024.10.txt +0 -0
  140. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/2025.06.txt +0 -0
  141. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/PREVIEW.txt +0 -0
  142. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/README.md +0 -0
  143. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/requirements/base-images.json +0 -0
  144. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/retries.py +0 -0
  145. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/runner.py +0 -0
  146. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/runner.pyi +0 -0
  147. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/running_app.py +0 -0
  148. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/sandbox.py +0 -0
  149. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/sandbox.pyi +0 -0
  150. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/schedule.py +0 -0
  151. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/scheduler_placement.py +0 -0
  152. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/secret.pyi +0 -0
  153. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/serving.py +0 -0
  154. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/serving.pyi +0 -0
  155. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/snapshot.py +0 -0
  156. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/snapshot.pyi +0 -0
  157. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/stream_type.py +0 -0
  158. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/token_flow.py +0 -0
  159. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/token_flow.pyi +0 -0
  160. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/volume.py +0 -0
  161. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal/volume.pyi +0 -0
  162. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/SOURCES.txt +0 -0
  163. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/dependency_links.txt +0 -0
  164. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/entry_points.txt +0 -0
  165. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/requires.txt +0 -0
  166. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal.egg-info/top_level.txt +0 -0
  167. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/__init__.py +0 -0
  168. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/gen_cli_docs.py +0 -0
  169. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/gen_reference_docs.py +0 -0
  170. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/mdmd/__init__.py +0 -0
  171. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/mdmd/mdmd.py +0 -0
  172. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_docs/mdmd/signatures.py +0 -0
  173. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/__init__.py +0 -0
  174. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api_grpc.py +0 -0
  175. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api_pb2_grpc.py +0 -0
  176. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/api_pb2_grpc.pyi +0 -0
  177. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/modal_api_grpc.py +0 -0
  178. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/modal_options_grpc.py +0 -0
  179. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options.proto +0 -0
  180. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options_grpc.py +0 -0
  181. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options_pb2.py +0 -0
  182. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options_pb2.pyi +0 -0
  183. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options_pb2_grpc.py +0 -0
  184. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/options_pb2_grpc.pyi +0 -0
  185. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_proto/py.typed +0 -0
  186. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/modal_version/__main__.py +0 -0
  187. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/pyproject.toml +0 -0
  188. {modal-1.1.1.dev15 → modal-1.1.1.dev17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1.dev15
3
+ Version: 1.1.1.dev17
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.dev15",
36
+ version: str = "1.1.1.dev17",
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.dev15",
167
+ version: str = "1.1.1.dev17",
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
@@ -153,7 +153,7 @@ class _Dict(_Object, type_prefix="di"):
153
153
  )
154
154
  response = await resolver.client.stub.DictGetOrCreate(req)
155
155
  logger.debug(f"Created dict with id {response.dict_id}")
156
- self._hydrate(response.dict_id, resolver.client, None)
156
+ self._hydrate(response.dict_id, resolver.client, response.metadata)
157
157
 
158
158
  return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
159
159
 
@@ -428,7 +428,7 @@ class Function(
428
428
 
429
429
  _call_generator: ___call_generator_spec[typing_extensions.Self]
430
430
 
431
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
431
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
432
432
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
433
433
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
434
434
  ...
@@ -437,7 +437,7 @@ class Function(
437
437
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
438
438
  ...
439
439
 
440
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
440
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
441
441
 
442
442
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
443
443
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -464,7 +464,7 @@ class Function(
464
464
  """
465
465
  ...
466
466
 
467
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
467
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
468
468
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
469
469
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
470
470
 
@@ -488,7 +488,7 @@ class Function(
488
488
  ...
489
489
 
490
490
  _experimental_spawn: ___experimental_spawn_spec[
491
- modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
491
+ modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
492
492
  ]
493
493
 
494
494
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -497,7 +497,7 @@ class Function(
497
497
 
498
498
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
499
499
 
500
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
500
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
501
501
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
502
502
  """Calls the function with the given arguments, without waiting for the results.
503
503
 
@@ -518,7 +518,7 @@ class Function(
518
518
  """
519
519
  ...
520
520
 
521
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
521
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
522
522
 
523
523
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
524
524
  """Return the inner Python object wrapped by this Modal Function."""
@@ -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],
@@ -173,7 +173,7 @@ class _Queue(_Object, type_prefix="qu"):
173
173
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
174
174
  )
175
175
  response = await resolver.client.stub.QueueGetOrCreate(req)
176
- self._hydrate(response.queue_id, resolver.client, None)
176
+ self._hydrate(response.queue_id, resolver.client, response.metadata)
177
177
 
178
178
  return _Queue._from_loader(_load, "Queue()", is_another_app=True, hydrate_lazily=True)
179
179
 
@@ -73,7 +73,7 @@ class _Secret(_Object, type_prefix="st"):
73
73
  if exc.status == Status.FAILED_PRECONDITION:
74
74
  raise InvalidError(exc.message)
75
75
  raise
76
- self._hydrate(resp.secret_id, resolver.client, None)
76
+ self._hydrate(resp.secret_id, resolver.client, resp.metadata)
77
77
 
78
78
  rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
79
79
  return _Secret._from_loader(_load, rep, hydrate_lazily=True)
@@ -157,7 +157,7 @@ class _Secret(_Object, type_prefix="st"):
157
157
  )
158
158
  resp = await resolver.client.stub.SecretGetOrCreate(req)
159
159
 
160
- self._hydrate(resp.secret_id, resolver.client, None)
160
+ self._hydrate(resp.secret_id, resolver.client, resp.metadata)
161
161
 
162
162
  return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
163
163
 
@@ -200,7 +200,7 @@ class _Secret(_Object, type_prefix="st"):
200
200
  raise NotFoundError(exc.message)
201
201
  else:
202
202
  raise
203
- self._hydrate(response.secret_id, resolver.client, None)
203
+ self._hydrate(response.secret_id, resolver.client, response.metadata)
204
204
 
205
205
  return _Secret._from_loader(_load, "Secret()", hydrate_lazily=True)
206
206
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1.dev15
3
+ Version: 1.1.1.dev17
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0