modAL 1.4.3.dev17__py3-none-any.whl → 1.4.3.dev19__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.
@@ -717,7 +717,14 @@ def use_md5(url: str) -> bool:
717
717
  host = urlparse(url).netloc.split(":")[0]
718
718
  if host.endswith(".amazonaws.com") or host.endswith(".r2.cloudflarestorage.com"):
719
719
  return True
720
- elif host in ["127.0.0.1", "localhost", "172.20.0.1", "172.21.0.1"]:
720
+ if host == "localhost":
721
721
  return False
722
- else:
723
- raise Exception(f"Unknown S3 host: {host}")
722
+ try:
723
+ import ipaddress
724
+
725
+ addr = ipaddress.ip_address(host)
726
+ if addr.is_private or addr.is_loopback:
727
+ return False
728
+ except ValueError:
729
+ pass
730
+ raise Exception(f"Unknown S3 host: {host}")
@@ -16,7 +16,7 @@ from grpclib import GRPCError, Status
16
16
  from grpclib.exceptions import StreamTerminatedError
17
17
 
18
18
  from modal.config import logger
19
- from modal.exception import ConflictError, ExecTimeoutError
19
+ from modal.exception import ExecTimeoutError
20
20
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
21
21
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
22
22
 
@@ -196,21 +196,12 @@ class TaskCommandRouterClient:
196
196
  return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
197
197
 
198
198
  @classmethod
199
- async def try_init(
199
+ async def init(
200
200
  cls,
201
201
  server_client,
202
202
  task_id: str,
203
- ) -> Optional["TaskCommandRouterClient"]:
204
- """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
205
-
206
- Returns None if command router access is not enabled (FAILED_PRECONDITION).
207
- """
208
- try:
209
- resp = await fetch_command_router_access(server_client, task_id)
210
- except ConflictError:
211
- logger.debug(f"Command router access is not enabled for task {task_id}")
212
- return None
213
-
203
+ ) -> "TaskCommandRouterClient":
204
+ resp = await fetch_command_router_access(server_client, task_id)
214
205
  logger.debug(f"Using command router access for task {task_id}")
215
206
  return await cls._connect(server_client, task_id, resp.url, resp.jwt)
216
207
 
@@ -241,7 +232,7 @@ class TaskCommandRouterClient:
241
232
  stream_stdio_retry_delay_factor: float = 2,
242
233
  stream_stdio_max_retries: int = 10,
243
234
  ) -> None:
244
- """Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
235
+ """Callers should not use this directly. Use TaskCommandRouterClient.init() instead."""
245
236
  # Record the loop this instance is bound to so __del__ can safely schedule cleanup
246
237
  # even if finalization happens from a different thread (e.g. via synchronicity).
247
238
  self._loop = loop
modal/app.py CHANGED
@@ -778,7 +778,6 @@ class _App:
778
778
  deprecation_warning(
779
779
  (2025, 12, 16),
780
780
  "The `max_inputs` parameter is deprecated. Please set `single_use_containers=True` instead.",
781
- pending=True,
782
781
  )
783
782
  single_use_containers = max_inputs == 1
784
783
 
@@ -987,7 +986,6 @@ class _App:
987
986
  deprecation_warning(
988
987
  (2025, 12, 16),
989
988
  "The `max_inputs` parameter is deprecated. Please set `single_use_containers=True` instead.",
990
- pending=True,
991
989
  )
992
990
  single_use_containers = max_inputs == 1
993
991
 
modal/cli/cluster.py CHANGED
@@ -16,7 +16,6 @@ from modal.cli.utils import display_table, env_option, is_tty
16
16
  from modal.client import _Client
17
17
  from modal.config import config
18
18
  from modal.container_process import _ContainerProcess
19
- from modal.exception import InvalidError
20
19
  from modal.output import OutputManager
21
20
  from modal.stream_type import StreamType
22
21
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
@@ -79,9 +78,7 @@ async def shell(cluster_id: str, rank: int = 0):
79
78
 
80
79
  pty = is_tty()
81
80
 
82
- command_router_client = await TaskCommandRouterClient.try_init(client, task_id)
83
- if command_router_client is None:
84
- raise InvalidError(f"Command router access is not available for container {task_id}")
81
+ command_router_client = await TaskCommandRouterClient.init(client, task_id)
85
82
 
86
83
  process_id = str(uuid.uuid4())
87
84
 
modal/cli/container.py CHANGED
@@ -278,9 +278,7 @@ async def _exec_impl(
278
278
 
279
279
  client = await _Client.from_env()
280
280
 
281
- command_router_client = await TaskCommandRouterClient.try_init(client, container_id)
282
- if command_router_client is None:
283
- raise InvalidError(f"Command router access is not available for container {container_id}")
281
+ command_router_client = await TaskCommandRouterClient.init(client, container_id)
284
282
 
285
283
  process_id = str(uuid.uuid4())
286
284
 
modal/client.pyi CHANGED
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.3.dev17",
38
+ version: str = "1.4.3.dev19",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -175,7 +175,7 @@ class Client:
175
175
  server_url: str,
176
176
  client_type: int,
177
177
  credentials: typing.Optional[tuple[str, str]],
178
- version: str = "1.4.3.dev17",
178
+ version: str = "1.4.3.dev19",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,7 +1,6 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
3
  import platform
4
- import time
5
4
  from typing import Generic, Optional, TypeVar
6
5
 
7
6
  from modal_proto import api_pb2
@@ -12,194 +11,17 @@ from ._utils.task_command_router_client import TaskCommandRouterClient
12
11
  from .client import _Client
13
12
  from .config import logger
14
13
  from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
15
- from .io_streams import _StreamReader, _StreamWriter
14
+ from .io_streams import (
15
+ _StreamReader,
16
+ _StreamReaderThroughCommandRouterParams,
17
+ _StreamWriter,
18
+ _StreamWriterThroughCommandRouterParams,
19
+ )
16
20
  from .stream_type import StreamType
17
21
 
18
22
  T = TypeVar("T", str, bytes)
19
23
 
20
24
 
21
- class _ContainerProcessThroughServer(Generic[T]):
22
- _process_id: Optional[str] = None
23
- _stdout: _StreamReader[T]
24
- _stderr: _StreamReader[T]
25
- _stdin: _StreamWriter
26
- _exec_deadline: Optional[float] = None
27
- _text: bool
28
- _by_line: bool
29
- _returncode: Optional[int] = None
30
-
31
- def __init__(
32
- self,
33
- process_id: str,
34
- task_id: str,
35
- client: _Client,
36
- stdout: StreamType = StreamType.PIPE,
37
- stderr: StreamType = StreamType.PIPE,
38
- exec_deadline: Optional[float] = None,
39
- text: bool = True,
40
- by_line: bool = False,
41
- ) -> None:
42
- self._process_id = process_id
43
- self._client = client
44
- self._exec_deadline = exec_deadline
45
- self._text = text
46
- self._by_line = by_line
47
- self._stdout = _StreamReader[T](
48
- api_pb2.FILE_DESCRIPTOR_STDOUT,
49
- process_id,
50
- "container_process",
51
- self._client,
52
- stream_type=stdout,
53
- text=text,
54
- by_line=by_line,
55
- deadline=exec_deadline,
56
- task_id=task_id,
57
- )
58
- self._stderr = _StreamReader[T](
59
- api_pb2.FILE_DESCRIPTOR_STDERR,
60
- process_id,
61
- "container_process",
62
- self._client,
63
- stream_type=stderr,
64
- text=text,
65
- by_line=by_line,
66
- deadline=exec_deadline,
67
- task_id=task_id,
68
- )
69
- self._stdin = _StreamWriter(process_id, "container_process", self._client)
70
-
71
- def __repr__(self) -> str:
72
- return f"ContainerProcess(process_id={self._process_id!r})"
73
-
74
- @property
75
- def stdout(self) -> _StreamReader[T]:
76
- """StreamReader for the container process's stdout stream."""
77
- return self._stdout
78
-
79
- @property
80
- def stderr(self) -> _StreamReader[T]:
81
- """StreamReader for the container process's stderr stream."""
82
- return self._stderr
83
-
84
- @property
85
- def stdin(self) -> _StreamWriter:
86
- """StreamWriter for the container process's stdin stream."""
87
- return self._stdin
88
-
89
- @property
90
- def returncode(self) -> int:
91
- if self._returncode is None:
92
- raise InvalidError(
93
- "You must call wait() before accessing the returncode. "
94
- "To poll for the status of a running process, use poll() instead."
95
- )
96
- return self._returncode
97
-
98
- async def poll(self) -> Optional[int]:
99
- """Check if the container process has finished running.
100
-
101
- Returns `None` if the process is still running, else returns the exit code.
102
- """
103
- assert self._process_id
104
- if self._returncode is not None:
105
- return self._returncode
106
- if self._exec_deadline and time.monotonic() >= self._exec_deadline:
107
- # TODO(matt): In the future, it would be nice to raise a ContainerExecTimeoutError to make it
108
- # clear to the user that their sandbox terminated due to a timeout
109
- self._returncode = -1
110
- return self._returncode
111
-
112
- req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
113
- resp = await self._client.stub.ContainerExecWait(req)
114
-
115
- if resp.completed:
116
- self._returncode = resp.exit_code
117
- return self._returncode
118
-
119
- return None
120
-
121
- async def _wait_for_completion(self) -> int:
122
- assert self._process_id
123
- while True:
124
- req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
125
- resp = await self._client.stub.ContainerExecWait(req)
126
- if resp.completed:
127
- return resp.exit_code
128
-
129
- async def wait(self) -> int:
130
- """Wait for the container process to finish running. Returns the exit code."""
131
- if self._returncode is not None:
132
- return self._returncode
133
-
134
- try:
135
- timeout = None
136
- if self._exec_deadline:
137
- timeout = self._exec_deadline - time.monotonic()
138
- if timeout <= 0:
139
- raise TimeoutError()
140
- self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
141
- except (asyncio.TimeoutError, TimeoutError):
142
- self._returncode = -1
143
- logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
144
- return self._returncode
145
-
146
- async def attach(self):
147
- """mdmd:hidden"""
148
- if platform.system() == "Windows":
149
- print("interactive exec is not currently supported on Windows.") # noqa: T201
150
- return
151
-
152
- from .output import OutputManager
153
-
154
- output = OutputManager.get()
155
- connecting_status = output.status("Connecting...")
156
- connecting_status.start()
157
- on_connect = asyncio.Event()
158
-
159
- async def _write_to_fd_loop(stream: _StreamReader):
160
- # This is required to make modal shell to an existing task work,
161
- # since that uses ContainerExec RPCs directly, but this is hacky.
162
- #
163
- # TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
164
- from .io_streams import _StreamReaderThroughServer
165
-
166
- assert isinstance(stream._impl, _StreamReaderThroughServer)
167
- stream_impl = stream._impl
168
- # Don't skip empty messages so we can detect when the process has booted.
169
- async for chunk in stream_impl._get_logs(skip_empty_messages=False):
170
- if not on_connect.is_set():
171
- connecting_status.stop()
172
- on_connect.set()
173
-
174
- await write_to_fd(stream.file_descriptor, chunk)
175
-
176
- async def _handle_input(data: bytes, message_index: int):
177
- self.stdin.write(data)
178
- await self.stdin.drain()
179
-
180
- async with TaskContext() as tc:
181
- stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
182
- stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
183
-
184
- try:
185
- # time out if we can't connect to the server fast enough
186
- await asyncio.wait_for(on_connect.wait(), timeout=60)
187
-
188
- async with stream_from_stdin(_handle_input, use_raw_terminal=True):
189
- await stdout_task
190
- await stderr_task
191
-
192
- # TODO: this doesn't work right now.
193
- # if exit_status != 0:
194
- # raise ExecutionError(f"Process exited with status code {exit_status}")
195
-
196
- except (asyncio.TimeoutError, TimeoutError):
197
- connecting_status.stop()
198
- stdout_task.cancel()
199
- stderr_task.cancel()
200
- raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
201
-
202
-
203
25
  async def _iter_stream_as_bytes(stream: _StreamReader[T]):
204
26
  """Yield raw bytes from a StreamReader regardless of text mode/backend."""
205
27
  async for part in stream:
@@ -209,19 +31,19 @@ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
209
31
  yield part
210
32
 
211
33
 
212
- class _ContainerProcessThroughCommandRouter(Generic[T]):
213
- """
214
- Container process implementation that works via direct communication with
34
+ class _ContainerProcess(Generic[T]):
35
+ """Represents a running process in a container.
36
+
37
+ Container processes communicate via direct communication with
215
38
  the Modal worker where the container is running.
216
39
  """
217
40
 
218
41
  def __init__(
219
42
  self,
220
43
  process_id: str,
44
+ task_id: str,
221
45
  client: _Client,
222
46
  command_router_client: TaskCommandRouterClient,
223
- task_id: str,
224
- *,
225
47
  stdout: StreamType = StreamType.PIPE,
226
48
  stderr: StreamType = StreamType.PIPE,
227
49
  exec_deadline: Optional[float] = None,
@@ -236,35 +58,35 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
236
58
  self._by_line = by_line
237
59
  self._task_id = task_id
238
60
  self._stdout = _StreamReader[T](
239
- api_pb2.FILE_DESCRIPTOR_STDOUT,
240
- process_id,
241
- "container_process",
242
- self._client,
61
+ _StreamReaderThroughCommandRouterParams(
62
+ file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
63
+ task_id=self._task_id,
64
+ object_id=process_id,
65
+ command_router_client=self._command_router_client,
66
+ deadline=exec_deadline,
67
+ ),
243
68
  stream_type=stdout,
244
69
  text=text,
245
70
  by_line=by_line,
246
- deadline=exec_deadline,
247
- command_router_client=self._command_router_client,
248
- task_id=self._task_id,
249
71
  )
250
72
  self._stderr = _StreamReader[T](
251
- api_pb2.FILE_DESCRIPTOR_STDERR,
252
- process_id,
253
- "container_process",
254
- self._client,
73
+ _StreamReaderThroughCommandRouterParams(
74
+ file_descriptor=api_pb2.FILE_DESCRIPTOR_STDERR,
75
+ task_id=self._task_id,
76
+ object_id=process_id,
77
+ command_router_client=self._command_router_client,
78
+ deadline=exec_deadline,
79
+ ),
255
80
  stream_type=stderr,
256
81
  text=text,
257
82
  by_line=by_line,
258
- deadline=exec_deadline,
259
- command_router_client=self._command_router_client,
260
- task_id=self._task_id,
261
83
  )
262
84
  self._stdin = _StreamWriter(
263
- process_id,
264
- "container_process",
265
- self._client,
266
- command_router_client=self._command_router_client,
267
- task_id=self._task_id,
85
+ _StreamWriterThroughCommandRouterParams(
86
+ task_id=self._task_id,
87
+ object_id=process_id,
88
+ command_router_client=self._command_router_client,
89
+ )
268
90
  )
269
91
  self._returncode = None
270
92
 
@@ -273,14 +95,17 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
273
95
 
274
96
  @property
275
97
  def stdout(self) -> _StreamReader[T]:
98
+ """StreamReader for the container process's stdout stream."""
276
99
  return self._stdout
277
100
 
278
101
  @property
279
102
  def stderr(self) -> _StreamReader[T]:
103
+ """StreamReader for the container process's stderr stream."""
280
104
  return self._stderr
281
105
 
282
106
  @property
283
107
  def stdin(self) -> _StreamWriter:
108
+ """StreamWriter for the container process's stdin stream."""
284
109
  return self._stdin
285
110
 
286
111
  @property
@@ -293,6 +118,10 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
293
118
  return self._returncode
294
119
 
295
120
  async def poll(self) -> Optional[int]:
121
+ """Check if the container process has finished running.
122
+
123
+ Returns `None` if the process is still running, else returns the exit code.
124
+ """
296
125
  if self._returncode is not None:
297
126
  return self._returncode
298
127
  try:
@@ -322,6 +151,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
322
151
  raise
323
152
 
324
153
  async def wait(self) -> int:
154
+ """Wait for the container process to finish running. Returns the exit code."""
325
155
  if self._returncode is not None:
326
156
  return self._returncode
327
157
 
@@ -345,6 +175,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
345
175
  return self._returncode
346
176
 
347
177
  async def attach(self):
178
+ """mdmd:hidden"""
348
179
  if platform.system() == "Windows":
349
180
  print("interactive exec is not currently supported on Windows.") # noqa: T201
350
181
  return
@@ -390,81 +221,4 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
390
221
  raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
391
222
 
392
223
 
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
-
470
224
  ContainerProcess = synchronize_api(_ContainerProcess)