modal 1.2.1.dev12__py3-none-any.whl → 1.2.1.dev14__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.

@@ -292,7 +292,9 @@ class TaskCommandRouterClient:
292
292
  lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
293
293
  )
294
294
 
295
- async def exec_poll(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecPollResponse:
295
+ async def exec_poll(
296
+ self, task_id: str, exec_id: str, deadline: Optional[float] = None
297
+ ) -> sr_pb2.TaskExecPollResponse:
296
298
  """Poll for the exit status of an exec'd command, properly retrying on transient errors.
297
299
 
298
300
  Args:
@@ -302,13 +304,25 @@ class TaskCommandRouterClient:
302
304
  sr_pb2.TaskExecPollResponse: The exit status of the command if it has completed.
303
305
 
304
306
  Raises:
307
+ ExecTimeoutError: If the deadline is exceeded.
305
308
  Other errors: If retries are exhausted on transient errors or if there's an error
306
309
  from the RPC itself.
307
310
  """
308
311
  request = sr_pb2.TaskExecPollRequest(task_id=task_id, exec_id=exec_id)
309
- return await call_with_retries_on_transient_errors(
310
- lambda: self._call_with_auth_retry(self._stub.TaskExecPoll, request)
311
- )
312
+ # The timeout here is really a backstop in the event of a hang contacting
313
+ # the command router. Poll should usually be instantaneous.
314
+ timeout = deadline - time.monotonic() if deadline is not None else None
315
+ if timeout is not None and timeout <= 0:
316
+ raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
317
+ try:
318
+ return await asyncio.wait_for(
319
+ call_with_retries_on_transient_errors(
320
+ lambda: self._call_with_auth_retry(self._stub.TaskExecPoll, request)
321
+ ),
322
+ timeout=timeout,
323
+ )
324
+ except asyncio.TimeoutError:
325
+ raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
312
326
 
313
327
  async def exec_wait(
314
328
  self,
modal/cli/cluster.py CHANGED
@@ -83,7 +83,9 @@ async def shell(
83
83
  )
84
84
  exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
85
85
  if pty:
86
- await _ContainerProcess(exec_res.exec_id, client).attach()
86
+ await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
87
87
  else:
88
88
  # TODO: redirect stderr to its own stream?
89
- await _ContainerProcess(exec_res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
89
+ await _ContainerProcess(
90
+ exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
91
+ ).wait()
modal/cli/container.py CHANGED
@@ -80,10 +80,12 @@ async def exec(
80
80
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
81
81
 
82
82
  if pty:
83
- await _ContainerProcess(res.exec_id, client).attach()
83
+ await _ContainerProcess(res.exec_id, container_id, client).attach()
84
84
  else:
85
85
  # TODO: redirect stderr to its own stream?
86
- await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
86
+ await _ContainerProcess(
87
+ res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
88
+ ).wait()
87
89
 
88
90
 
89
91
  @container_cli.command("stop")
modal/client.pyi CHANGED
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.2.1.dev12",
36
+ version: str = "1.2.1.dev14",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.2.1.dev12",
167
+ version: str = "1.2.1.dev14",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -9,16 +9,17 @@ from modal_proto import api_pb2
9
9
  from ._utils.async_utils import TaskContext, synchronize_api
10
10
  from ._utils.grpc_utils import retry_transient_errors
11
11
  from ._utils.shell_utils import stream_from_stdin, write_to_fd
12
+ from ._utils.task_command_router_client import TaskCommandRouterClient
12
13
  from .client import _Client
13
14
  from .config import logger
14
- from .exception import InteractiveTimeoutError, InvalidError
15
+ from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
15
16
  from .io_streams import _StreamReader, _StreamWriter
16
17
  from .stream_type import StreamType
17
18
 
18
19
  T = TypeVar("T", str, bytes)
19
20
 
20
21
 
21
- class _ContainerProcess(Generic[T]):
22
+ class _ContainerProcessThroughServer(Generic[T]):
22
23
  _process_id: Optional[str] = None
23
24
  _stdout: _StreamReader[T]
24
25
  _stderr: _StreamReader[T]
@@ -31,6 +32,7 @@ class _ContainerProcess(Generic[T]):
31
32
  def __init__(
32
33
  self,
33
34
  process_id: str,
35
+ task_id: str,
34
36
  client: _Client,
35
37
  stdout: StreamType = StreamType.PIPE,
36
38
  stderr: StreamType = StreamType.PIPE,
@@ -52,6 +54,7 @@ class _ContainerProcess(Generic[T]):
52
54
  text=text,
53
55
  by_line=by_line,
54
56
  deadline=exec_deadline,
57
+ task_id=task_id,
55
58
  )
56
59
  self._stderr = _StreamReader[T](
57
60
  api_pb2.FILE_DESCRIPTOR_STDERR,
@@ -62,6 +65,7 @@ class _ContainerProcess(Generic[T]):
62
65
  text=text,
63
66
  by_line=by_line,
64
67
  deadline=exec_deadline,
68
+ task_id=task_id,
65
69
  )
66
70
  self._stdin = _StreamWriter(process_id, "container_process", self._client)
67
71
 
@@ -201,4 +205,266 @@ class _ContainerProcess(Generic[T]):
201
205
  raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
202
206
 
203
207
 
208
+ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
209
+ """Yield raw bytes from a StreamReader regardless of text mode/backend."""
210
+ async for part in stream:
211
+ if isinstance(part, str):
212
+ yield part.encode("utf-8")
213
+ else:
214
+ yield part
215
+
216
+
217
+ class _ContainerProcessThroughCommandRouter(Generic[T]):
218
+ """
219
+ Container process implementation that works via direct communication with
220
+ the Modal worker where the container is running.
221
+ """
222
+
223
+ def __init__(
224
+ self,
225
+ process_id: str,
226
+ client: _Client,
227
+ command_router_client: TaskCommandRouterClient,
228
+ task_id: str,
229
+ *,
230
+ stdout: StreamType = StreamType.PIPE,
231
+ stderr: StreamType = StreamType.PIPE,
232
+ exec_deadline: Optional[float] = None,
233
+ text: bool = True,
234
+ by_line: bool = False,
235
+ ) -> None:
236
+ self._client = client
237
+ self._command_router_client = command_router_client
238
+ self._process_id = process_id
239
+ self._exec_deadline = exec_deadline
240
+ self._text = text
241
+ self._by_line = by_line
242
+ self._task_id = task_id
243
+ self._stdout = _StreamReader[T](
244
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
245
+ process_id,
246
+ "container_process",
247
+ self._client,
248
+ stream_type=stdout,
249
+ text=text,
250
+ by_line=by_line,
251
+ deadline=exec_deadline,
252
+ command_router_client=self._command_router_client,
253
+ task_id=self._task_id,
254
+ )
255
+ self._stderr = _StreamReader[T](
256
+ api_pb2.FILE_DESCRIPTOR_STDERR,
257
+ process_id,
258
+ "container_process",
259
+ self._client,
260
+ stream_type=stderr,
261
+ text=text,
262
+ by_line=by_line,
263
+ deadline=exec_deadline,
264
+ command_router_client=self._command_router_client,
265
+ task_id=self._task_id,
266
+ )
267
+ self._stdin = _StreamWriter(
268
+ process_id,
269
+ "container_process",
270
+ self._client,
271
+ command_router_client=self._command_router_client,
272
+ task_id=self._task_id,
273
+ )
274
+ self._returncode = None
275
+
276
+ @property
277
+ def stdout(self) -> _StreamReader[T]:
278
+ return self._stdout
279
+
280
+ @property
281
+ def stderr(self) -> _StreamReader[T]:
282
+ return self._stderr
283
+
284
+ @property
285
+ def stdin(self) -> _StreamWriter:
286
+ return self._stdin
287
+
288
+ @property
289
+ def returncode(self) -> int:
290
+ if self._returncode is None:
291
+ raise InvalidError(
292
+ "You must call wait() before accessing the returncode. "
293
+ "To poll for the status of a running process, use poll() instead."
294
+ )
295
+ return self._returncode
296
+
297
+ async def poll(self) -> Optional[int]:
298
+ if self._returncode is not None:
299
+ return self._returncode
300
+ try:
301
+ resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
302
+ which = resp.WhichOneof("exit_status")
303
+ if which is None:
304
+ return None
305
+
306
+ if which == "code":
307
+ self._returncode = int(resp.code)
308
+ return self._returncode
309
+ elif which == "signal":
310
+ self._returncode = 128 + int(resp.signal)
311
+ return self._returncode
312
+ else:
313
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
314
+ raise InvalidError("Unexpected exit status")
315
+ except ExecTimeoutError:
316
+ logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
317
+ return None
318
+ except Exception as e:
319
+ # Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
320
+ logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
321
+ raise
322
+
323
+ async def wait(self) -> int:
324
+ if self._returncode is not None:
325
+ return self._returncode
326
+
327
+ try:
328
+ resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
329
+ which = resp.WhichOneof("exit_status")
330
+ if which == "code":
331
+ self._returncode = int(resp.code)
332
+ elif which == "signal":
333
+ self._returncode = 128 + int(resp.signal)
334
+ else:
335
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
336
+ self._returncode = -1
337
+ raise InvalidError("Unexpected exit status")
338
+ except ExecTimeoutError:
339
+ logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
340
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
341
+ # should be a ExecTimeoutError.
342
+ self._returncode = -1
343
+
344
+ return self._returncode
345
+
346
+ async def attach(self):
347
+ if platform.system() == "Windows":
348
+ print("interactive exec is not currently supported on Windows.")
349
+ return
350
+
351
+ from ._output import make_console
352
+
353
+ console = make_console()
354
+
355
+ connecting_status = console.status("Connecting...")
356
+ connecting_status.start()
357
+ on_connect = asyncio.Event()
358
+
359
+ async def _write_to_fd_loop(stream: _StreamReader[T]):
360
+ async for chunk in _iter_stream_as_bytes(stream):
361
+ if chunk is None:
362
+ break
363
+
364
+ if not on_connect.is_set():
365
+ connecting_status.stop()
366
+ on_connect.set()
367
+
368
+ await write_to_fd(stream.file_descriptor, chunk)
369
+
370
+ async def _handle_input(data: bytes, message_index: int):
371
+ self.stdin.write(data)
372
+ await self.stdin.drain()
373
+
374
+ async with TaskContext() as tc:
375
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
376
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
377
+
378
+ try:
379
+ # Time out if we can't connect fast enough.
380
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
381
+
382
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
383
+ await stdout_task
384
+ await stderr_task
385
+
386
+ except (asyncio.TimeoutError, TimeoutError):
387
+ connecting_status.stop()
388
+ stdout_task.cancel()
389
+ stderr_task.cancel()
390
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
391
+
392
+
393
+ class _ContainerProcess(Generic[T]):
394
+ """Represents a running process in a container."""
395
+
396
+ def __init__(
397
+ self,
398
+ process_id: str,
399
+ task_id: str,
400
+ client: _Client,
401
+ stdout: StreamType = StreamType.PIPE,
402
+ stderr: StreamType = StreamType.PIPE,
403
+ exec_deadline: Optional[float] = None,
404
+ text: bool = True,
405
+ by_line: bool = False,
406
+ command_router_client: Optional[TaskCommandRouterClient] = None,
407
+ ) -> None:
408
+ if command_router_client is None:
409
+ self._impl = _ContainerProcessThroughServer(
410
+ process_id,
411
+ task_id,
412
+ client,
413
+ stdout=stdout,
414
+ stderr=stderr,
415
+ exec_deadline=exec_deadline,
416
+ text=text,
417
+ by_line=by_line,
418
+ )
419
+ else:
420
+ self._impl = _ContainerProcessThroughCommandRouter(
421
+ process_id,
422
+ client,
423
+ command_router_client,
424
+ task_id,
425
+ stdout=stdout,
426
+ stderr=stderr,
427
+ exec_deadline=exec_deadline,
428
+ text=text,
429
+ by_line=by_line,
430
+ )
431
+
432
+ def __repr__(self) -> str:
433
+ return self._impl.__repr__()
434
+
435
+ @property
436
+ def stdout(self) -> _StreamReader[T]:
437
+ """StreamReader for the container process's stdout stream."""
438
+ return self._impl.stdout
439
+
440
+ @property
441
+ def stderr(self) -> _StreamReader[T]:
442
+ """StreamReader for the container process's stderr stream."""
443
+ return self._impl.stderr
444
+
445
+ @property
446
+ def stdin(self) -> _StreamWriter:
447
+ """StreamWriter for the container process's stdin stream."""
448
+ return self._impl.stdin
449
+
450
+ @property
451
+ def returncode(self) -> int:
452
+ return self._impl.returncode
453
+
454
+ async def poll(self) -> Optional[int]:
455
+ """Check if the container process has finished running.
456
+
457
+ Returns `None` if the process is still running, else returns the exit code.
458
+ """
459
+ return await self._impl.poll()
460
+
461
+ async def wait(self) -> int:
462
+ """Wait for the container process to finish running. Returns the exit code."""
463
+ return await self._impl.wait()
464
+
465
+ async def attach(self):
466
+ """mdmd:hidden"""
467
+ await self._impl.attach()
468
+
469
+
204
470
  ContainerProcess = synchronize_api(_ContainerProcess)
@@ -1,3 +1,4 @@
1
+ import modal._utils.task_command_router_client
1
2
  import modal.client
2
3
  import modal.io_streams
3
4
  import modal.stream_type
@@ -6,7 +7,7 @@ import typing_extensions
6
7
 
7
8
  T = typing.TypeVar("T")
8
9
 
9
- class _ContainerProcess(typing.Generic[T]):
10
+ class _ContainerProcessThroughServer(typing.Generic[T]):
10
11
  """Abstract base class for generic types.
11
12
 
12
13
  A generic type is typically declared by inheriting from
@@ -39,6 +40,7 @@ class _ContainerProcess(typing.Generic[T]):
39
40
  def __init__(
40
41
  self,
41
42
  process_id: str,
43
+ task_id: str,
42
44
  client: modal.client._Client,
43
45
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
44
46
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
@@ -86,47 +88,110 @@ class _ContainerProcess(typing.Generic[T]):
86
88
  """mdmd:hidden"""
87
89
  ...
88
90
 
89
- SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
91
+ def _iter_stream_as_bytes(stream: modal.io_streams._StreamReader[T]):
92
+ """Yield raw bytes from a StreamReader regardless of text mode/backend."""
93
+ ...
90
94
 
91
- class ContainerProcess(typing.Generic[T]):
92
- """Abstract base class for generic types.
95
+ class _ContainerProcessThroughCommandRouter(typing.Generic[T]):
96
+ """Container process implementation that works via direct communication with
97
+ the Modal worker where the container is running.
98
+ """
99
+ def __init__(
100
+ self,
101
+ process_id: str,
102
+ client: modal.client._Client,
103
+ command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
104
+ task_id: str,
105
+ *,
106
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
107
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
108
+ exec_deadline: typing.Optional[float] = None,
109
+ text: bool = True,
110
+ by_line: bool = False,
111
+ ) -> None:
112
+ """Initialize self. See help(type(self)) for accurate signature."""
113
+ ...
93
114
 
94
- A generic type is typically declared by inheriting from
95
- this class parameterized with one or more type variables.
96
- For example, a generic mapping type might be defined as::
115
+ @property
116
+ def stdout(self) -> modal.io_streams._StreamReader[T]: ...
117
+ @property
118
+ def stderr(self) -> modal.io_streams._StreamReader[T]: ...
119
+ @property
120
+ def stdin(self) -> modal.io_streams._StreamWriter: ...
121
+ @property
122
+ def returncode(self) -> int: ...
123
+ async def poll(self) -> typing.Optional[int]: ...
124
+ async def wait(self) -> int: ...
125
+ async def attach(self): ...
97
126
 
98
- class Mapping(Generic[KT, VT]):
99
- def __getitem__(self, key: KT) -> VT:
100
- ...
101
- # Etc.
127
+ class _ContainerProcess(typing.Generic[T]):
128
+ """Represents a running process in a container."""
129
+ def __init__(
130
+ self,
131
+ process_id: str,
132
+ task_id: str,
133
+ client: modal.client._Client,
134
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
135
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
136
+ exec_deadline: typing.Optional[float] = None,
137
+ text: bool = True,
138
+ by_line: bool = False,
139
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
140
+ ) -> None:
141
+ """Initialize self. See help(type(self)) for accurate signature."""
142
+ ...
102
143
 
103
- This class can then be used as follows::
144
+ def __repr__(self) -> str:
145
+ """Return repr(self)."""
146
+ ...
104
147
 
105
- def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
106
- try:
107
- return mapping[key]
108
- except KeyError:
109
- return default
110
- """
148
+ @property
149
+ def stdout(self) -> modal.io_streams._StreamReader[T]:
150
+ """StreamReader for the container process's stdout stream."""
151
+ ...
111
152
 
112
- _process_id: typing.Optional[str]
113
- _stdout: modal.io_streams.StreamReader[T]
114
- _stderr: modal.io_streams.StreamReader[T]
115
- _stdin: modal.io_streams.StreamWriter
116
- _exec_deadline: typing.Optional[float]
117
- _text: bool
118
- _by_line: bool
119
- _returncode: typing.Optional[int]
153
+ @property
154
+ def stderr(self) -> modal.io_streams._StreamReader[T]:
155
+ """StreamReader for the container process's stderr stream."""
156
+ ...
157
+
158
+ @property
159
+ def stdin(self) -> modal.io_streams._StreamWriter:
160
+ """StreamWriter for the container process's stdin stream."""
161
+ ...
162
+
163
+ @property
164
+ def returncode(self) -> int: ...
165
+ async def poll(self) -> typing.Optional[int]:
166
+ """Check if the container process has finished running.
120
167
 
168
+ Returns `None` if the process is still running, else returns the exit code.
169
+ """
170
+ ...
171
+
172
+ async def wait(self) -> int:
173
+ """Wait for the container process to finish running. Returns the exit code."""
174
+ ...
175
+
176
+ async def attach(self):
177
+ """mdmd:hidden"""
178
+ ...
179
+
180
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
181
+
182
+ class ContainerProcess(typing.Generic[T]):
183
+ """Represents a running process in a container."""
121
184
  def __init__(
122
185
  self,
123
186
  process_id: str,
187
+ task_id: str,
124
188
  client: modal.client.Client,
125
189
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
126
190
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
127
191
  exec_deadline: typing.Optional[float] = None,
128
192
  text: bool = True,
129
193
  by_line: bool = False,
194
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
130
195
  ) -> None: ...
131
196
  def __repr__(self) -> str: ...
132
197
  @property
@@ -164,12 +229,6 @@ class ContainerProcess(typing.Generic[T]):
164
229
 
165
230
  poll: __poll_spec[typing_extensions.Self]
166
231
 
167
- class ___wait_for_completion_spec(typing_extensions.Protocol[SUPERSELF]):
168
- def __call__(self, /) -> int: ...
169
- async def aio(self, /) -> int: ...
170
-
171
- _wait_for_completion: ___wait_for_completion_spec[typing_extensions.Self]
172
-
173
232
  class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
174
233
  def __call__(self, /) -> int:
175
234
  """Wait for the container process to finish running. Returns the exit code."""
modal/functions.pyi CHANGED
@@ -401,7 +401,7 @@ class Function(
401
401
 
402
402
  _call_generator: ___call_generator_spec[typing_extensions.Self]
403
403
 
404
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
404
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
405
405
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
406
406
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
407
407
  ...
@@ -410,7 +410,7 @@ class Function(
410
410
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
411
411
  ...
412
412
 
413
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
413
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
414
414
 
415
415
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
416
416
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -437,7 +437,7 @@ class Function(
437
437
  """
438
438
  ...
439
439
 
440
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
440
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
441
441
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
442
442
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
443
443
 
@@ -461,7 +461,7 @@ class Function(
461
461
  ...
462
462
 
463
463
  _experimental_spawn: ___experimental_spawn_spec[
464
- modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
464
+ modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
465
465
  ]
466
466
 
467
467
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -470,7 +470,7 @@ class Function(
470
470
 
471
471
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
472
472
 
473
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
473
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
474
474
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
475
475
  """Calls the function with the given arguments, without waiting for the results.
476
476
 
@@ -491,7 +491,7 @@ class Function(
491
491
  """
492
492
  ...
493
493
 
494
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
494
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
495
495
 
496
496
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
497
497
  """Return the inner Python object wrapped by this Modal Function."""
modal/io_streams.py CHANGED
@@ -605,7 +605,7 @@ class _StreamReader(Generic[T]):
605
605
  MAX_BUFFER_SIZE = 2 * 1024 * 1024
606
606
 
607
607
 
608
- class _StreamWriter:
608
+ class _StreamWriterThroughServer:
609
609
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
610
610
 
611
611
  def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client) -> None:
@@ -627,25 +627,6 @@ class _StreamWriter:
627
627
 
628
628
  This is non-blocking and queues the data to an internal buffer. Must be
629
629
  used along with the `drain()` method, which flushes the buffer.
630
-
631
- **Usage**
632
-
633
- ```python fixture:running_app
634
- from modal import Sandbox
635
-
636
- sandbox = Sandbox.create(
637
- "bash",
638
- "-c",
639
- "while read line; do echo $line; done",
640
- app=running_app,
641
- )
642
- sandbox.stdin.write(b"foo\\n")
643
- sandbox.stdin.write(b"bar\\n")
644
- sandbox.stdin.write_eof()
645
-
646
- sandbox.stdin.drain()
647
- sandbox.wait()
648
- ```
649
630
  """
650
631
  if self._is_closed:
651
632
  raise ValueError("Stdin is closed. Cannot write to it.")
@@ -653,7 +634,7 @@ class _StreamWriter:
653
634
  if isinstance(data, str):
654
635
  data = data.encode("utf-8")
655
636
  if len(self._buffer) + len(data) > MAX_BUFFER_SIZE:
656
- raise BufferError("Buffer size exceed limit. Call drain to clear the buffer.")
637
+ raise BufferError("Buffer size exceed limit. Call drain to flush the buffer.")
657
638
  self._buffer.extend(data)
658
639
  else:
659
640
  raise TypeError(f"data argument must be a bytes-like object, not {type(data).__name__}")
@@ -672,19 +653,6 @@ class _StreamWriter:
672
653
 
673
654
  This is a flow control method that blocks until data is sent. It returns
674
655
  when it is appropriate to continue writing data to the stream.
675
-
676
- **Usage**
677
-
678
- ```python notest
679
- writer.write(data)
680
- writer.drain()
681
- ```
682
-
683
- Async usage:
684
- ```python notest
685
- writer.write(data) # not a blocking operation
686
- await writer.drain.aio()
687
- ```
688
656
  """
689
657
  data = bytes(self._buffer)
690
658
  self._buffer.clear()
@@ -713,5 +681,127 @@ class _StreamWriter:
713
681
  raise exc
714
682
 
715
683
 
684
+ class _StreamWriterThroughCommandRouter:
685
+ def __init__(
686
+ self,
687
+ object_id: str,
688
+ command_router_client: TaskCommandRouterClient,
689
+ task_id: str,
690
+ ) -> None:
691
+ self._object_id = object_id
692
+ self._command_router_client = command_router_client
693
+ self._task_id = task_id
694
+ self._is_closed = False
695
+ self._buffer = bytearray()
696
+ self._offset = 0
697
+
698
+ def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
699
+ if self._is_closed:
700
+ raise ValueError("Stdin is closed. Cannot write to it.")
701
+ if isinstance(data, (bytes, bytearray, memoryview, str)):
702
+ if isinstance(data, str):
703
+ data = data.encode("utf-8")
704
+ if len(self._buffer) + len(data) > MAX_BUFFER_SIZE:
705
+ raise BufferError("Buffer size exceed limit. Call drain to flush the buffer.")
706
+ self._buffer.extend(data)
707
+ else:
708
+ raise TypeError(f"data argument must be a bytes-like object, not {type(data).__name__}")
709
+
710
+ def write_eof(self) -> None:
711
+ self._is_closed = True
712
+
713
+ async def drain(self) -> None:
714
+ eof = self._is_closed
715
+ # NB: There's no need to prevent writing eof twice, because the command router will ignore the second EOF.
716
+ if self._buffer or eof:
717
+ data = bytes(self._buffer)
718
+ await self._command_router_client.exec_stdin_write(
719
+ task_id=self._task_id, exec_id=self._object_id, offset=self._offset, data=data, eof=eof
720
+ )
721
+ # Only clear the buffer after writing the data to the command router is successful.
722
+ # This allows the client to retry drain() in the event of an exception (though
723
+ # exec_stdin_write already retries on transient errors, so most users will probably
724
+ # not do this).
725
+ self._buffer.clear()
726
+ self._offset += len(data)
727
+
728
+
729
+ class _StreamWriter:
730
+ """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
731
+
732
+ def __init__(
733
+ self,
734
+ object_id: str,
735
+ object_type: Literal["sandbox", "container_process"],
736
+ client: _Client,
737
+ command_router_client: Optional[TaskCommandRouterClient] = None,
738
+ task_id: Optional[str] = None,
739
+ ) -> None:
740
+ """mdmd:hidden"""
741
+ if command_router_client is None:
742
+ self._impl = _StreamWriterThroughServer(object_id, object_type, client)
743
+ else:
744
+ assert task_id is not None
745
+ assert object_type == "container_process"
746
+ self._impl = _StreamWriterThroughCommandRouter(object_id, command_router_client, task_id=task_id)
747
+
748
+ def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
749
+ """Write data to the stream but does not send it immediately.
750
+
751
+ This is non-blocking and queues the data to an internal buffer. Must be
752
+ used along with the `drain()` method, which flushes the buffer.
753
+
754
+ **Usage**
755
+
756
+ ```python fixture:running_app
757
+ from modal import Sandbox
758
+
759
+ sandbox = Sandbox.create(
760
+ "bash",
761
+ "-c",
762
+ "while read line; do echo $line; done",
763
+ app=running_app,
764
+ )
765
+ sandbox.stdin.write(b"foo\\n")
766
+ sandbox.stdin.write(b"bar\\n")
767
+ sandbox.stdin.write_eof()
768
+
769
+ sandbox.stdin.drain()
770
+ sandbox.wait()
771
+ ```
772
+ """
773
+ self._impl.write(data)
774
+
775
+ def write_eof(self) -> None:
776
+ """Close the write end of the stream after the buffered data is drained.
777
+
778
+ If the process was blocked on input, it will become unblocked after
779
+ `write_eof()`. This method needs to be used along with the `drain()`
780
+ method, which flushes the EOF to the process.
781
+ """
782
+ self._impl.write_eof()
783
+
784
+ async def drain(self) -> None:
785
+ """Flush the write buffer and send data to the running process.
786
+
787
+ This is a flow control method that blocks until data is sent. It returns
788
+ when it is appropriate to continue writing data to the stream.
789
+
790
+ **Usage**
791
+
792
+ ```python notest
793
+ writer.write(data)
794
+ writer.drain()
795
+ ```
796
+
797
+ Async usage:
798
+ ```python notest
799
+ writer.write(data) # not a blocking operation
800
+ await writer.drain.aio()
801
+ ```
802
+ """
803
+ await self._impl.drain()
804
+
805
+
716
806
  StreamReader = synchronize_api(_StreamReader)
717
807
  StreamWriter = synchronize_api(_StreamWriter)
modal/io_streams.pyi CHANGED
@@ -249,7 +249,7 @@ class _StreamReader(typing.Generic[T]):
249
249
  """mdmd:hidden"""
250
250
  ...
251
251
 
252
- class _StreamWriter:
252
+ class _StreamWriterThroughServer:
253
253
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
254
254
  def __init__(
255
255
  self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client._Client
@@ -258,6 +258,58 @@ class _StreamWriter:
258
258
  ...
259
259
 
260
260
  def _get_next_index(self) -> int: ...
261
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
262
+ """Write data to the stream but does not send it immediately.
263
+
264
+ This is non-blocking and queues the data to an internal buffer. Must be
265
+ used along with the `drain()` method, which flushes the buffer.
266
+ """
267
+ ...
268
+
269
+ def write_eof(self) -> None:
270
+ """Close the write end of the stream after the buffered data is drained.
271
+
272
+ If the process was blocked on input, it will become unblocked after
273
+ `write_eof()`. This method needs to be used along with the `drain()`
274
+ method, which flushes the EOF to the process.
275
+ """
276
+ ...
277
+
278
+ async def drain(self) -> None:
279
+ """Flush the write buffer and send data to the running process.
280
+
281
+ This is a flow control method that blocks until data is sent. It returns
282
+ when it is appropriate to continue writing data to the stream.
283
+ """
284
+ ...
285
+
286
+ class _StreamWriterThroughCommandRouter:
287
+ def __init__(
288
+ self,
289
+ object_id: str,
290
+ command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
291
+ task_id: str,
292
+ ) -> None:
293
+ """Initialize self. See help(type(self)) for accurate signature."""
294
+ ...
295
+
296
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
297
+ def write_eof(self) -> None: ...
298
+ async def drain(self) -> None: ...
299
+
300
+ class _StreamWriter:
301
+ """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
302
+ def __init__(
303
+ self,
304
+ object_id: str,
305
+ object_type: typing.Literal["sandbox", "container_process"],
306
+ client: modal.client._Client,
307
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
308
+ task_id: typing.Optional[str] = None,
309
+ ) -> None:
310
+ """mdmd:hidden"""
311
+ ...
312
+
261
313
  def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
262
314
  """Write data to the stream but does not send it immediately.
263
315
 
@@ -423,12 +475,16 @@ class StreamReader(typing.Generic[T]):
423
475
  class StreamWriter:
424
476
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
425
477
  def __init__(
426
- self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client.Client
478
+ self,
479
+ object_id: str,
480
+ object_type: typing.Literal["sandbox", "container_process"],
481
+ client: modal.client.Client,
482
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
483
+ task_id: typing.Optional[str] = None,
427
484
  ) -> None:
428
485
  """mdmd:hidden"""
429
486
  ...
430
487
 
431
- def _get_next_index(self) -> int: ...
432
488
  def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
433
489
  """Write data to the stream but does not send it immediately.
434
490
 
modal/sandbox.py CHANGED
@@ -870,6 +870,7 @@ class _Sandbox(_Object, type_prefix="sb"):
870
870
  logger.debug(f"Created ContainerProcess for exec_id {resp.exec_id} on Sandbox {self.object_id}")
871
871
  return _ContainerProcess(
872
872
  resp.exec_id,
873
+ task_id,
873
874
  self._client,
874
875
  stdout=stdout,
875
876
  stderr=stderr,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.1.dev12
3
+ Version: 1.2.1.dev14
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -24,14 +24,14 @@ modal/app.pyi,sha256=AUV5Rp8qQrZJTP2waoKHFY7rYgsXNMYibMcCAQKuSeo,50544
24
24
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
25
25
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
26
26
  modal/client.py,sha256=kyAIVB3Ay-XKJizQ_1ufUFB__EagV0MLmHJpyYyJ7J0,18636
27
- modal/client.pyi,sha256=46-sOq2ZFWLaRP_M6tYYyxhIpAggNOg8AD2JAAzevN4,15831
27
+ modal/client.pyi,sha256=2_DiKPDQsg7NASk_5NqJmNb9HYieLChxPmte-GpCPIY,15831
28
28
  modal/cloud_bucket_mount.py,sha256=I2GRXYhOWLIz2kJZjXu75jAm9EJkBNcutGc6jR2ReUw,5928
29
29
  modal/cloud_bucket_mount.pyi,sha256=VuUOipMIHqFXMkD-3g2bsoqpSxV5qswlFHDOqPQzYAo,7405
30
30
  modal/cls.py,sha256=ZxzivE3fNci4-A5uyBYNAzXMXtdqDg3gnYvgbdy5fhg,40384
31
31
  modal/cls.pyi,sha256=jJsDPFoqzM4ht-V-e-xEJKJ5TINLF0fYtoBm_UeAW5Y,27281
32
32
  modal/config.py,sha256=hpgkgQKbjzo6gVbRzXQrky72_KpdSEm65RNi1M2iNjc,13038
33
- modal/container_process.py,sha256=DnqlgHiM-7rgVdJNcaXyZlXFD6DFLxEgMSFkieQ6Oj0,7452
34
- modal/container_process.pyi,sha256=9m-st3hCUlNN1GOTctfPPvIvoLtEl7FbuGWwif5-7YU,6037
33
+ modal/container_process.py,sha256=X5gkIpXeoeaRemrOFHIpI2XYSVq5NmP7iP54cTqvMFE,16643
34
+ modal/container_process.pyi,sha256=raXBD-ImyYUum4UtgEXCgTGuZoWQ7SriDkRuD91Nfuc,8296
35
35
  modal/dict.py,sha256=XkaxuojMVtcc4bZvCjJcd6DedU5xxfF8H4w-mDzFPCo,21580
36
36
  modal/dict.pyi,sha256=deOiwuwZtwXqedC3h19SwoQIWc4mUnDTBM5XkONt48Y,31712
37
37
  modal/environments.py,sha256=xXYDfgzd20CuFdww_zQ53OB0qANQG-j_ls_fT7mGdoQ,6028
@@ -41,12 +41,12 @@ modal/file_io.py,sha256=OSKr77TujcXGJW1iikzYiHckLSmv07QBgBHcxxYEkoI,21456
41
41
  modal/file_io.pyi,sha256=xtO6Glf_BFwDE7QiQQo24QqcMf_Vv-iz7WojcGVlLBU,15932
42
42
  modal/file_pattern_matcher.py,sha256=A_Kdkej6q7YQyhM_2-BvpFmPqJ0oHb54B6yf9VqvPVE,8116
43
43
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
44
- modal/functions.pyi,sha256=Z6VuukLrjASAgf0kV9I6c09WvP_b2gCujX6f9j2bBaw,37988
44
+ modal/functions.pyi,sha256=CMwApS396tdElFrjnV6RuL2DTCz4C3jYzYoq1y_LPUQ,37988
45
45
  modal/gpu.py,sha256=Fe5ORvVPDIstSq1xjmM6OoNgLYFWvogP9r5BgmD3hYg,6769
46
46
  modal/image.py,sha256=HDkOnhIAN8g63a8LTN4J5SjC9ciReFQQJIxTS2z5KFM,107216
47
47
  modal/image.pyi,sha256=dMvMwAuvWkNN2BRYJFijkEy2m_xtEXgCKK0T7FVldsc,77514
48
- modal/io_streams.py,sha256=APkECBiP1-D7xeBdFLuWKYOZq6MZFNAhVwuiO1ymcNE,25835
49
- modal/io_streams.pyi,sha256=dnuWjJrkQsMla_twPTe05NnixsYtT2gjWYjVFnyff5I,15960
48
+ modal/io_streams.py,sha256=Kv-No6WcNBouwdoogwHafOsmPOKqxTpvVGLU0mM6xMc,29564
49
+ modal/io_streams.pyi,sha256=h7qtAbj8LsN-eJKAGjBhnMBegvWprc_0AmwVFi6rj2Y,18084
50
50
  modal/mount.py,sha256=G7_xhQMZqokgfsaFLMch0YR3fs-OUNqYUm3f4jHTSMQ,33161
51
51
  modal/mount.pyi,sha256=MD_zV2M7eCWxbOpQRjU60aHevN-bmbiywaCX82QoFlw,15380
52
52
  modal/network_file_system.py,sha256=ZdEIRgdcR-p_ILyw_AecEtPOhhrSWJeADYCtFnhtaHM,13509
@@ -67,7 +67,7 @@ modal/retries.py,sha256=IvNLDM0f_GLUDD5VgEDoN09C88yoxSrCquinAuxT1Sc,5205
67
67
  modal/runner.py,sha256=Ni54hwa42SEBxLPpqFwKMsUPYY8Dv-I-Kz3_jL1StCI,25220
68
68
  modal/runner.pyi,sha256=DV3Z7h0owgRyOu9W5KU5O3UbRftX99KGrZQId91fpsU,8671
69
69
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
70
- modal/sandbox.py,sha256=R1WOFDTmg_SiG7NIEbzQMeTYHwLvX9r3j9lpINhIufc,45970
70
+ modal/sandbox.py,sha256=TVENhuPVJXu3RiOKkqSKVWMnHTP7zMUO61StZ2-Ldb8,45991
71
71
  modal/sandbox.pyi,sha256=elVE1xEy_ZhD009oNPnCwlZi4tK-RUb1qAoxkVteG9E,50713
72
72
  modal/schedule.py,sha256=ng0g0AqNY5GQI9KhkXZQ5Wam5G42glbkqVQsNpBtbDE,3078
73
73
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
@@ -113,7 +113,7 @@ modal/_utils/package_utils.py,sha256=LcL2olGN4xaUzu2Tbv-C-Ft9Qp6bsLxEfETOAVd-mjU
113
113
  modal/_utils/pattern_utils.py,sha256=ZUffaECfe2iYBhH6cvCB-0-UWhmEBTZEl_TwG_So3ag,6714
114
114
  modal/_utils/rand_pb_testing.py,sha256=mmVPk1rZldHwHZx0DnHTuHQlRLAiiAYdxjwEJpxvT9c,3900
115
115
  modal/_utils/shell_utils.py,sha256=hWHzv730Br2Xyj6cGPiMZ-198Z3RZuOu3pDXhFSZ22c,2157
116
- modal/_utils/task_command_router_client.py,sha256=EABQz9A-WX_O2OnvPWepxQJ1Nu9U8wd8yfj3f6br1hc,22859
116
+ modal/_utils/task_command_router_client.py,sha256=c8WD8DHkQ91lcMb9M_8eBj-NFm6cxeZSv_hgJjRfpJo,23577
117
117
  modal/_utils/time_utils.py,sha256=43tpFVwT7ykOjlETIFLVt9auMsRZqYYRYBEKxGCrRSA,1212
118
118
  modal/_vendor/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
119
119
  modal/_vendor/a2wsgi_wsgi.py,sha256=Q1AsjpV_Q_vzQsz_cSqmP9jWzsGsB-ARFU6vpQYml8k,21878
@@ -131,9 +131,9 @@ modal/cli/__init__.py,sha256=6FRleWQxBDT19y7OayO4lBOzuL6Bs9r0rLINYYYbHwQ,769
131
131
  modal/cli/_download.py,sha256=_Q_CG1Qb4cjVSAwHGGPOPoNTsmK9gHvF-HNWCNdFjaY,4900
132
132
  modal/cli/_traceback.py,sha256=IKj9xtc6LjAxyhGJWolNIXEX3MhAIulnRqywZNOFmkU,7324
133
133
  modal/cli/app.py,sha256=rbuAG92my-1eZN0olk6p2eD4oBnyBliUsrCOUW-U-9k,7832
134
- modal/cli/cluster.py,sha256=MMdFXqyOOeNO7P2VIUN78xKJLvesLIy_28Dt56XEXhM,3307
134
+ modal/cli/cluster.py,sha256=BLcKDpwpDmlqE2UC4V0qNpJKiQ-ZXfI9g_SE7u6vnIU,3347
135
135
  modal/cli/config.py,sha256=lhp2Pq4RbTDhaZJ-ZJvhrMqJj8c-WjuRX6gjE3TrvXc,1691
136
- modal/cli/container.py,sha256=9Ti-TIZ6vjDSmn9mk9h6SRwyhkQjtwirBN18LjpLyvE,3719
136
+ modal/cli/container.py,sha256=IdJ07p7hwFGXz3oYB__NYilucWpVv-K1JL3QKduu7ok,3769
137
137
  modal/cli/dict.py,sha256=YAJtiv41YcCd5Fqam3hXCNTs4Y0yOgGR_i6RfQNSAFM,4572
138
138
  modal/cli/entry_point.py,sha256=F06p54rPOs1xAUeYW76RaimFOgLW_I17RCvNwfZRqPc,4747
139
139
  modal/cli/environment.py,sha256=LGBq8RVQjfBH3EWz8QgmYe19UO66JKSDNxOXMUjw7JM,4285
@@ -156,7 +156,7 @@ modal/experimental/__init__.py,sha256=9gkVuDmu3m4TlKoU3MzEtTOemUSs8EEOWba40s7Aa0
156
156
  modal/experimental/flash.py,sha256=C4sef08rARYFllsgtqukFmYL18SZW0_JpMS0BejDcUs,28552
157
157
  modal/experimental/flash.pyi,sha256=vV_OQhtdrPn8SW0XrBK-aLLHHIvxAzLzwFbWrke-m74,15463
158
158
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
159
- modal-1.2.1.dev12.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
159
+ modal-1.2.1.dev14.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
160
160
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
161
161
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
162
162
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -184,10 +184,10 @@ modal_proto/task_command_router_pb2.py,sha256=_pD2ZpU0bNzhwBdzmLoLyLtAtftI_Agxwn
184
184
  modal_proto/task_command_router_pb2.pyi,sha256=EyDgXPLr7alqjXYERV8w_MPuO404x0uCppmSkrfE9IE,14589
185
185
  modal_proto/task_command_router_pb2_grpc.py,sha256=uEQ0HdrCp8v-9bB5yIic9muA8spCShLHY6Bz9cCgOUE,10114
186
186
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=s3Yxsrawdj4nr8vqQqsAxyX6ilWaGbdECy425KKbLIA,3301
187
- modal_version/__init__.py,sha256=OYOaillNG-YG3dph7s36QFNeIzc-mCx6s7KPhjISWzY,121
187
+ modal_version/__init__.py,sha256=743KmmOlJb-M5EkswhYesVjee8eiLrK4PizfYr2lZCM,121
188
188
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
189
- modal-1.2.1.dev12.dist-info/METADATA,sha256=aGlYkorlrlPY8h1THL2Ece76Z2PspqkANMRmy_F8xQk,2484
190
- modal-1.2.1.dev12.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
191
- modal-1.2.1.dev12.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
192
- modal-1.2.1.dev12.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
193
- modal-1.2.1.dev12.dist-info/RECORD,,
189
+ modal-1.2.1.dev14.dist-info/METADATA,sha256=kp21p_YTNC2q_oQRDT9xdagRNkhSrnlIfh-uQayaTqo,2484
190
+ modal-1.2.1.dev14.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
191
+ modal-1.2.1.dev14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
192
+ modal-1.2.1.dev14.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
193
+ modal-1.2.1.dev14.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.1.dev12"
4
+ __version__ = "1.2.1.dev14"