modal 1.0.6.dev58__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 (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
@@ -7,17 +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
- from .exception import InteractiveTimeoutError, InvalidError
13
+ from .config import logger
14
+ from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
14
15
  from .io_streams import _StreamReader, _StreamWriter
15
16
  from .stream_type import StreamType
16
17
 
17
18
  T = TypeVar("T", str, bytes)
18
19
 
19
20
 
20
- class _ContainerProcess(Generic[T]):
21
+ class _ContainerProcessThroughServer(Generic[T]):
21
22
  _process_id: Optional[str] = None
22
23
  _stdout: _StreamReader[T]
23
24
  _stderr: _StreamReader[T]
@@ -30,6 +31,7 @@ class _ContainerProcess(Generic[T]):
30
31
  def __init__(
31
32
  self,
32
33
  process_id: str,
34
+ task_id: str,
33
35
  client: _Client,
34
36
  stdout: StreamType = StreamType.PIPE,
35
37
  stderr: StreamType = StreamType.PIPE,
@@ -51,6 +53,7 @@ class _ContainerProcess(Generic[T]):
51
53
  text=text,
52
54
  by_line=by_line,
53
55
  deadline=exec_deadline,
56
+ task_id=task_id,
54
57
  )
55
58
  self._stderr = _StreamReader[T](
56
59
  api_pb2.FILE_DESCRIPTOR_STDERR,
@@ -61,6 +64,7 @@ class _ContainerProcess(Generic[T]):
61
64
  text=text,
62
65
  by_line=by_line,
63
66
  deadline=exec_deadline,
67
+ task_id=task_id,
64
68
  )
65
69
  self._stdin = _StreamWriter(process_id, "container_process", self._client)
66
70
 
@@ -96,6 +100,7 @@ class _ContainerProcess(Generic[T]):
96
100
 
97
101
  Returns `None` if the process is still running, else returns the exit code.
98
102
  """
103
+ assert self._process_id
99
104
  if self._returncode is not None:
100
105
  return self._returncode
101
106
  if self._exec_deadline and time.monotonic() >= self._exec_deadline:
@@ -105,7 +110,7 @@ class _ContainerProcess(Generic[T]):
105
110
  return self._returncode
106
111
 
107
112
  req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
108
- resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(self._client.stub.ContainerExecWait, req)
113
+ resp = await self._client.stub.ContainerExecWait(req)
109
114
 
110
115
  if resp.completed:
111
116
  self._returncode = resp.exit_code
@@ -114,11 +119,10 @@ class _ContainerProcess(Generic[T]):
114
119
  return None
115
120
 
116
121
  async def _wait_for_completion(self) -> int:
122
+ assert self._process_id
117
123
  while True:
118
124
  req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
119
- resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
120
- self._client.stub.ContainerExecWait, req
121
- )
125
+ resp = await self._client.stub.ContainerExecWait(req)
122
126
  if resp.completed:
123
127
  return resp.exit_code
124
128
 
@@ -136,6 +140,7 @@ class _ContainerProcess(Generic[T]):
136
140
  self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
137
141
  except (asyncio.TimeoutError, TimeoutError):
138
142
  self._returncode = -1
143
+ logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
139
144
  return self._returncode
140
145
 
141
146
  async def attach(self):
@@ -144,20 +149,25 @@ class _ContainerProcess(Generic[T]):
144
149
  print("interactive exec is not currently supported on Windows.")
145
150
  return
146
151
 
147
- from rich.console import Console
152
+ from ._output import make_console
148
153
 
149
- console = Console()
154
+ console = make_console()
150
155
 
151
156
  connecting_status = console.status("Connecting...")
152
157
  connecting_status.start()
153
158
  on_connect = asyncio.Event()
154
159
 
155
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
156
169
  # Don't skip empty messages so we can detect when the process has booted.
157
- async for chunk in stream._get_logs(skip_empty_messages=False):
158
- if chunk is None:
159
- break
160
-
170
+ async for chunk in stream_impl._get_logs(skip_empty_messages=False):
161
171
  if not on_connect.is_set():
162
172
  connecting_status.stop()
163
173
  on_connect.set()
@@ -191,4 +201,272 @@ class _ContainerProcess(Generic[T]):
191
201
  raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
192
202
 
193
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.")
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
+
194
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,114 @@ 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
+ ...
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
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
120
185
 
186
+ class ContainerProcess(typing.Generic[T]):
187
+ """Represents a running process in a container."""
121
188
  def __init__(
122
189
  self,
123
190
  process_id: str,
191
+ task_id: str,
124
192
  client: modal.client.Client,
125
193
  stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
126
194
  stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
127
195
  exec_deadline: typing.Optional[float] = None,
128
196
  text: bool = True,
129
197
  by_line: bool = False,
198
+ command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
130
199
  ) -> None: ...
131
200
  def __repr__(self) -> str: ...
132
201
  @property
@@ -164,12 +233,6 @@ class ContainerProcess(typing.Generic[T]):
164
233
 
165
234
  poll: __poll_spec[typing_extensions.Self]
166
235
 
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
236
  class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
174
237
  def __call__(self, /) -> int:
175
238
  """Wait for the container process to finish running. Returns the exit code."""