modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__py3-none-any.whl

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 (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
@@ -9,13 +9,25 @@ import synchronicity.combined_types
9
9
  import typing
10
10
  import typing_extensions
11
11
 
12
- class UserException(Exception): ...
13
- class Sentinel: ...
12
+ class UserException(Exception):
13
+ """Used to shut down the task gracefully."""
14
+
15
+ ...
16
+
17
+ class Sentinel:
18
+ """Used to get type-stubs to work with this object."""
19
+
20
+ ...
14
21
 
15
22
  class IOContext:
23
+ """Context object for managing input, function calls, and function executions
24
+ in a batched or single input context.
25
+ """
26
+
16
27
  input_ids: list[str]
17
28
  retry_counts: list[int]
18
29
  function_call_ids: list[str]
30
+ attempt_tokens: list[str]
19
31
  function_inputs: list[modal_proto.api_pb2.FunctionInput]
20
32
  finalized_function: modal._runtime.user_code_imports.FinalizedFunction
21
33
  _cancel_issued: bool
@@ -26,32 +38,56 @@ class IOContext:
26
38
  input_ids: list[str],
27
39
  retry_counts: list[int],
28
40
  function_call_ids: list[str],
41
+ attempt_tokens: list[str],
29
42
  finalized_function: modal._runtime.user_code_imports.FinalizedFunction,
30
43
  function_inputs: list[modal_proto.api_pb2.FunctionInput],
31
44
  is_batched: bool,
32
45
  client: modal.client._Client,
33
- ): ...
46
+ ):
47
+ """Initialize self. See help(type(self)) for accurate signature."""
48
+ ...
49
+
34
50
  @classmethod
35
51
  async def create(
36
52
  cls,
37
53
  client: modal.client._Client,
38
54
  finalized_functions: dict[str, modal._runtime.user_code_imports.FinalizedFunction],
39
- inputs: list[tuple[str, int, str, modal_proto.api_pb2.FunctionInput]],
55
+ inputs: list[tuple[str, int, str, str, modal_proto.api_pb2.FunctionInput]],
40
56
  is_batched: bool,
41
57
  ) -> IOContext: ...
42
58
  def set_cancel_callback(self, cb: collections.abc.Callable[[], None]): ...
43
59
  def cancel(self): ...
44
60
  def _args_and_kwargs(self) -> tuple[tuple[typing.Any, ...], dict[str, list[typing.Any]]]: ...
45
- def call_finalized_function(self) -> typing.Any: ...
46
- def validate_output_data(self, data: typing.Any) -> list[typing.Any]: ...
61
+ def _generator_output_format(self) -> int: ...
62
+ def _prepare_batch_output(self, data: typing.Any) -> list[typing.Any]: ...
63
+ def call_function_sync(self) -> list[typing.Any]: ...
64
+ async def call_function_async(self) -> list[typing.Any]: ...
65
+ def call_generator_sync(self) -> typing.Generator[typing.Any, None, None]: ...
66
+ def call_generator_async(self) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
67
+ async def output_items_cancellation(self, started_at: float): ...
68
+ def _determine_output_format(self, input_format: int) -> int: ...
69
+ async def output_items_exception(
70
+ self, started_at: float, task_id: str, exc: BaseException
71
+ ) -> list[modal_proto.api_pb2.FunctionPutOutputsItem]: ...
72
+ def output_items_generator_done(
73
+ self, started_at: float, items_total: int
74
+ ) -> list[modal_proto.api_pb2.FunctionPutOutputsItem]: ...
75
+ async def output_items(
76
+ self, started_at: float, data: list[typing.Any]
77
+ ) -> list[modal_proto.api_pb2.FunctionPutOutputsItem]: ...
47
78
 
48
79
  class InputSlots:
80
+ """A semaphore that allows dynamically adjusting the concurrency."""
81
+
49
82
  active: int
50
83
  value: int
51
84
  waiter: typing.Optional[asyncio.Future]
52
85
  closed: bool
53
86
 
54
- def __init__(self, value: int) -> None: ...
87
+ def __init__(self, value: int) -> None:
88
+ """Initialize self. See help(type(self)) for accurate signature."""
89
+ ...
90
+
55
91
  async def acquire(self) -> None: ...
56
92
  def _wake_waiter(self) -> None: ...
57
93
  def release(self) -> None: ...
@@ -59,11 +95,18 @@ class InputSlots:
59
95
  async def close(self) -> None: ...
60
96
 
61
97
  class _ContainerIOManager:
98
+ """Synchronizes all RPC calls and network operations for a running container.
99
+
100
+ TODO: maybe we shouldn't synchronize the whole class.
101
+ Then we could potentially move a bunch of the global functions onto it.
102
+ """
103
+
62
104
  task_id: str
63
105
  function_id: str
64
106
  app_id: str
65
107
  function_def: modal_proto.api_pb2.Function
66
108
  checkpoint_id: typing.Optional[str]
109
+ input_plane_server_url: typing.Optional[str]
67
110
  calls_completed: int
68
111
  total_user_time: float
69
112
  current_input_id: typing.Optional[str]
@@ -81,7 +124,6 @@ class _ContainerIOManager:
81
124
  _is_interactivity_enabled: bool
82
125
  _fetching_inputs: bool
83
126
  _client: modal.client._Client
84
- _GENERATOR_STOP_SENTINEL: typing.ClassVar[Sentinel]
85
127
  _singleton: typing.ClassVar[typing.Optional[_ContainerIOManager]]
86
128
 
87
129
  def _init(self, container_args: modal_proto.api_pb2.ContainerArguments, client: modal.client._Client): ...
@@ -90,9 +132,15 @@ class _ContainerIOManager:
90
132
  @staticmethod
91
133
  def __new__(
92
134
  cls, container_args: modal_proto.api_pb2.ContainerArguments, client: modal.client._Client
93
- ) -> _ContainerIOManager: ...
135
+ ) -> _ContainerIOManager:
136
+ """Create and return a new object. See help(type) for accurate signature."""
137
+ ...
138
+
94
139
  @classmethod
95
- def _reset_singleton(cls): ...
140
+ def _reset_singleton(cls):
141
+ """Only used for tests."""
142
+ ...
143
+
96
144
  async def hello(self): ...
97
145
  async def _run_heartbeat_loop(self): ...
98
146
  async def _heartbeat_handle_cancellations(self) -> bool: ...
@@ -100,46 +148,82 @@ class _ContainerIOManager:
100
148
  def stop_heartbeat(self): ...
101
149
  def dynamic_concurrency_manager(self) -> typing.AsyncContextManager[None]: ...
102
150
  async def _dynamic_concurrency_loop(self): ...
103
- def serialize_data_format(self, obj: typing.Any, data_format: int) -> bytes: ...
104
- async def format_blob_data(self, data: bytes) -> dict[str, typing.Any]: ...
105
- def get_data_in(self, function_call_id: str) -> collections.abc.AsyncIterator[typing.Any]: ...
151
+ def get_data_in(
152
+ self, function_call_id: str, attempt_token: typing.Optional[str]
153
+ ) -> collections.abc.AsyncIterator[typing.Any]:
154
+ """Read from the `data_in` stream of a function call."""
155
+ ...
156
+
106
157
  async def put_data_out(
107
- self, function_call_id: str, start_index: int, data_format: int, serialized_messages: list[typing.Any]
108
- ) -> None: ...
109
- async def generator_output_task(
110
- self, function_call_id: str, data_format: int, message_rx: asyncio.queues.Queue
111
- ) -> None: ...
112
- async def _queue_create(self, size: int) -> asyncio.queues.Queue: ...
113
- async def _queue_put(self, queue: asyncio.queues.Queue, value: typing.Any) -> None: ...
158
+ self,
159
+ function_call_id: str,
160
+ attempt_token: str,
161
+ start_index: int,
162
+ data_format: int,
163
+ serialized_messages: list[typing.Any],
164
+ ) -> None:
165
+ """Put data onto the `data_out` stream of a function call.
166
+
167
+ This is used for generator outputs, which includes web endpoint responses. Note that this
168
+ was introduced as a performance optimization in client version 0.57, so older clients will
169
+ still use the previous Postgres-backed system based on `FunctionPutOutputs()`.
170
+ """
171
+ ...
172
+
173
+ def generator_output_sender(
174
+ self, function_call_id: str, attempt_token: str, data_format: int, message_rx: asyncio.queues.Queue
175
+ ) -> typing.AsyncContextManager[None]:
176
+ """Runs background task that feeds generator outputs into a function call's `data_out` stream."""
177
+ ...
178
+
179
+ async def _queue_create(self, size: int) -> asyncio.queues.Queue:
180
+ """Create a queue, on the synchronicity event loop (needed on Python 3.8 and 3.9)."""
181
+ ...
182
+
183
+ async def _queue_put(self, queue: asyncio.queues.Queue, value: typing.Any) -> None:
184
+ """Put a value onto a queue, using the synchronicity event loop."""
185
+ ...
186
+
114
187
  def get_average_call_time(self) -> float: ...
115
188
  def get_max_inputs_to_fetch(self): ...
116
189
  def _generate_inputs(
117
190
  self, batch_max_size: int, batch_wait_ms: int
118
- ) -> collections.abc.AsyncIterator[list[tuple[str, int, str, modal_proto.api_pb2.FunctionInput]]]: ...
191
+ ) -> collections.abc.AsyncIterator[list[tuple[str, int, str, str, modal_proto.api_pb2.FunctionInput]]]: ...
119
192
  def run_inputs_outputs(
120
193
  self,
121
194
  finalized_functions: dict[str, modal._runtime.user_code_imports.FinalizedFunction],
122
195
  batch_max_size: int = 0,
123
196
  batch_wait_ms: int = 0,
124
197
  ) -> collections.abc.AsyncIterator[IOContext]: ...
125
- async def _push_outputs(
126
- self,
127
- io_context: IOContext,
128
- started_at: float,
129
- data_format: int,
130
- results: list[modal_proto.api_pb2.GenericResult],
131
- ) -> None: ...
132
- def serialize_exception(self, exc: BaseException) -> bytes: ...
133
- def serialize_traceback(self, exc: BaseException) -> tuple[typing.Optional[bytes], typing.Optional[bytes]]: ...
134
- def handle_user_exception(self) -> typing.AsyncContextManager[None]: ...
135
- def handle_input_exception(self, io_context: IOContext, started_at: float) -> typing.AsyncContextManager[None]: ...
198
+ async def _send_outputs(self, started_at: float, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
199
+ """Send pre-built output items with retry and chunking."""
200
+ ...
201
+
202
+ def handle_user_exception(self) -> typing.AsyncContextManager[None]:
203
+ """Sets the task as failed in a way where it's not retried.
204
+
205
+ Used for handling exceptions from container lifecycle methods at the moment, which should
206
+ trigger a task failure state.
207
+ """
208
+ ...
209
+
210
+ def handle_input_exception(self, io_context: IOContext, started_at: float) -> typing.AsyncContextManager[None]:
211
+ """Handle an exception while processing a function input."""
212
+ ...
213
+
136
214
  def exit_context(self, started_at, input_ids: list[str]): ...
137
- async def push_outputs(
138
- self, io_context: IOContext, started_at: float, data: typing.Any, data_format: int
139
- ) -> None: ...
215
+ async def push_outputs(self, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
140
216
  async def memory_restore(self) -> None: ...
141
- async def memory_snapshot(self) -> None: ...
142
- async def volume_commit(self, volume_ids: list[str]) -> None: ...
217
+ async def memory_snapshot(self) -> None:
218
+ """Message server indicating that function is ready to be checkpointed."""
219
+ ...
220
+
221
+ async def volume_commit(self, volume_ids: list[str]) -> None:
222
+ """Perform volume commit for given `volume_ids`.
223
+ Only used on container exit to persist uncommitted changes on behalf of user.
224
+ """
225
+ ...
226
+
143
227
  async def interact(self, from_breakpoint: bool = False): ...
144
228
  @property
145
229
  def target_concurrency(self) -> int: ...
@@ -148,20 +232,41 @@ class _ContainerIOManager:
148
232
  @property
149
233
  def input_concurrency_enabled(self) -> int: ...
150
234
  @classmethod
151
- def get_input_concurrency(cls) -> int: ...
235
+ def get_input_concurrency(cls) -> int:
236
+ """Returns the number of usable input slots.
237
+
238
+ If concurrency is reduced, active slots can exceed allotted slots. Returns the larger value
239
+ in this case.
240
+ """
241
+ ...
242
+
152
243
  @classmethod
153
- def set_input_concurrency(cls, concurrency: int): ...
244
+ def set_input_concurrency(cls, concurrency: int):
245
+ """Edit the number of input slots.
246
+
247
+ This disables the background loop which automatically adjusts concurrency
248
+ within [target_concurrency, max_concurrency].
249
+ """
250
+ ...
251
+
154
252
  @classmethod
155
253
  def stop_fetching_inputs(cls): ...
156
254
 
157
255
  SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
158
256
 
159
257
  class ContainerIOManager:
258
+ """Synchronizes all RPC calls and network operations for a running container.
259
+
260
+ TODO: maybe we shouldn't synchronize the whole class.
261
+ Then we could potentially move a bunch of the global functions onto it.
262
+ """
263
+
160
264
  task_id: str
161
265
  function_id: str
162
266
  app_id: str
163
267
  function_def: modal_proto.api_pb2.Function
164
268
  checkpoint_id: typing.Optional[str]
269
+ input_plane_server_url: typing.Optional[str]
165
270
  calls_completed: int
166
271
  total_user_time: float
167
272
  current_input_id: typing.Optional[str]
@@ -179,15 +284,19 @@ class ContainerIOManager:
179
284
  _is_interactivity_enabled: bool
180
285
  _fetching_inputs: bool
181
286
  _client: modal.client.Client
182
- _GENERATOR_STOP_SENTINEL: typing.ClassVar[Sentinel]
183
287
  _singleton: typing.ClassVar[typing.Optional[ContainerIOManager]]
184
288
 
185
- def __init__(self, /, *args, **kwargs): ...
289
+ def __init__(self, /, *args, **kwargs):
290
+ """Initialize self. See help(type(self)) for accurate signature."""
291
+ ...
292
+
186
293
  def _init(self, container_args: modal_proto.api_pb2.ContainerArguments, client: modal.client.Client): ...
187
294
  @property
188
295
  def heartbeat_condition(self) -> asyncio.locks.Condition: ...
189
296
  @classmethod
190
- def _reset_singleton(cls): ...
297
+ def _reset_singleton(cls):
298
+ """Only used for tests."""
299
+ ...
191
300
 
192
301
  class __hello_spec(typing_extensions.Protocol[SUPERSELF]):
193
302
  def __call__(self, /): ...
@@ -229,45 +338,92 @@ class ContainerIOManager:
229
338
 
230
339
  _dynamic_concurrency_loop: ___dynamic_concurrency_loop_spec[typing_extensions.Self]
231
340
 
232
- def serialize_data_format(self, obj: typing.Any, data_format: int) -> bytes: ...
233
-
234
- class __format_blob_data_spec(typing_extensions.Protocol[SUPERSELF]):
235
- def __call__(self, /, data: bytes) -> dict[str, typing.Any]: ...
236
- async def aio(self, /, data: bytes) -> dict[str, typing.Any]: ...
237
-
238
- format_blob_data: __format_blob_data_spec[typing_extensions.Self]
239
-
240
341
  class __get_data_in_spec(typing_extensions.Protocol[SUPERSELF]):
241
- def __call__(self, /, function_call_id: str) -> typing.Iterator[typing.Any]: ...
242
- def aio(self, /, function_call_id: str) -> collections.abc.AsyncIterator[typing.Any]: ...
342
+ def __call__(
343
+ self, /, function_call_id: str, attempt_token: typing.Optional[str]
344
+ ) -> typing.Iterator[typing.Any]:
345
+ """Read from the `data_in` stream of a function call."""
346
+ ...
347
+
348
+ def aio(
349
+ self, /, function_call_id: str, attempt_token: typing.Optional[str]
350
+ ) -> collections.abc.AsyncIterator[typing.Any]:
351
+ """Read from the `data_in` stream of a function call."""
352
+ ...
243
353
 
244
354
  get_data_in: __get_data_in_spec[typing_extensions.Self]
245
355
 
246
356
  class __put_data_out_spec(typing_extensions.Protocol[SUPERSELF]):
247
357
  def __call__(
248
- self, /, function_call_id: str, start_index: int, data_format: int, serialized_messages: list[typing.Any]
249
- ) -> None: ...
358
+ self,
359
+ /,
360
+ function_call_id: str,
361
+ attempt_token: str,
362
+ start_index: int,
363
+ data_format: int,
364
+ serialized_messages: list[typing.Any],
365
+ ) -> None:
366
+ """Put data onto the `data_out` stream of a function call.
367
+
368
+ This is used for generator outputs, which includes web endpoint responses. Note that this
369
+ was introduced as a performance optimization in client version 0.57, so older clients will
370
+ still use the previous Postgres-backed system based on `FunctionPutOutputs()`.
371
+ """
372
+ ...
373
+
250
374
  async def aio(
251
- self, /, function_call_id: str, start_index: int, data_format: int, serialized_messages: list[typing.Any]
252
- ) -> None: ...
375
+ self,
376
+ /,
377
+ function_call_id: str,
378
+ attempt_token: str,
379
+ start_index: int,
380
+ data_format: int,
381
+ serialized_messages: list[typing.Any],
382
+ ) -> None:
383
+ """Put data onto the `data_out` stream of a function call.
384
+
385
+ This is used for generator outputs, which includes web endpoint responses. Note that this
386
+ was introduced as a performance optimization in client version 0.57, so older clients will
387
+ still use the previous Postgres-backed system based on `FunctionPutOutputs()`.
388
+ """
389
+ ...
253
390
 
254
391
  put_data_out: __put_data_out_spec[typing_extensions.Self]
255
392
 
256
- class __generator_output_task_spec(typing_extensions.Protocol[SUPERSELF]):
257
- def __call__(self, /, function_call_id: str, data_format: int, message_rx: asyncio.queues.Queue) -> None: ...
258
- async def aio(self, /, function_call_id: str, data_format: int, message_rx: asyncio.queues.Queue) -> None: ...
393
+ class __generator_output_sender_spec(typing_extensions.Protocol[SUPERSELF]):
394
+ def __call__(
395
+ self, /, function_call_id: str, attempt_token: str, data_format: int, message_rx: asyncio.queues.Queue
396
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]:
397
+ """Runs background task that feeds generator outputs into a function call's `data_out` stream."""
398
+ ...
259
399
 
260
- generator_output_task: __generator_output_task_spec[typing_extensions.Self]
400
+ def aio(
401
+ self, /, function_call_id: str, attempt_token: str, data_format: int, message_rx: asyncio.queues.Queue
402
+ ) -> typing.AsyncContextManager[None]:
403
+ """Runs background task that feeds generator outputs into a function call's `data_out` stream."""
404
+ ...
405
+
406
+ generator_output_sender: __generator_output_sender_spec[typing_extensions.Self]
261
407
 
262
408
  class ___queue_create_spec(typing_extensions.Protocol[SUPERSELF]):
263
- def __call__(self, /, size: int) -> asyncio.queues.Queue: ...
264
- async def aio(self, /, size: int) -> asyncio.queues.Queue: ...
409
+ def __call__(self, /, size: int) -> asyncio.queues.Queue:
410
+ """Create a queue, on the synchronicity event loop (needed on Python 3.8 and 3.9)."""
411
+ ...
412
+
413
+ async def aio(self, /, size: int) -> asyncio.queues.Queue:
414
+ """Create a queue, on the synchronicity event loop (needed on Python 3.8 and 3.9)."""
415
+ ...
265
416
 
266
417
  _queue_create: ___queue_create_spec[typing_extensions.Self]
267
418
 
268
419
  class ___queue_put_spec(typing_extensions.Protocol[SUPERSELF]):
269
- def __call__(self, /, queue: asyncio.queues.Queue, value: typing.Any) -> None: ...
270
- async def aio(self, /, queue: asyncio.queues.Queue, value: typing.Any) -> None: ...
420
+ def __call__(self, /, queue: asyncio.queues.Queue, value: typing.Any) -> None:
421
+ """Put a value onto a queue, using the synchronicity event loop."""
422
+ ...
423
+
424
+ async def aio(self, /, queue: asyncio.queues.Queue, value: typing.Any) -> None:
425
+ """Put a value onto a queue, using the synchronicity event loop."""
426
+ ...
271
427
 
272
428
  _queue_put: ___queue_put_spec[typing_extensions.Self]
273
429
 
@@ -277,10 +433,10 @@ class ContainerIOManager:
277
433
  class ___generate_inputs_spec(typing_extensions.Protocol[SUPERSELF]):
278
434
  def __call__(
279
435
  self, /, batch_max_size: int, batch_wait_ms: int
280
- ) -> typing.Iterator[list[tuple[str, int, str, modal_proto.api_pb2.FunctionInput]]]: ...
436
+ ) -> typing.Iterator[list[tuple[str, int, str, str, modal_proto.api_pb2.FunctionInput]]]: ...
281
437
  def aio(
282
438
  self, /, batch_max_size: int, batch_wait_ms: int
283
- ) -> collections.abc.AsyncIterator[list[tuple[str, int, str, modal_proto.api_pb2.FunctionInput]]]: ...
439
+ ) -> collections.abc.AsyncIterator[list[tuple[str, int, str, str, modal_proto.api_pb2.FunctionInput]]]: ...
284
440
 
285
441
  _generate_inputs: ___generate_inputs_spec[typing_extensions.Self]
286
442
 
@@ -302,50 +458,54 @@ class ContainerIOManager:
302
458
 
303
459
  run_inputs_outputs: __run_inputs_outputs_spec[typing_extensions.Self]
304
460
 
305
- class ___push_outputs_spec(typing_extensions.Protocol[SUPERSELF]):
306
- def __call__(
307
- self,
308
- /,
309
- io_context: IOContext,
310
- started_at: float,
311
- data_format: int,
312
- results: list[modal_proto.api_pb2.GenericResult],
313
- ) -> None: ...
314
- async def aio(
315
- self,
316
- /,
317
- io_context: IOContext,
318
- started_at: float,
319
- data_format: int,
320
- results: list[modal_proto.api_pb2.GenericResult],
321
- ) -> None: ...
461
+ class ___send_outputs_spec(typing_extensions.Protocol[SUPERSELF]):
462
+ def __call__(self, /, started_at: float, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
463
+ """Send pre-built output items with retry and chunking."""
464
+ ...
322
465
 
323
- _push_outputs: ___push_outputs_spec[typing_extensions.Self]
466
+ async def aio(self, /, started_at: float, outputs: list[modal_proto.api_pb2.FunctionPutOutputsItem]) -> None:
467
+ """Send pre-built output items with retry and chunking."""
468
+ ...
324
469
 
325
- def serialize_exception(self, exc: BaseException) -> bytes: ...
326
- def serialize_traceback(self, exc: BaseException) -> tuple[typing.Optional[bytes], typing.Optional[bytes]]: ...
470
+ _send_outputs: ___send_outputs_spec[typing_extensions.Self]
327
471
 
328
472
  class __handle_user_exception_spec(typing_extensions.Protocol[SUPERSELF]):
329
- def __call__(self, /) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]: ...
330
- def aio(self, /) -> typing.AsyncContextManager[None]: ...
473
+ def __call__(self, /) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]:
474
+ """Sets the task as failed in a way where it's not retried.
475
+
476
+ Used for handling exceptions from container lifecycle methods at the moment, which should
477
+ trigger a task failure state.
478
+ """
479
+ ...
480
+
481
+ def aio(self, /) -> typing.AsyncContextManager[None]:
482
+ """Sets the task as failed in a way where it's not retried.
483
+
484
+ Used for handling exceptions from container lifecycle methods at the moment, which should
485
+ trigger a task failure state.
486
+ """
487
+ ...
331
488
 
332
489
  handle_user_exception: __handle_user_exception_spec[typing_extensions.Self]
333
490
 
334
491
  class __handle_input_exception_spec(typing_extensions.Protocol[SUPERSELF]):
335
492
  def __call__(
336
493
  self, /, io_context: IOContext, started_at: float
337
- ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]: ...
338
- def aio(self, /, io_context: IOContext, started_at: float) -> typing.AsyncContextManager[None]: ...
494
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[None]:
495
+ """Handle an exception while processing a function input."""
496
+ ...
497
+
498
+ def aio(self, /, io_context: IOContext, started_at: float) -> typing.AsyncContextManager[None]:
499
+ """Handle an exception while processing a function input."""
500
+ ...
339
501
 
340
502
  handle_input_exception: __handle_input_exception_spec[typing_extensions.Self]
341
503
 
342
504
  def exit_context(self, started_at, input_ids: list[str]): ...
343
505
 
344
506
  class __push_outputs_spec(typing_extensions.Protocol[SUPERSELF]):
345
- def __call__(self, /, io_context: IOContext, started_at: float, data: typing.Any, data_format: int) -> None: ...
346
- async def aio(
347
- self, /, io_context: IOContext, started_at: float, data: typing.Any, data_format: int
348
- ) -> None: ...
507
+ def __call__(self, /, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
508
+ async def aio(self, /, io_context: IOContext, started_at: float, output_data: list[typing.Any]) -> None: ...
349
509
 
350
510
  push_outputs: __push_outputs_spec[typing_extensions.Self]
351
511
 
@@ -356,14 +516,28 @@ class ContainerIOManager:
356
516
  memory_restore: __memory_restore_spec[typing_extensions.Self]
357
517
 
358
518
  class __memory_snapshot_spec(typing_extensions.Protocol[SUPERSELF]):
359
- def __call__(self, /) -> None: ...
360
- async def aio(self, /) -> None: ...
519
+ def __call__(self, /) -> None:
520
+ """Message server indicating that function is ready to be checkpointed."""
521
+ ...
522
+
523
+ async def aio(self, /) -> None:
524
+ """Message server indicating that function is ready to be checkpointed."""
525
+ ...
361
526
 
362
527
  memory_snapshot: __memory_snapshot_spec[typing_extensions.Self]
363
528
 
364
529
  class __volume_commit_spec(typing_extensions.Protocol[SUPERSELF]):
365
- def __call__(self, /, volume_ids: list[str]) -> None: ...
366
- async def aio(self, /, volume_ids: list[str]) -> None: ...
530
+ def __call__(self, /, volume_ids: list[str]) -> None:
531
+ """Perform volume commit for given `volume_ids`.
532
+ Only used on container exit to persist uncommitted changes on behalf of user.
533
+ """
534
+ ...
535
+
536
+ async def aio(self, /, volume_ids: list[str]) -> None:
537
+ """Perform volume commit for given `volume_ids`.
538
+ Only used on container exit to persist uncommitted changes on behalf of user.
539
+ """
540
+ ...
367
541
 
368
542
  volume_commit: __volume_commit_spec[typing_extensions.Self]
369
543
 
@@ -380,13 +554,34 @@ class ContainerIOManager:
380
554
  @property
381
555
  def input_concurrency_enabled(self) -> int: ...
382
556
  @classmethod
383
- def get_input_concurrency(cls) -> int: ...
557
+ def get_input_concurrency(cls) -> int:
558
+ """Returns the number of usable input slots.
559
+
560
+ If concurrency is reduced, active slots can exceed allotted slots. Returns the larger value
561
+ in this case.
562
+ """
563
+ ...
564
+
384
565
  @classmethod
385
- def set_input_concurrency(cls, concurrency: int): ...
566
+ def set_input_concurrency(cls, concurrency: int):
567
+ """Edit the number of input slots.
568
+
569
+ This disables the background loop which automatically adjusts concurrency
570
+ within [target_concurrency, max_concurrency].
571
+ """
572
+ ...
573
+
386
574
  @classmethod
387
575
  def stop_fetching_inputs(cls): ...
388
576
 
389
- def check_fastapi_pydantic_compatibility(exc: ImportError) -> None: ...
577
+ def check_fastapi_pydantic_compatibility(exc: ImportError) -> None:
578
+ """Add a helpful note to an exception that is likely caused by a pydantic<>fastapi version incompatibility.
579
+
580
+ We need this becasue the legacy set of container requirements (image_builder_version=2023.12) contains a
581
+ version of fastapi that is not forwards-compatible with pydantic 2.0+, and users commonly run into issues
582
+ building an image that specifies a more recent version only for pydantic.
583
+ """
584
+ ...
390
585
 
391
586
  MAX_OUTPUT_BATCH_SIZE: int
392
587
 
@@ -72,22 +72,38 @@ def current_function_call_id() -> Optional[str]:
72
72
  return None
73
73
 
74
74
 
75
- def _set_current_context_ids(input_ids: list[str], function_call_ids: list[str]) -> Callable[[], None]:
76
- assert len(input_ids) == len(function_call_ids) and len(input_ids) > 0
75
+ def current_attempt_token() -> Optional[str]:
76
+ # This ContextVar isn't useful to expose to users.
77
+ try:
78
+ return _current_attempt_token.get()
79
+ except LookupError:
80
+ return None
81
+
82
+
83
+ def _set_current_context_ids(
84
+ input_ids: list[str], function_call_ids: list[str], attempt_tokens: list[str]
85
+ ) -> Callable[[], None]:
86
+ assert len(input_ids) == len(function_call_ids) == len(attempt_tokens) and input_ids
87
+
77
88
  input_id = input_ids[0]
78
89
  function_call_id = function_call_ids[0]
90
+ attempt_token = attempt_tokens[0]
91
+
79
92
  input_token = _current_input_id.set(input_id)
80
93
  function_call_token = _current_function_call_id.set(function_call_id)
94
+ attempt_token_token = _current_attempt_token.set(attempt_token)
81
95
 
82
96
  def _reset_current_context_ids():
83
97
  _current_input_id.reset(input_token)
84
98
  _current_function_call_id.reset(function_call_token)
99
+ _current_attempt_token.reset(attempt_token_token)
85
100
 
86
101
  return _reset_current_context_ids
87
102
 
88
103
 
89
104
  _current_input_id: ContextVar = ContextVar("_current_input_id")
90
105
  _current_function_call_id: ContextVar = ContextVar("_current_function_call_id")
106
+ _current_attempt_token: ContextVar = ContextVar("_current_attempt_token")
91
107
 
92
108
  _is_currently_importing = False # we set this to True while a container is importing user code
93
109