modAL 1.3.3__tar.gz → 1.3.4__tar.gz
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.
- {modal-1.3.3 → modal-1.3.4}/PKG-INFO +1 -1
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/grpc_utils.py +17 -1
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/task_command_router_client.py +3 -1
- {modal-1.3.3 → modal-1.3.4}/modal/app.py +5 -0
- {modal-1.3.3 → modal-1.3.4}/modal/app.pyi +7 -0
- {modal-1.3.3 → modal-1.3.4}/modal/client.pyi +2 -2
- {modal-1.3.3 → modal-1.3.4}/modal/cloud_bucket_mount.py +3 -1
- {modal-1.3.3 → modal-1.3.4}/modal/cloud_bucket_mount.pyi +3 -1
- {modal-1.3.3 → modal-1.3.4}/modal/gpu.py +3 -0
- {modal-1.3.3 → modal-1.3.4}/modal/image.py +9 -0
- {modal-1.3.3 → modal-1.3.4}/modal/image.pyi +6 -0
- {modal-1.3.3 → modal-1.3.4}/modal/io_streams.py +4 -1
- {modal-1.3.3 → modal-1.3.4}/modal/network_file_system.py +1 -0
- {modal-1.3.3 → modal-1.3.4}/modal/network_file_system.pyi +3 -1
- {modal-1.3.3 → modal-1.3.4}/modal/sandbox.py +148 -18
- {modal-1.3.3 → modal-1.3.4}/modal/sandbox.pyi +203 -29
- {modal-1.3.3 → modal-1.3.4}/modal/volume.py +44 -0
- {modal-1.3.3 → modal-1.3.4}/modal/volume.pyi +52 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.3.3 → modal-1.3.4}/modal_docs/gen_reference_docs.py +1 -0
- {modal-1.3.3 → modal-1.3.4}/modal_docs/mdmd/mdmd.py +10 -2
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api.proto +10 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api_grpc.py +16 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api_pb2.py +155 -135
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api_pb2.pyi +33 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.3.3 → modal-1.3.4}/modal_version/__init__.py +1 -1
- {modal-1.3.3 → modal-1.3.4}/LICENSE +0 -0
- {modal-1.3.3 → modal-1.3.4}/README.md +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/__main__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_billing.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_clustered_functions.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_container_entrypoint.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_functions.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_grpc_client.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_ipython.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_load_context.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_location.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_object.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_output/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_output/manager.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_output/pty.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_output/rich.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_output/status.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_partial_function.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_resolver.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_resources.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_serialization.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_server.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_traceback.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_tunnel.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_tunnel.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_type_manager.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/async_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/blob_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/browser_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/deprecation.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/function_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/logger.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/mount_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/_watcher.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/billing.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/2025.06.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/README.md +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/builder/base-images.json +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/call_graph.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/_download.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/_traceback.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/app.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/billing.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/cluster.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/config.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/container.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/dashboard.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/dict.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/entry_point.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/environment.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/import_refs.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/launch.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/profile.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/queues.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/run.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/secret.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/shell.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/token.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/utils.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cli/volume.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/client.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cls.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/cls.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/config.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/container_process.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/container_process.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/dict.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/dict.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/environments.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/environments.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/exception.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/experimental/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/experimental/flash.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/experimental/ipython.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/file_io.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/file_io.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/functions.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/functions.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/io_streams.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/mount.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/mount.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/object.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/object.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/output.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/parallel_map.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/parallel_map.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/partial_function.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/partial_function.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/proxy.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/proxy.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/py.typed +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/queue.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/queue.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/retries.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/runner.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/runner.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/running_app.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/schedule.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/scheduler_placement.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/secret.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/secret.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/server.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/server.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/serving.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/serving.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/snapshot.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/snapshot.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/stream_type.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/token_flow.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal/token_flow.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/requires.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_docs/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/__init__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/py.typed +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router.proto +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.3 → modal-1.3.4}/modal_version/__main__.py +0 -0
- {modal-1.3.3 → modal-1.3.4}/pyproject.toml +0 -0
- {modal-1.3.3 → modal-1.3.4}/setup.cfg +0 -0
|
@@ -23,7 +23,7 @@ from grpclib.encoding.base import StatusDetailsCodecBase
|
|
|
23
23
|
from grpclib.exceptions import StreamTerminatedError
|
|
24
24
|
from grpclib.protocol import H2Protocol
|
|
25
25
|
|
|
26
|
-
from modal.exception import ConnectionError
|
|
26
|
+
from modal.exception import ClientClosed, ConnectionError
|
|
27
27
|
from modal_proto import api_pb2
|
|
28
28
|
from modal_version import __version__
|
|
29
29
|
|
|
@@ -65,6 +65,22 @@ class Subchannel:
|
|
|
65
65
|
return True
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
class PermanentCloseableChannel(grpclib.client.Channel):
|
|
69
|
+
def __init__(self, *args, closed_error_message: str, **kwargs):
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self.__closed = False
|
|
72
|
+
self.__closed_error_message = closed_error_message
|
|
73
|
+
|
|
74
|
+
async def __connect__(self):
|
|
75
|
+
if self.__closed:
|
|
76
|
+
raise ClientClosed(self.__closed_error_message)
|
|
77
|
+
return await super().__connect__()
|
|
78
|
+
|
|
79
|
+
def close(self):
|
|
80
|
+
self.__closed = True
|
|
81
|
+
return super().close()
|
|
82
|
+
|
|
83
|
+
|
|
68
84
|
RETRYABLE_GRPC_STATUS_CODES = [
|
|
69
85
|
Status.DEADLINE_EXCEEDED,
|
|
70
86
|
Status.UNAVAILABLE,
|
|
@@ -21,6 +21,7 @@ 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
|
|
|
23
23
|
from .._grpc_client import grpc_error_converter
|
|
24
|
+
from .._utils.grpc_utils import PermanentCloseableChannel
|
|
24
25
|
from .async_utils import aclosing
|
|
25
26
|
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES, connect_channel
|
|
26
27
|
|
|
@@ -157,7 +158,7 @@ class TaskCommandRouterClient:
|
|
|
157
158
|
ssl_context.check_hostname = False
|
|
158
159
|
ssl_context.verify_mode = ssl.CERT_NONE
|
|
159
160
|
|
|
160
|
-
channel =
|
|
161
|
+
channel = PermanentCloseableChannel(
|
|
161
162
|
host,
|
|
162
163
|
port,
|
|
163
164
|
ssl=ssl_context,
|
|
@@ -165,6 +166,7 @@ class TaskCommandRouterClient:
|
|
|
165
166
|
http2_connection_window_size=64 * 1024 * 1024, # 64 MiB
|
|
166
167
|
http2_stream_window_size=64 * 1024 * 1024, # 64 MiB
|
|
167
168
|
),
|
|
169
|
+
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
168
170
|
)
|
|
169
171
|
|
|
170
172
|
await connect_channel(channel)
|
|
@@ -324,6 +324,11 @@ class _App:
|
|
|
324
324
|
app._running_app = RunningApp(response.app_id, interactive=False)
|
|
325
325
|
return app
|
|
326
326
|
|
|
327
|
+
async def get_dashboard_url(self) -> str:
|
|
328
|
+
if self._app_id is None:
|
|
329
|
+
raise InvalidError("App is not running")
|
|
330
|
+
return f"https://modal.com/id/{self._app_id}"
|
|
331
|
+
|
|
327
332
|
def set_description(self, description: str):
|
|
328
333
|
"""mdmd:hidden
|
|
329
334
|
Set the description of the App before it starts running.
|
|
@@ -226,6 +226,7 @@ class _App:
|
|
|
226
226
|
"""
|
|
227
227
|
...
|
|
228
228
|
|
|
229
|
+
async def get_dashboard_url(self) -> str: ...
|
|
229
230
|
def set_description(self, description: str):
|
|
230
231
|
"""mdmd:hidden
|
|
231
232
|
Set the description of the App before it starts running.
|
|
@@ -833,6 +834,12 @@ class App:
|
|
|
833
834
|
|
|
834
835
|
lookup: typing.ClassVar[__lookup_spec]
|
|
835
836
|
|
|
837
|
+
class __get_dashboard_url_spec(typing_extensions.Protocol):
|
|
838
|
+
def __call__(self, /) -> str: ...
|
|
839
|
+
async def aio(self, /) -> str: ...
|
|
840
|
+
|
|
841
|
+
get_dashboard_url: __get_dashboard_url_spec
|
|
842
|
+
|
|
836
843
|
def set_description(self, description: str):
|
|
837
844
|
"""mdmd:hidden
|
|
838
845
|
Set the description of the App before it starts running.
|
|
@@ -31,7 +31,7 @@ class _Client:
|
|
|
31
31
|
client_type: int
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.
|
|
34
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.4"
|
|
35
35
|
):
|
|
36
36
|
"""mdmd:hidden
|
|
37
37
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -163,7 +163,7 @@ class Client:
|
|
|
163
163
|
client_type: int
|
|
164
164
|
|
|
165
165
|
def __init__(
|
|
166
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.
|
|
166
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.3.4"
|
|
167
167
|
):
|
|
168
168
|
"""mdmd:hidden
|
|
169
169
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -121,7 +121,9 @@ class _CloudBucketMount:
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def cloud_bucket_mounts_to_proto(mounts: Sequence[tuple[str, _CloudBucketMount]]) -> list[api_pb2.CloudBucketMount]:
|
|
124
|
-
"""
|
|
124
|
+
"""mdmd:hidden
|
|
125
|
+
Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
|
|
126
|
+
"""
|
|
125
127
|
cloud_bucket_mounts: list[api_pb2.CloudBucketMount] = []
|
|
126
128
|
|
|
127
129
|
for path, mount in mounts:
|
|
@@ -126,7 +126,9 @@ class _CloudBucketMount:
|
|
|
126
126
|
def cloud_bucket_mounts_to_proto(
|
|
127
127
|
mounts: typing.Sequence[tuple[str, _CloudBucketMount]],
|
|
128
128
|
) -> list[modal_proto.api_pb2.CloudBucketMount]:
|
|
129
|
-
"""
|
|
129
|
+
"""mdmd:hidden
|
|
130
|
+
Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
|
|
131
|
+
"""
|
|
130
132
|
...
|
|
131
133
|
|
|
132
134
|
class CloudBucketMount:
|
|
@@ -199,6 +199,9 @@ GPU_T = Union[None, str, _GPUConfig]
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def parse_gpu_config(value: GPU_T) -> api_pb2.GPUConfig:
|
|
202
|
+
"""mdmd:hidden
|
|
203
|
+
Parses a GPU config into a protobuf representation.
|
|
204
|
+
"""
|
|
202
205
|
if isinstance(value, _GPUConfig):
|
|
203
206
|
return value._to_proto()
|
|
204
207
|
elif isinstance(value, str):
|
|
@@ -418,6 +418,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
418
418
|
] # added as mounts on any container referencing the Image, see `def _mount_layers`
|
|
419
419
|
_added_python_source_set: frozenset[str] # used to warn about missing mounts during auto-mount deprecation
|
|
420
420
|
_metadata: Optional[api_pb2.ImageMetadata] = None # set on hydration, private for now
|
|
421
|
+
_is_empty: bool
|
|
421
422
|
|
|
422
423
|
def _initialize_from_empty(self):
|
|
423
424
|
self.inside_exceptions = []
|
|
@@ -425,6 +426,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
425
426
|
self._deferred_mounts = ()
|
|
426
427
|
self._added_python_source_set = frozenset()
|
|
427
428
|
self.force_build = False
|
|
429
|
+
self._is_empty = False
|
|
428
430
|
|
|
429
431
|
def _initialize_from_other(self, other: "_Image"):
|
|
430
432
|
self.inside_exceptions = other.inside_exceptions
|
|
@@ -432,6 +434,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
432
434
|
self._serve_mounts = other._serve_mounts
|
|
433
435
|
self._deferred_mounts = other._deferred_mounts
|
|
434
436
|
self._added_python_source_set = other._added_python_source_set
|
|
437
|
+
self._is_empty = other._is_empty
|
|
435
438
|
|
|
436
439
|
def _get_metadata(self) -> Optional[Message]:
|
|
437
440
|
return self._metadata
|
|
@@ -721,6 +724,12 @@ class _Image(_Object, type_prefix="im"):
|
|
|
721
724
|
)
|
|
722
725
|
return obj
|
|
723
726
|
|
|
727
|
+
@staticmethod
|
|
728
|
+
def _from_scratch() -> "_Image":
|
|
729
|
+
image = _Image.from_registry("scratch")
|
|
730
|
+
image._is_empty = True
|
|
731
|
+
return image
|
|
732
|
+
|
|
724
733
|
def _copy_mount(self, mount: _Mount, remote_path: Union[str, Path] = ".") -> "_Image":
|
|
725
734
|
"""mdmd:hidden
|
|
726
735
|
Internal
|
|
@@ -143,6 +143,7 @@ class _Image(modal._object._Object):
|
|
|
143
143
|
_deferred_mounts: collections.abc.Sequence[modal.mount._Mount]
|
|
144
144
|
_added_python_source_set: frozenset[str]
|
|
145
145
|
_metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
|
|
146
|
+
_is_empty: bool
|
|
146
147
|
|
|
147
148
|
def _initialize_from_empty(self): ...
|
|
148
149
|
def _initialize_from_other(self, other: _Image): ...
|
|
@@ -186,6 +187,8 @@ class _Image(modal._object._Object):
|
|
|
186
187
|
_namespace: int = 1,
|
|
187
188
|
_do_assert_no_mount_layers: bool = True,
|
|
188
189
|
): ...
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _from_scratch() -> _Image: ...
|
|
189
192
|
def _copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image:
|
|
190
193
|
"""mdmd:hidden
|
|
191
194
|
Internal
|
|
@@ -1058,6 +1061,7 @@ class Image(modal.object.Object):
|
|
|
1058
1061
|
_deferred_mounts: collections.abc.Sequence[modal.mount.Mount]
|
|
1059
1062
|
_added_python_source_set: frozenset[str]
|
|
1060
1063
|
_metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
|
|
1064
|
+
_is_empty: bool
|
|
1061
1065
|
|
|
1062
1066
|
def __init__(self, *args, **kwargs):
|
|
1063
1067
|
"""mdmd:hidden"""
|
|
@@ -1105,6 +1109,8 @@ class Image(modal.object.Object):
|
|
|
1105
1109
|
_namespace: int = 1,
|
|
1106
1110
|
_do_assert_no_mount_layers: bool = True,
|
|
1107
1111
|
): ...
|
|
1112
|
+
@staticmethod
|
|
1113
|
+
def _from_scratch() -> Image: ...
|
|
1108
1114
|
def _copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image:
|
|
1109
1115
|
"""mdmd:hidden
|
|
1110
1116
|
Internal
|
|
@@ -628,6 +628,9 @@ class _StreamReader(Generic[T]):
|
|
|
628
628
|
|
|
629
629
|
|
|
630
630
|
MAX_BUFFER_SIZE = 2 * 1024 * 1024
|
|
631
|
+
# Larger buffer limit for the new exec path via the task command router.
|
|
632
|
+
# This applies only to task_exec_stdin_write; legacy ContainerExec paths keep MAX_BUFFER_SIZE.
|
|
633
|
+
TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE = 16 * 1024 * 1024
|
|
631
634
|
|
|
632
635
|
|
|
633
636
|
class _StreamWriterThroughServer:
|
|
@@ -721,7 +724,7 @@ class _StreamWriterThroughCommandRouter:
|
|
|
721
724
|
if isinstance(data, (bytes, bytearray, memoryview, str)):
|
|
722
725
|
if isinstance(data, str):
|
|
723
726
|
data = data.encode("utf-8")
|
|
724
|
-
if len(self._buffer) + len(data) >
|
|
727
|
+
if len(self._buffer) + len(data) > TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE:
|
|
725
728
|
raise BufferError("Buffer size exceed limit. Call drain to flush the buffer.")
|
|
726
729
|
self._buffer.extend(data)
|
|
727
730
|
else:
|
|
@@ -37,6 +37,7 @@ NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
|
|
|
37
37
|
def network_file_system_mount_protos(
|
|
38
38
|
validated_network_file_systems: list[tuple[str, "_NetworkFileSystem"]],
|
|
39
39
|
) -> list[api_pb2.SharedVolumeMount]:
|
|
40
|
+
"""mdmd:hidden"""
|
|
40
41
|
network_file_system_mounts = []
|
|
41
42
|
# Relies on dicts being ordered (true as of Python 3.6).
|
|
42
43
|
for path, volume in validated_network_file_systems:
|
|
@@ -11,7 +11,9 @@ import typing_extensions
|
|
|
11
11
|
|
|
12
12
|
def network_file_system_mount_protos(
|
|
13
13
|
validated_network_file_systems: list[tuple[str, _NetworkFileSystem]],
|
|
14
|
-
) -> list[modal_proto.api_pb2.SharedVolumeMount]:
|
|
14
|
+
) -> list[modal_proto.api_pb2.SharedVolumeMount]:
|
|
15
|
+
"""mdmd:hidden"""
|
|
16
|
+
...
|
|
15
17
|
|
|
16
18
|
class _NetworkFileSystem(modal._object._Object):
|
|
17
19
|
"""A shared, writable file system accessible by one or more Modal functions.
|
|
@@ -35,7 +35,7 @@ from ._utils.name_utils import check_object_name
|
|
|
35
35
|
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
36
36
|
from .client import _Client
|
|
37
37
|
from .container_process import _ContainerProcess
|
|
38
|
-
from .exception import ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
|
|
38
|
+
from .exception import ClientClosed, ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
|
|
39
39
|
from .file_io import FileWatchEvent, FileWatchEventType, _FileIO
|
|
40
40
|
from .gpu import GPU_T
|
|
41
41
|
from .image import _Image
|
|
@@ -116,6 +116,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
116
116
|
_tunnels: Optional[dict[int, Tunnel]]
|
|
117
117
|
_enable_snapshot: bool
|
|
118
118
|
_command_router_client: Optional[TaskCommandRouterClient]
|
|
119
|
+
_attached: bool
|
|
119
120
|
|
|
120
121
|
@staticmethod
|
|
121
122
|
def _default_pty_info() -> api_pb2.PTYInfo:
|
|
@@ -516,6 +517,40 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
516
517
|
self._enable_snapshot = False
|
|
517
518
|
self._command_router_client = None
|
|
518
519
|
|
|
520
|
+
def _initialize_from_other(self, other):
|
|
521
|
+
super()._initialize_from_other(other)
|
|
522
|
+
self._attached = other._attached
|
|
523
|
+
|
|
524
|
+
def _initialize_from_empty(self):
|
|
525
|
+
super()._initialize_from_empty()
|
|
526
|
+
self._attached = True
|
|
527
|
+
|
|
528
|
+
async def detach(self):
|
|
529
|
+
"""Disconnects your client from the sandbox and cleans up resources assoicated with the connection.
|
|
530
|
+
|
|
531
|
+
Be sure to only call `detach` when you are done interacting with the sandbox. After calling `detach`,
|
|
532
|
+
any operation using the Sandbox object is not guaranteed to work anymore. If you want to continue interacting
|
|
533
|
+
with a running sandbox, use `Sandbox.from_id` to get a new Sandbox object.
|
|
534
|
+
"""
|
|
535
|
+
if not self._attached:
|
|
536
|
+
return
|
|
537
|
+
if self._command_router_client is not None:
|
|
538
|
+
await self._command_router_client.close()
|
|
539
|
+
self._attached = False
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def _client(self):
|
|
543
|
+
self._ensure_attached()
|
|
544
|
+
return self.__client
|
|
545
|
+
|
|
546
|
+
@_client.setter
|
|
547
|
+
def _client(self, value):
|
|
548
|
+
self.__client = value
|
|
549
|
+
|
|
550
|
+
def _ensure_attached(self):
|
|
551
|
+
if not self._attached:
|
|
552
|
+
raise ClientClosed("Unable to perform operation on a detached sandbox")
|
|
553
|
+
|
|
519
554
|
@staticmethod
|
|
520
555
|
async def from_name(
|
|
521
556
|
app_name: str,
|
|
@@ -609,21 +644,57 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
609
644
|
|
|
610
645
|
return image
|
|
611
646
|
|
|
612
|
-
async def
|
|
613
|
-
"""Mount an Image at a path in
|
|
647
|
+
async def mount_image(self, path: Union[PurePosixPath, str], image: _Image):
|
|
648
|
+
"""Mount an Image at a specified path in a running Sandbox.
|
|
649
|
+
|
|
650
|
+
`path` should be a directory. If it doesn't exist it will be created. If it exists and contains
|
|
651
|
+
data, the previous directory will be replaced by the mount.
|
|
652
|
+
|
|
653
|
+
The `image` argument currently only supports Images that are either:
|
|
654
|
+
- prebuilt using `image.build()`
|
|
655
|
+
- referenced by image id, e.g. `Image.from_id(...)`
|
|
656
|
+
- filesystem/directory snapshots e.g. created by `.snapshot_directory()`
|
|
657
|
+
or `.snapshot_filesystem()`"
|
|
658
|
+
|
|
659
|
+
Usage:
|
|
660
|
+
```py notest
|
|
661
|
+
user_project_snapshot: Image = sandbox_session_1.snapshot_directory("/user_project")
|
|
614
662
|
|
|
615
|
-
|
|
663
|
+
# You can later mount this snapshot to another Sandbox:
|
|
664
|
+
sandbox_session_2 = modal.Sandbox.create(...)
|
|
665
|
+
sandbox_session_2.mount_image("/user_project", user_project_snapshot)
|
|
666
|
+
sandbox_session_2.ls("/user_project")
|
|
667
|
+
```
|
|
668
|
+
"""
|
|
616
669
|
|
|
617
|
-
if image:
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
670
|
+
if not isinstance(image, _Image):
|
|
671
|
+
raise TypeError(f"Sandbox.mount_image(image=...) expects an Image object, got {image!r}")
|
|
672
|
+
|
|
673
|
+
if image._mount_layers:
|
|
674
|
+
raise InvalidError(
|
|
675
|
+
"Sandbox.mount_image() only supports pre-built images. When using `add_local*` methods, "
|
|
676
|
+
"specify `copy=True` and call `.build()` before passing the image to `mount_image()`:\n\nE.g.\n"
|
|
677
|
+
'img = modal.Image.debian_slim().add_local_file("foo", "/foo", copy=True).build(app)\n'
|
|
678
|
+
"sandbox.mount_image(path, img)"
|
|
679
|
+
)
|
|
680
|
+
if image._is_empty:
|
|
681
|
+
image_id = ""
|
|
682
|
+
elif image._object_id:
|
|
621
683
|
image_id = image._object_id
|
|
622
684
|
else:
|
|
623
|
-
|
|
685
|
+
raise InvalidError(
|
|
686
|
+
"Sandbox.mount_image() currently only supports Images that are either:\n"
|
|
687
|
+
"- prebuilt using `image.build()`\n"
|
|
688
|
+
"- referenced by id, e.g. `Image.from_id()`\n"
|
|
689
|
+
"- filesystem/directory snapshots e.g. created by `.snapshot_directory()` "
|
|
690
|
+
"or `.snapshot_filesystem()`\n"
|
|
691
|
+
)
|
|
624
692
|
|
|
625
693
|
task_id = await self._get_task_id()
|
|
626
694
|
if (command_router_client := await self._get_command_router_client(task_id)) is None:
|
|
695
|
+
# It used to be the case that sandboxes could either be controlled through the control
|
|
696
|
+
# plane or through direct connections, but nowadays they should always use direct control
|
|
697
|
+
# so this error should be unexpected
|
|
627
698
|
raise InvalidError("Mounting directories requires direct Sandbox control - please contact Modal support.")
|
|
628
699
|
|
|
629
700
|
posix_path = PurePosixPath(path)
|
|
@@ -634,8 +705,31 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
634
705
|
req = sr_pb2.TaskMountDirectoryRequest(task_id=task_id, path=path_bytes, image_id=image_id)
|
|
635
706
|
await command_router_client.mount_image(req)
|
|
636
707
|
|
|
637
|
-
async def
|
|
638
|
-
"""
|
|
708
|
+
async def _experimental_mount_image(self, path: Union[PurePosixPath, str], image: Optional[_Image]):
|
|
709
|
+
"""Deprecated alias for `Sandbox.mount_image()`."""
|
|
710
|
+
deprecation_warning(
|
|
711
|
+
(2026, 2, 20),
|
|
712
|
+
"The `Sandbox._experimental_mount_image()` method is deprecated. Use `Sandbox.mount_image()` instead.",
|
|
713
|
+
)
|
|
714
|
+
if image is None:
|
|
715
|
+
image = _Image._from_scratch()
|
|
716
|
+
await self.mount_image(path, image)
|
|
717
|
+
|
|
718
|
+
async def snapshot_directory(self, path: Union[PurePosixPath, str]) -> _Image:
|
|
719
|
+
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
720
|
+
|
|
721
|
+
Directory snapshots are currently persisted for 30 days after they were last created or used.
|
|
722
|
+
|
|
723
|
+
Usage:
|
|
724
|
+
```py notest
|
|
725
|
+
user_project_snapshot: Image = sandbox_session_1.snapshot_directory("/user_project")
|
|
726
|
+
|
|
727
|
+
# You can later mount this snapshot to another Sandbox:
|
|
728
|
+
sandbox_session_2 = modal.Sandbox.create(...)
|
|
729
|
+
sandbox_session_2.mount_image("/user_project", user_project_snapshot)
|
|
730
|
+
sandbox_session_2.ls("/user_project")
|
|
731
|
+
```
|
|
732
|
+
"""
|
|
639
733
|
|
|
640
734
|
task_id = await self._get_task_id()
|
|
641
735
|
if (command_router_client := await self._get_command_router_client(task_id)) is None:
|
|
@@ -652,6 +746,15 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
652
746
|
res = await command_router_client.snapshot_directory(req)
|
|
653
747
|
return _Image._new_hydrated(res.image_id, self._client, None)
|
|
654
748
|
|
|
749
|
+
async def _experimental_snapshot_directory(self, path: Union[PurePosixPath, str]) -> _Image:
|
|
750
|
+
"""Deprecated alias for `Sandbox.snapshot_directory()`."""
|
|
751
|
+
deprecation_warning(
|
|
752
|
+
(2026, 2, 20),
|
|
753
|
+
"The `Sandbox._experimental_snapshot_directory()` method is deprecated. "
|
|
754
|
+
"Use `Sandbox.snapshot_directory()` instead.",
|
|
755
|
+
)
|
|
756
|
+
return await self.snapshot_directory(path)
|
|
757
|
+
|
|
655
758
|
# Live handle methods
|
|
656
759
|
|
|
657
760
|
async def wait(self, raise_on_termination: bool = True):
|
|
@@ -659,7 +762,8 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
659
762
|
|
|
660
763
|
while True:
|
|
661
764
|
req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=10)
|
|
662
|
-
|
|
765
|
+
# Use the private __client to allow `wait` to work with a detached sandbox
|
|
766
|
+
resp = await self.__client.stub.SandboxWait(req)
|
|
663
767
|
if resp.result.status:
|
|
664
768
|
logger.debug(f"Sandbox {self.object_id} wait completed with status {resp.result.status}")
|
|
665
769
|
self._result = resp.result
|
|
@@ -702,7 +806,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
702
806
|
self, user_metadata: Optional[Union[str, dict[str, Any]]] = None
|
|
703
807
|
) -> SandboxConnectCredentials:
|
|
704
808
|
"""
|
|
705
|
-
|
|
809
|
+
Create a token for making HTTP connections to the Sandbox.
|
|
706
810
|
|
|
707
811
|
Also accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
708
812
|
will be added to the headers by the proxy when forwarding requests to the Sandbox."""
|
|
@@ -728,12 +832,32 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
728
832
|
),
|
|
729
833
|
)
|
|
730
834
|
|
|
731
|
-
|
|
835
|
+
@overload
|
|
836
|
+
async def terminate(
|
|
837
|
+
self,
|
|
838
|
+
*,
|
|
839
|
+
wait: Literal[True],
|
|
840
|
+
) -> int: ...
|
|
841
|
+
|
|
842
|
+
@overload
|
|
843
|
+
async def terminate(
|
|
844
|
+
self,
|
|
845
|
+
*,
|
|
846
|
+
wait: Literal[False] = False,
|
|
847
|
+
) -> None: ...
|
|
848
|
+
|
|
849
|
+
async def terminate(
|
|
850
|
+
self,
|
|
851
|
+
*,
|
|
852
|
+
wait: bool = False, # wait for terminate to complete and return the exit code.
|
|
853
|
+
) -> int | None:
|
|
732
854
|
"""Terminate Sandbox execution.
|
|
733
855
|
|
|
734
856
|
This is a no-op if the Sandbox has already finished running."""
|
|
735
|
-
|
|
736
857
|
await self._client.stub.SandboxTerminate(api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id))
|
|
858
|
+
if wait:
|
|
859
|
+
await self.wait(raise_on_termination=False)
|
|
860
|
+
return self.returncode
|
|
737
861
|
|
|
738
862
|
async def poll(self) -> Optional[int]:
|
|
739
863
|
"""Check if the Sandbox has finished running.
|
|
@@ -1079,6 +1203,12 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1079
1203
|
raise ExecutionError(resp.task_result.exception)
|
|
1080
1204
|
return sandbox
|
|
1081
1205
|
|
|
1206
|
+
@overload
|
|
1207
|
+
async def open(
|
|
1208
|
+
self,
|
|
1209
|
+
path: str,
|
|
1210
|
+
) -> _FileIO[str]: ...
|
|
1211
|
+
|
|
1082
1212
|
@overload
|
|
1083
1213
|
async def open(
|
|
1084
1214
|
self,
|
|
@@ -1147,7 +1277,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1147
1277
|
[`StreamReader`](https://modal.com/docs/reference/modal.io_streams#modalio_streamsstreamreader) for
|
|
1148
1278
|
the sandbox's stdout stream.
|
|
1149
1279
|
"""
|
|
1150
|
-
|
|
1280
|
+
self._ensure_attached()
|
|
1151
1281
|
return self._stdout
|
|
1152
1282
|
|
|
1153
1283
|
@property
|
|
@@ -1155,7 +1285,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1155
1285
|
"""[`StreamReader`](https://modal.com/docs/reference/modal.io_streams#modalio_streamsstreamreader) for
|
|
1156
1286
|
the Sandbox's stderr stream.
|
|
1157
1287
|
"""
|
|
1158
|
-
|
|
1288
|
+
self._ensure_attached()
|
|
1159
1289
|
return self._stderr
|
|
1160
1290
|
|
|
1161
1291
|
@property
|
|
@@ -1164,7 +1294,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1164
1294
|
[`StreamWriter`](https://modal.com/docs/reference/modal.io_streams#modalio_streamsstreamwriter) for
|
|
1165
1295
|
the Sandbox's stdin stream.
|
|
1166
1296
|
"""
|
|
1167
|
-
|
|
1297
|
+
self._ensure_attached()
|
|
1168
1298
|
return self._stdin
|
|
1169
1299
|
|
|
1170
1300
|
@property
|