modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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 (143) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +171 -138
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +30 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/name_utils.py +2 -3
  31. modal/_utils/package_utils.py +0 -1
  32. modal/_utils/rand_pb_testing.py +8 -1
  33. modal/_utils/task_command_router_client.py +524 -0
  34. modal/_vendor/cloudpickle.py +144 -48
  35. modal/app.py +285 -105
  36. modal/app.pyi +216 -53
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +6 -3
  39. modal/builder/PREVIEW.txt +2 -1
  40. modal/builder/base-images.json +4 -2
  41. modal/cli/_download.py +19 -3
  42. modal/cli/cluster.py +4 -2
  43. modal/cli/config.py +3 -1
  44. modal/cli/container.py +5 -4
  45. modal/cli/dict.py +5 -2
  46. modal/cli/entry_point.py +26 -2
  47. modal/cli/environment.py +2 -16
  48. modal/cli/launch.py +1 -76
  49. modal/cli/network_file_system.py +5 -20
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/vscode.py +1 -1
  52. modal/cli/queues.py +5 -4
  53. modal/cli/run.py +24 -204
  54. modal/cli/secret.py +1 -2
  55. modal/cli/shell.py +375 -0
  56. modal/cli/utils.py +1 -13
  57. modal/cli/volume.py +11 -17
  58. modal/client.py +16 -125
  59. modal/client.pyi +94 -144
  60. modal/cloud_bucket_mount.py +3 -1
  61. modal/cloud_bucket_mount.pyi +4 -0
  62. modal/cls.py +101 -64
  63. modal/cls.pyi +9 -8
  64. modal/config.py +21 -1
  65. modal/container_process.py +288 -12
  66. modal/container_process.pyi +99 -38
  67. modal/dict.py +72 -33
  68. modal/dict.pyi +88 -57
  69. modal/environments.py +16 -8
  70. modal/environments.pyi +6 -2
  71. modal/exception.py +154 -16
  72. modal/experimental/__init__.py +24 -53
  73. modal/experimental/flash.py +161 -74
  74. modal/experimental/flash.pyi +97 -49
  75. modal/file_io.py +50 -92
  76. modal/file_io.pyi +117 -89
  77. modal/functions.pyi +70 -87
  78. modal/image.py +82 -47
  79. modal/image.pyi +51 -30
  80. modal/io_streams.py +500 -149
  81. modal/io_streams.pyi +279 -189
  82. modal/mount.py +60 -46
  83. modal/mount.pyi +41 -17
  84. modal/network_file_system.py +19 -11
  85. modal/network_file_system.pyi +72 -39
  86. modal/object.pyi +114 -22
  87. modal/parallel_map.py +42 -44
  88. modal/parallel_map.pyi +9 -17
  89. modal/partial_function.pyi +4 -2
  90. modal/proxy.py +14 -6
  91. modal/proxy.pyi +10 -2
  92. modal/queue.py +45 -38
  93. modal/queue.pyi +88 -52
  94. modal/runner.py +96 -96
  95. modal/runner.pyi +44 -27
  96. modal/sandbox.py +225 -107
  97. modal/sandbox.pyi +226 -60
  98. modal/secret.py +58 -56
  99. modal/secret.pyi +28 -13
  100. modal/serving.py +7 -11
  101. modal/serving.pyi +7 -8
  102. modal/snapshot.py +29 -15
  103. modal/snapshot.pyi +18 -10
  104. modal/token_flow.py +1 -1
  105. modal/token_flow.pyi +4 -6
  106. modal/volume.py +102 -55
  107. modal/volume.pyi +125 -66
  108. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  109. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  110. modal_proto/api.proto +141 -70
  111. modal_proto/api_grpc.py +42 -26
  112. modal_proto/api_pb2.py +1123 -1103
  113. modal_proto/api_pb2.pyi +331 -83
  114. modal_proto/api_pb2_grpc.py +80 -48
  115. modal_proto/api_pb2_grpc.pyi +26 -18
  116. modal_proto/modal_api_grpc.py +175 -174
  117. modal_proto/task_command_router.proto +164 -0
  118. modal_proto/task_command_router_grpc.py +138 -0
  119. modal_proto/task_command_router_pb2.py +180 -0
  120. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
  121. modal_proto/task_command_router_pb2_grpc.py +272 -0
  122. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  123. modal_version/__init__.py +1 -1
  124. modal_version/__main__.py +1 -1
  125. modal/cli/programs/launch_instance_ssh.py +0 -94
  126. modal/cli/programs/run_marimo.py +0 -95
  127. modal-1.1.5.dev66.dist-info/RECORD +0 -191
  128. modal_proto/modal_options_grpc.py +0 -3
  129. modal_proto/options.proto +0 -19
  130. modal_proto/options_grpc.py +0 -3
  131. modal_proto/options_pb2.py +0 -35
  132. modal_proto/options_pb2.pyi +0 -20
  133. modal_proto/options_pb2_grpc.py +0 -4
  134. modal_proto/options_pb2_grpc.pyi +0 -7
  135. modal_proto/sandbox_router.proto +0 -125
  136. modal_proto/sandbox_router_grpc.py +0 -89
  137. modal_proto/sandbox_router_pb2.py +0 -128
  138. modal_proto/sandbox_router_pb2_grpc.py +0 -169
  139. modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
  140. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  141. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  142. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  143. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
@@ -7,18 +7,18 @@ from typing import Generic, Optional, TypeVar
7
7
  from modal_proto import api_pb2
8
8
 
9
9
  from ._utils.async_utils import TaskContext, synchronize_api
10
- from ._utils.grpc_utils import retry_transient_errors
11
10
  from ._utils.shell_utils import stream_from_stdin, write_to_fd
11
+ from ._utils.task_command_router_client import TaskCommandRouterClient
12
12
  from .client import _Client
13
13
  from .config import logger
14
- from .exception import InteractiveTimeoutError, InvalidError
14
+ from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
15
15
  from .io_streams import _StreamReader, _StreamWriter
16
16
  from .stream_type import StreamType
17
17
 
18
18
  T = TypeVar("T", str, bytes)
19
19
 
20
20
 
21
- class _ContainerProcess(Generic[T]):
21
+ class _ContainerProcessThroughServer(Generic[T]):
22
22
  _process_id: Optional[str] = None
23
23
  _stdout: _StreamReader[T]
24
24
  _stderr: _StreamReader[T]
@@ -31,6 +31,7 @@ class _ContainerProcess(Generic[T]):
31
31
  def __init__(
32
32
  self,
33
33
  process_id: str,
34
+ task_id: str,
34
35
  client: _Client,
35
36
  stdout: StreamType = StreamType.PIPE,
36
37
  stderr: StreamType = StreamType.PIPE,
@@ -52,6 +53,7 @@ class _ContainerProcess(Generic[T]):
52
53
  text=text,
53
54
  by_line=by_line,
54
55
  deadline=exec_deadline,
56
+ task_id=task_id,
55
57
  )
56
58
  self._stderr = _StreamReader[T](
57
59
  api_pb2.FILE_DESCRIPTOR_STDERR,
@@ -62,6 +64,7 @@ class _ContainerProcess(Generic[T]):
62
64
  text=text,
63
65
  by_line=by_line,
64
66
  deadline=exec_deadline,
67
+ task_id=task_id,
65
68
  )
66
69
  self._stdin = _StreamWriter(process_id, "container_process", self._client)
67
70
 
@@ -97,6 +100,7 @@ class _ContainerProcess(Generic[T]):
97
100
 
98
101
  Returns `None` if the process is still running, else returns the exit code.
99
102
  """
103
+ assert self._process_id
100
104
  if self._returncode is not None:
101
105
  return self._returncode
102
106
  if self._exec_deadline and time.monotonic() >= self._exec_deadline:
@@ -106,7 +110,7 @@ class _ContainerProcess(Generic[T]):
106
110
  return self._returncode
107
111
 
108
112
  req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
109
- resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(self._client.stub.ContainerExecWait, req)
113
+ resp = await self._client.stub.ContainerExecWait(req)
110
114
 
111
115
  if resp.completed:
112
116
  self._returncode = resp.exit_code
@@ -115,11 +119,10 @@ class _ContainerProcess(Generic[T]):
115
119
  return None
116
120
 
117
121
  async def _wait_for_completion(self) -> int:
122
+ assert self._process_id
118
123
  while True:
119
124
  req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
120
- resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
121
- self._client.stub.ContainerExecWait, req
122
- )
125
+ resp = await self._client.stub.ContainerExecWait(req)
123
126
  if resp.completed:
124
127
  return resp.exit_code
125
128
 
@@ -143,7 +146,7 @@ class _ContainerProcess(Generic[T]):
143
146
  async def attach(self):
144
147
  """mdmd:hidden"""
145
148
  if platform.system() == "Windows":
146
- print("interactive exec is not currently supported on Windows.")
149
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
147
150
  return
148
151
 
149
152
  from ._output import make_console
@@ -155,11 +158,16 @@ class _ContainerProcess(Generic[T]):
155
158
  on_connect = asyncio.Event()
156
159
 
157
160
  async def _write_to_fd_loop(stream: _StreamReader):
161
+ # This is required to make modal shell to an existing task work,
162
+ # since that uses ContainerExec RPCs directly, but this is hacky.
163
+ #
164
+ # TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
165
+ from .io_streams import _StreamReaderThroughServer
166
+
167
+ assert isinstance(stream._impl, _StreamReaderThroughServer)
168
+ stream_impl = stream._impl
158
169
  # Don't skip empty messages so we can detect when the process has booted.
159
- async for chunk in stream._get_logs(skip_empty_messages=False):
160
- if chunk is None:
161
- break
162
-
170
+ async for chunk in stream_impl._get_logs(skip_empty_messages=False):
163
171
  if not on_connect.is_set():
164
172
  connecting_status.stop()
165
173
  on_connect.set()
@@ -193,4 +201,272 @@ class _ContainerProcess(Generic[T]):
193
201
  raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
194
202
 
195
203
 
204
+ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
205
+ """Yield raw bytes from a StreamReader regardless of text mode/backend."""
206
+ async for part in stream:
207
+ if isinstance(part, str):
208
+ yield part.encode("utf-8")
209
+ else:
210
+ yield part
211
+
212
+
213
+ class _ContainerProcessThroughCommandRouter(Generic[T]):
214
+ """
215
+ Container process implementation that works via direct communication with
216
+ the Modal worker where the container is running.
217
+ """
218
+
219
+ def __init__(
220
+ self,
221
+ process_id: str,
222
+ client: _Client,
223
+ command_router_client: TaskCommandRouterClient,
224
+ task_id: str,
225
+ *,
226
+ stdout: StreamType = StreamType.PIPE,
227
+ stderr: StreamType = StreamType.PIPE,
228
+ exec_deadline: Optional[float] = None,
229
+ text: bool = True,
230
+ by_line: bool = False,
231
+ ) -> None:
232
+ self._client = client
233
+ self._command_router_client = command_router_client
234
+ self._process_id = process_id
235
+ self._exec_deadline = exec_deadline
236
+ self._text = text
237
+ self._by_line = by_line
238
+ self._task_id = task_id
239
+ self._stdout = _StreamReader[T](
240
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
241
+ process_id,
242
+ "container_process",
243
+ self._client,
244
+ stream_type=stdout,
245
+ text=text,
246
+ by_line=by_line,
247
+ deadline=exec_deadline,
248
+ command_router_client=self._command_router_client,
249
+ task_id=self._task_id,
250
+ )
251
+ self._stderr = _StreamReader[T](
252
+ api_pb2.FILE_DESCRIPTOR_STDERR,
253
+ process_id,
254
+ "container_process",
255
+ self._client,
256
+ stream_type=stderr,
257
+ text=text,
258
+ by_line=by_line,
259
+ deadline=exec_deadline,
260
+ command_router_client=self._command_router_client,
261
+ task_id=self._task_id,
262
+ )
263
+ self._stdin = _StreamWriter(
264
+ process_id,
265
+ "container_process",
266
+ self._client,
267
+ command_router_client=self._command_router_client,
268
+ task_id=self._task_id,
269
+ )
270
+ self._returncode = None
271
+
272
+ def __repr__(self) -> str:
273
+ return f"ContainerProcess(process_id={self._process_id!r})"
274
+
275
+ @property
276
+ def stdout(self) -> _StreamReader[T]:
277
+ return self._stdout
278
+
279
+ @property
280
+ def stderr(self) -> _StreamReader[T]:
281
+ return self._stderr
282
+
283
+ @property
284
+ def stdin(self) -> _StreamWriter:
285
+ return self._stdin
286
+
287
+ @property
288
+ def returncode(self) -> int:
289
+ if self._returncode is None:
290
+ raise InvalidError(
291
+ "You must call wait() before accessing the returncode. "
292
+ "To poll for the status of a running process, use poll() instead."
293
+ )
294
+ return self._returncode
295
+
296
+ async def poll(self) -> Optional[int]:
297
+ if self._returncode is not None:
298
+ return self._returncode
299
+ try:
300
+ resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
301
+ which = resp.WhichOneof("exit_status")
302
+ if which is None:
303
+ return None
304
+
305
+ if which == "code":
306
+ self._returncode = int(resp.code)
307
+ return self._returncode
308
+ elif which == "signal":
309
+ self._returncode = 128 + int(resp.signal)
310
+ return self._returncode
311
+ else:
312
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
313
+ raise InvalidError("Unexpected exit status")
314
+ except ExecTimeoutError:
315
+ logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
316
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
317
+ # should probably raise an ExecTimeoutError instead.
318
+ self._returncode = -1
319
+ return self._returncode
320
+ except Exception as e:
321
+ # Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
322
+ logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
323
+ raise
324
+
325
+ async def wait(self) -> int:
326
+ if self._returncode is not None:
327
+ return self._returncode
328
+
329
+ try:
330
+ resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
331
+ which = resp.WhichOneof("exit_status")
332
+ if which == "code":
333
+ self._returncode = int(resp.code)
334
+ elif which == "signal":
335
+ self._returncode = 128 + int(resp.signal)
336
+ else:
337
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
338
+ self._returncode = -1
339
+ raise InvalidError("Unexpected exit status")
340
+ except ExecTimeoutError:
341
+ logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
342
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
343
+ # should be a ExecTimeoutError.
344
+ self._returncode = -1
345
+
346
+ return self._returncode
347
+
348
+ async def attach(self):
349
+ if platform.system() == "Windows":
350
+ print("interactive exec is not currently supported on Windows.") # noqa: T201
351
+ return
352
+
353
+ from ._output import make_console
354
+
355
+ console = make_console()
356
+
357
+ connecting_status = console.status("Connecting...")
358
+ connecting_status.start()
359
+ on_connect = asyncio.Event()
360
+
361
+ async def _write_to_fd_loop(stream: _StreamReader[T]):
362
+ async for chunk in _iter_stream_as_bytes(stream):
363
+ if chunk is None:
364
+ break
365
+
366
+ if not on_connect.is_set():
367
+ connecting_status.stop()
368
+ on_connect.set()
369
+
370
+ await write_to_fd(stream.file_descriptor, chunk)
371
+
372
+ async def _handle_input(data: bytes, message_index: int):
373
+ self.stdin.write(data)
374
+ await self.stdin.drain()
375
+
376
+ async with TaskContext() as tc:
377
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
378
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
379
+
380
+ try:
381
+ # Time out if we can't connect fast enough.
382
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
383
+
384
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
385
+ await stdout_task
386
+ await stderr_task
387
+
388
+ except (asyncio.TimeoutError, TimeoutError):
389
+ connecting_status.stop()
390
+ stdout_task.cancel()
391
+ stderr_task.cancel()
392
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
393
+
394
+
395
+ class _ContainerProcess(Generic[T]):
396
+ """Represents a running process in a container."""
397
+
398
+ def __init__(
399
+ self,
400
+ process_id: str,
401
+ task_id: str,
402
+ client: _Client,
403
+ stdout: StreamType = StreamType.PIPE,
404
+ stderr: StreamType = StreamType.PIPE,
405
+ exec_deadline: Optional[float] = None,
406
+ text: bool = True,
407
+ by_line: bool = False,
408
+ command_router_client: Optional[TaskCommandRouterClient] = None,
409
+ ) -> None:
410
+ if command_router_client is None:
411
+ self._impl = _ContainerProcessThroughServer(
412
+ process_id,
413
+ task_id,
414
+ client,
415
+ stdout=stdout,
416
+ stderr=stderr,
417
+ exec_deadline=exec_deadline,
418
+ text=text,
419
+ by_line=by_line,
420
+ )
421
+ else:
422
+ self._impl = _ContainerProcessThroughCommandRouter(
423
+ process_id,
424
+ client,
425
+ command_router_client,
426
+ task_id,
427
+ stdout=stdout,
428
+ stderr=stderr,
429
+ exec_deadline=exec_deadline,
430
+ text=text,
431
+ by_line=by_line,
432
+ )
433
+
434
+ def __repr__(self) -> str:
435
+ return self._impl.__repr__()
436
+
437
+ @property
438
+ def stdout(self) -> _StreamReader[T]:
439
+ """StreamReader for the container process's stdout stream."""
440
+ return self._impl.stdout
441
+
442
+ @property
443
+ def stderr(self) -> _StreamReader[T]:
444
+ """StreamReader for the container process's stderr stream."""
445
+ return self._impl.stderr
446
+
447
+ @property
448
+ def stdin(self) -> _StreamWriter:
449
+ """StreamWriter for the container process's stdin stream."""
450
+ return self._impl.stdin
451
+
452
+ @property
453
+ def returncode(self) -> int:
454
+ return self._impl.returncode
455
+
456
+ async def poll(self) -> Optional[int]:
457
+ """Check if the container process has finished running.
458
+
459
+ Returns `None` if the process is still running, else returns the exit code.
460
+ """
461
+ return await self._impl.poll()
462
+
463
+ async def wait(self) -> int:
464
+ """Wait for the container process to finish running. Returns the exit code."""
465
+ return await self._impl.wait()
466
+
467
+ async def attach(self):
468
+ """mdmd:hidden"""
469
+ await self._impl.attach()
470
+
471
+
196
472
  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,112 @@ 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
+ def __repr__(self) -> str:
116
+ """Return repr(self)."""
117
+ ...
97
118
 
98
- class Mapping(Generic[KT, VT]):
99
- def __getitem__(self, key: KT) -> VT:
100
- ...
101
- # Etc.
119
+ @property
120
+ def stdout(self) -> modal.io_streams._StreamReader[T]: ...
121
+ @property
122
+ def stderr(self) -> modal.io_streams._StreamReader[T]: ...
123
+ @property
124
+ def stdin(self) -> modal.io_streams._StreamWriter: ...
125
+ @property
126
+ def returncode(self) -> int: ...
127
+ async def poll(self) -> typing.Optional[int]: ...
128
+ async def wait(self) -> int: ...
129
+ async def attach(self): ...
102
130
 
103
- This class can then be used as follows::
131
+ class _ContainerProcess(typing.Generic[T]):
132
+ """Represents a running process in a container."""
133
+ def __init__(
134
+ self,
135
+ process_id: str,
136
+ task_id: str,
137
+ client: modal.client._Client,
138
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
139
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
140
+ exec_deadline: typing.Optional[float] = None,
141
+ text: bool = True,
142
+ by_line: bool = False,
143
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
144
+ ) -> None:
145
+ """Initialize self. See help(type(self)) for accurate signature."""
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
+ def __repr__(self) -> str:
149
+ """Return repr(self)."""
150
+ ...
111
151
 
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]
152
+ @property
153
+ def stdout(self) -> modal.io_streams._StreamReader[T]:
154
+ """StreamReader for the container process's stdout stream."""
155
+ ...
156
+
157
+ @property
158
+ def stderr(self) -> modal.io_streams._StreamReader[T]:
159
+ """StreamReader for the container process's stderr stream."""
160
+ ...
161
+
162
+ @property
163
+ def stdin(self) -> modal.io_streams._StreamWriter:
164
+ """StreamWriter for the container process's stdin stream."""
165
+ ...
166
+
167
+ @property
168
+ def returncode(self) -> int: ...
169
+ async def poll(self) -> typing.Optional[int]:
170
+ """Check if the container process has finished running.
171
+
172
+ Returns `None` if the process is still running, else returns the exit code.
173
+ """
174
+ ...
120
175
 
176
+ async def wait(self) -> int:
177
+ """Wait for the container process to finish running. Returns the exit code."""
178
+ ...
179
+
180
+ async def attach(self):
181
+ """mdmd:hidden"""
182
+ ...
183
+
184
+ class ContainerProcess(typing.Generic[T]):
185
+ """Represents a running process in a container."""
121
186
  def __init__(
122
187
  self,
123
188
  process_id: str,
189
+ task_id: str,
124
190
  client: modal.client.Client,
125
191
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
126
192
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
127
193
  exec_deadline: typing.Optional[float] = None,
128
194
  text: bool = True,
129
195
  by_line: bool = False,
196
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
130
197
  ) -> None: ...
131
198
  def __repr__(self) -> str: ...
132
199
  @property
@@ -147,7 +214,7 @@ class ContainerProcess(typing.Generic[T]):
147
214
  @property
148
215
  def returncode(self) -> int: ...
149
216
 
150
- class __poll_spec(typing_extensions.Protocol[SUPERSELF]):
217
+ class __poll_spec(typing_extensions.Protocol):
151
218
  def __call__(self, /) -> typing.Optional[int]:
152
219
  """Check if the container process has finished running.
153
220
 
@@ -162,15 +229,9 @@ class ContainerProcess(typing.Generic[T]):
162
229
  """
163
230
  ...
164
231
 
165
- poll: __poll_spec[typing_extensions.Self]
166
-
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]
232
+ poll: __poll_spec
172
233
 
173
- class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
234
+ class __wait_spec(typing_extensions.Protocol):
174
235
  def __call__(self, /) -> int:
175
236
  """Wait for the container process to finish running. Returns the exit code."""
176
237
  ...
@@ -179,9 +240,9 @@ class ContainerProcess(typing.Generic[T]):
179
240
  """Wait for the container process to finish running. Returns the exit code."""
180
241
  ...
181
242
 
182
- wait: __wait_spec[typing_extensions.Self]
243
+ wait: __wait_spec
183
244
 
184
- class __attach_spec(typing_extensions.Protocol[SUPERSELF]):
245
+ class __attach_spec(typing_extensions.Protocol):
185
246
  def __call__(self, /):
186
247
  """mdmd:hidden"""
187
248
  ...
@@ -190,4 +251,4 @@ class ContainerProcess(typing.Generic[T]):
190
251
  """mdmd:hidden"""
191
252
  ...
192
253
 
193
- attach: __attach_spec[typing_extensions.Self]
254
+ attach: __attach_spec