modal 1.6.2.dev2__py3-none-any.whl → 1.6.2.dev4__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.
- modal/_environments.py +2 -2
- modal/_functions.py +2 -2
- modal/_utils/name_utils.py +9 -7
- modal/cli/container.py +2 -1
- modal/cli/endpoint.py +6 -1
- modal/cli/environment.py +3 -3
- modal/cli/shell.py +3 -1
- modal/client.pyi +2 -2
- modal/experimental/__init__.py +66 -0
- modal/runner.py +1 -1
- modal/sandbox/__init__.py +9 -0
- modal/{sandbox.py → sandbox/_sandbox.py} +38 -53
- modal/sandbox/sandbox.py +9 -0
- modal/{sandbox.pyi → sandbox/sandbox.pyi} +43 -1371
- modal/sandbox_fs.py +6 -4
- modal/sandbox_fs.pyi +10 -5
- modal/secret.py +13 -4
- modal/secret.pyi +4 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/METADATA +1 -1
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/RECORD +31 -29
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +1276 -1233
- modal_proto/api_pb2.pyi +113 -4
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/__init__.py +1 -1
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/WHEEL +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/entry_points.txt +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/licenses/LICENSE +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/top_level.txt +0 -0
modal/_environments.py
CHANGED
|
@@ -54,7 +54,7 @@ class _EnvironmentManager:
|
|
|
54
54
|
modal.Environment.objects.create("my-environment")
|
|
55
55
|
```
|
|
56
56
|
"""
|
|
57
|
-
check_environment_name(name)
|
|
57
|
+
check_environment_name(name, allow_periods=False)
|
|
58
58
|
|
|
59
59
|
client = await _Client.from_env() if client is None else client
|
|
60
60
|
request = api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted)
|
|
@@ -464,7 +464,7 @@ class _Environment(_Object, type_prefix="en"):
|
|
|
464
464
|
client: _Client | None = None,
|
|
465
465
|
) -> "_Environment":
|
|
466
466
|
"""Look up an Environment object using its name."""
|
|
467
|
-
check_environment_name(name)
|
|
467
|
+
check_environment_name(name, allow_periods=True)
|
|
468
468
|
return _Environment._get_or_create(
|
|
469
469
|
name=name,
|
|
470
470
|
repr=f"Environment.from_name({name!r})",
|
modal/_functions.py
CHANGED
|
@@ -819,8 +819,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
819
819
|
raise InvalidError(
|
|
820
820
|
f"`min_containers` ({min_containers}) cannot be greater than `max_containers` ({max_containers})"
|
|
821
821
|
)
|
|
822
|
-
if scaledown_window is not None and scaledown_window <=
|
|
823
|
-
raise InvalidError("`scaledown_window` must be
|
|
822
|
+
if scaledown_window is not None and not (2 <= scaledown_window <= 3600):
|
|
823
|
+
raise InvalidError("`scaledown_window` must be between 2 and 3600 (inclusive)")
|
|
824
824
|
|
|
825
825
|
autoscaler_settings = api_pb2.AutoscalerSettings(
|
|
826
826
|
min_containers=min_containers,
|
modal/_utils/name_utils.py
CHANGED
|
@@ -27,13 +27,15 @@ def is_valid_object_name(name: str) -> bool:
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def is_valid_environment_name(name: str) -> bool:
|
|
30
|
+
def is_valid_environment_name(name: str, *, allow_periods: bool) -> bool:
|
|
31
31
|
# first char is alnum, the rest allows other chars
|
|
32
32
|
# The "en-" prefix is reserved for environment IDs, so that references
|
|
33
33
|
# to environments can be resolved by either name or ID unambiguously.
|
|
34
|
+
# New names cannot contain periods, but some existing Environments have them and must stay resolvable.
|
|
35
|
+
rest_chars = r"a-zA-Z0-9-_." if allow_periods else r"a-zA-Z0-9-_"
|
|
34
36
|
return (
|
|
35
37
|
len(name) <= 64
|
|
36
|
-
and re.match(
|
|
38
|
+
and re.match(rf"^[a-zA-Z0-9][{rest_chars}]*$", name) is not None
|
|
37
39
|
and not name.lower().startswith("en-")
|
|
38
40
|
)
|
|
39
41
|
|
|
@@ -67,12 +69,12 @@ def check_object_name(name: str, object_type: str) -> None:
|
|
|
67
69
|
raise InvalidError(message)
|
|
68
70
|
|
|
69
71
|
|
|
70
|
-
def check_environment_name(name: str) -> None:
|
|
72
|
+
def check_environment_name(name: str, *, allow_periods: bool) -> None:
|
|
71
73
|
message = (
|
|
72
74
|
f"Invalid Environment name: '{name}'."
|
|
73
|
-
"\n\nEnvironment names can only start with alphanumeric characters,"
|
|
74
|
-
" may contain only alphanumeric characters, dashes,
|
|
75
|
-
"
|
|
75
|
+
"\n\nEnvironment names must be 1-64 characters, can only start with alphanumeric characters,"
|
|
76
|
+
" may contain only alphanumeric characters, dashes, and underscores,"
|
|
77
|
+
" and cannot start with 'en-'."
|
|
76
78
|
)
|
|
77
|
-
if not is_valid_environment_name(name):
|
|
79
|
+
if not is_valid_environment_name(name, allow_periods=allow_periods):
|
|
78
80
|
raise InvalidError(message)
|
modal/cli/container.py
CHANGED
|
@@ -27,7 +27,8 @@ from modal.cli.utils import (
|
|
|
27
27
|
)
|
|
28
28
|
from modal.client import _Client
|
|
29
29
|
from modal.exception import ConflictError, InvalidError
|
|
30
|
-
from modal.sandbox import SandboxVersion,
|
|
30
|
+
from modal.sandbox._sandbox import SandboxVersion, _get_sandbox_version, _Sandbox
|
|
31
|
+
from modal.sandbox.sandbox import _container_exec
|
|
31
32
|
from modal_proto import api_pb2
|
|
32
33
|
|
|
33
34
|
from ._help import ModalGroup
|
modal/cli/endpoint.py
CHANGED
|
@@ -260,7 +260,7 @@ async def list_(*, json: bool = False, env: Optional[str] = None):
|
|
|
260
260
|
@click.option(
|
|
261
261
|
"--name",
|
|
262
262
|
default=None,
|
|
263
|
-
help="Endpoint name.
|
|
263
|
+
help="Endpoint name. Defaults to inference-{number} if not provided.",
|
|
264
264
|
)
|
|
265
265
|
@click.option(
|
|
266
266
|
"--model",
|
|
@@ -410,6 +410,8 @@ async def create(
|
|
|
410
410
|
|
|
411
411
|
output = OutputManager.get()
|
|
412
412
|
output.print(f"[green]✓[/green] Endpoint '{resp.name}' ({resp.endpoint_id}) was created and started provisioning.")
|
|
413
|
+
if resp.model_name:
|
|
414
|
+
output.print(f' → Send [cyan]"model": "{resp.model_name}"[/cyan] in requests to the Endpoint.')
|
|
413
415
|
if resp.endpoint_page_url:
|
|
414
416
|
output.print(f" → View progress at [magenta]{resp.endpoint_page_url}[/magenta].")
|
|
415
417
|
output.print(" → The Endpoint will also appear in [cyan]modal endpoint list[/cyan].")
|
|
@@ -517,6 +519,7 @@ async def info(
|
|
|
517
519
|
{
|
|
518
520
|
"name": endpoint_info.name,
|
|
519
521
|
"endpoint_id": endpoint_id,
|
|
522
|
+
"model_name": endpoint_info.model_name or None,
|
|
520
523
|
"repo_id": endpoint_info.repo_id,
|
|
521
524
|
"revision": endpoint_info.revision or None,
|
|
522
525
|
"volume_id": endpoint_info.volume_id or None,
|
|
@@ -547,6 +550,8 @@ async def info(
|
|
|
547
550
|
header.add_row(Text("Endpoint:"), endpoint_text)
|
|
548
551
|
header.add_row(Text("Endpoint ID:"), Text(endpoint_id))
|
|
549
552
|
header.add_row(Text("State:"), Text(status))
|
|
553
|
+
if endpoint_info.model_name:
|
|
554
|
+
header.add_row(Text("Model name:"), Text(endpoint_info.model_name))
|
|
550
555
|
model_text = Text(endpoint_info.repo_id)
|
|
551
556
|
if endpoint_info.revision:
|
|
552
557
|
model_text.append(f"@{endpoint_info.revision[:7]}")
|
modal/cli/environment.py
CHANGED
|
@@ -87,7 +87,7 @@ def create(
|
|
|
87
87
|
default_role: str | None = None,
|
|
88
88
|
public: bool = False,
|
|
89
89
|
):
|
|
90
|
-
check_environment_name(name)
|
|
90
|
+
check_environment_name(name, allow_periods=False)
|
|
91
91
|
Environment.objects.create(
|
|
92
92
|
name,
|
|
93
93
|
restricted=restricted,
|
|
@@ -116,8 +116,8 @@ def update(
|
|
|
116
116
|
if set_name is None and set_web_suffix is None:
|
|
117
117
|
raise UsageError("You need to at least one new property (using --set-name or --set-web-suffix)")
|
|
118
118
|
|
|
119
|
-
if set_name:
|
|
120
|
-
check_environment_name(set_name)
|
|
119
|
+
if set_name and set_name != current_name:
|
|
120
|
+
check_environment_name(set_name, allow_periods=False)
|
|
121
121
|
|
|
122
122
|
environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
|
|
123
123
|
rich.print("[green]✓[/green] Environment updated")
|
modal/cli/shell.py
CHANGED
|
@@ -17,7 +17,9 @@ from ..functions import Function
|
|
|
17
17
|
from ..image import Image
|
|
18
18
|
from ..mount import _Mount
|
|
19
19
|
from ..runner import interactive_shell
|
|
20
|
-
from ..sandbox import
|
|
20
|
+
from ..sandbox import Sandbox
|
|
21
|
+
from ..sandbox._sandbox import _MAIN_CONTAINER_NAME, SandboxVersion, _get_sandbox_version
|
|
22
|
+
from ..sandbox.sandbox import _container_exec
|
|
21
23
|
from ..secret import Secret
|
|
22
24
|
from ..stream_type import StreamType
|
|
23
25
|
from ..types import SandboxRuntime
|
modal/client.pyi
CHANGED
|
@@ -80,7 +80,7 @@ class _Client:
|
|
|
80
80
|
server_url: str,
|
|
81
81
|
client_type: int,
|
|
82
82
|
credentials: typing.Optional[tuple[str, str]],
|
|
83
|
-
version: str = "1.6.2.
|
|
83
|
+
version: str = "1.6.2.dev4",
|
|
84
84
|
*,
|
|
85
85
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
86
86
|
):
|
|
@@ -290,7 +290,7 @@ class Client:
|
|
|
290
290
|
server_url: str,
|
|
291
291
|
client_type: int,
|
|
292
292
|
credentials: typing.Optional[tuple[str, str]],
|
|
293
|
-
version: str = "1.6.2.
|
|
293
|
+
version: str = "1.6.2.dev4",
|
|
294
294
|
*,
|
|
295
295
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
296
296
|
):
|
modal/experimental/__init__.py
CHANGED
|
@@ -93,6 +93,72 @@ def clustered(size: int, broadcast: bool = True, rdma: bool = False, fabric_size
|
|
|
93
93
|
return wrapper
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
@dataclass
|
|
97
|
+
class ClusterReservationTask:
|
|
98
|
+
"""A container running on a worker that belongs to a cluster reservation."""
|
|
99
|
+
|
|
100
|
+
task_id: str
|
|
101
|
+
cluster_rank: int
|
|
102
|
+
app_id: str
|
|
103
|
+
function_id: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ClusterReservation:
|
|
108
|
+
"""Current state of reserved multi-node cluster capacity in a Workspace."""
|
|
109
|
+
|
|
110
|
+
name: str # Reservation name, as configured by Modal
|
|
111
|
+
gpu_type: str # e.g. "B300"
|
|
112
|
+
region: str
|
|
113
|
+
min_reserved_workers: int # Number of workers Modal has committed to keep reserved
|
|
114
|
+
reserved_workers: int # Healthy reserved workers currently present (active + idle)
|
|
115
|
+
active_workers: int # Reserved workers running this Workspace's containers
|
|
116
|
+
idle_workers: int # Reserved workers available for new clusters
|
|
117
|
+
unfulfilled_workers: int # max(0, min_reserved_workers - reserved_workers)
|
|
118
|
+
tasks: list[ClusterReservationTask] # Containers on reserved workers, ordered by cluster then rank
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@synchronizer.create_blocking
|
|
122
|
+
async def list_cluster_reservations(*, client: _Client | None = None) -> list[ClusterReservation]:
|
|
123
|
+
"""List reserved multi-node cluster capacity for the current Workspace.
|
|
124
|
+
|
|
125
|
+
Returns one entry per reservation Modal has configured for the Workspace, with how many of the
|
|
126
|
+
reserved workers are currently present, in use, idle or missing. Only Workspaces with reserved
|
|
127
|
+
cluster capacity (or explicitly allowlisted by Modal) may call this; others get a
|
|
128
|
+
`modal.exception.PermissionDeniedError`.
|
|
129
|
+
|
|
130
|
+
This interface is experimental. This information will continue to be available in the future,
|
|
131
|
+
but it may be accessed via a different interface, and the return value may have a different shape.
|
|
132
|
+
"""
|
|
133
|
+
client = client or await _Client.from_env()
|
|
134
|
+
|
|
135
|
+
resp: api_pb2.ClusterReservationListResponse = await client._stub.ClusterReservationList(
|
|
136
|
+
api_pb2.ClusterReservationListRequest()
|
|
137
|
+
)
|
|
138
|
+
return [
|
|
139
|
+
ClusterReservation(
|
|
140
|
+
name=reservation.name,
|
|
141
|
+
gpu_type=reservation.gpu_type,
|
|
142
|
+
region=reservation.region,
|
|
143
|
+
min_reserved_workers=reservation.min_reserved_workers,
|
|
144
|
+
reserved_workers=reservation.reserved_workers,
|
|
145
|
+
active_workers=reservation.active_workers,
|
|
146
|
+
idle_workers=reservation.idle_workers,
|
|
147
|
+
unfulfilled_workers=reservation.unfulfilled_workers,
|
|
148
|
+
tasks=[
|
|
149
|
+
ClusterReservationTask(
|
|
150
|
+
task_id=task.task_id,
|
|
151
|
+
cluster_rank=task.cluster_rank,
|
|
152
|
+
app_id=task.app_id,
|
|
153
|
+
function_id=task.function_id,
|
|
154
|
+
)
|
|
155
|
+
for task in reservation.tasks
|
|
156
|
+
],
|
|
157
|
+
)
|
|
158
|
+
for reservation in resp.reservations
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
96
162
|
@dataclass
|
|
97
163
|
class AppInfo:
|
|
98
164
|
app_id: str
|
modal/runner.py
CHANGED
|
@@ -43,7 +43,7 @@ from .exception import (
|
|
|
43
43
|
)
|
|
44
44
|
from .output import OutputManager
|
|
45
45
|
from .running_app import RunningApp, running_app_from_layout
|
|
46
|
-
from .sandbox import _Sandbox
|
|
46
|
+
from .sandbox._sandbox import _Sandbox
|
|
47
47
|
from .secret import _Secret
|
|
48
48
|
from .stream_type import StreamType
|
|
49
49
|
|
|
@@ -23,31 +23,31 @@ from modal._supports_logs import LogsFilters, _LogQueryData
|
|
|
23
23
|
from modal._tunnel import Tunnel
|
|
24
24
|
from modal.cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
25
25
|
from modal.mount import _Mount
|
|
26
|
-
from modal.secret import _local_secret_env, _resolvable_secrets
|
|
26
|
+
from modal.secret import _local_secret_env, _resolvable_secrets, _secret_sources
|
|
27
27
|
from modal.volume import _Volume, _volume_to_mount_proto
|
|
28
28
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
29
29
|
|
|
30
|
-
from
|
|
31
|
-
from
|
|
32
|
-
from
|
|
33
|
-
from
|
|
34
|
-
from
|
|
35
|
-
from
|
|
36
|
-
from
|
|
37
|
-
from
|
|
38
|
-
from
|
|
39
|
-
from
|
|
40
|
-
from
|
|
30
|
+
from .._image import _Image
|
|
31
|
+
from .._load_context import LoadContext
|
|
32
|
+
from .._object import _get_environment_name, _Object
|
|
33
|
+
from .._outbound_policy import _OutboundPolicy, _validate_compatible_network_access
|
|
34
|
+
from .._output.pty import get_pty_info
|
|
35
|
+
from .._resolver import Resolver
|
|
36
|
+
from .._resources import convert_fn_config_to_resources_config
|
|
37
|
+
from .._utils.async_utils import TaskContext
|
|
38
|
+
from .._utils.deprecation import deprecation_warning
|
|
39
|
+
from .._utils.grpc_utils import Retry, RetryTimeoutError
|
|
40
|
+
from .._utils.mount_utils import (
|
|
41
41
|
validate_network_file_systems,
|
|
42
42
|
validate_volumes,
|
|
43
43
|
validate_volumes_by_object_id,
|
|
44
44
|
)
|
|
45
|
-
from
|
|
46
|
-
from
|
|
47
|
-
from
|
|
48
|
-
from
|
|
49
|
-
from
|
|
50
|
-
from
|
|
45
|
+
from .._utils.name_utils import check_object_name
|
|
46
|
+
from .._utils.task_command_router_client import TaskCommandRouterClient, _is_v2_task_id
|
|
47
|
+
from ..client import _Client
|
|
48
|
+
from ..config import config, logger
|
|
49
|
+
from ..container_process import _ContainerProcess
|
|
50
|
+
from ..exception import (
|
|
51
51
|
ClientClosed,
|
|
52
52
|
ConflictError,
|
|
53
53
|
ExecutionError,
|
|
@@ -61,7 +61,7 @@ from .exception import (
|
|
|
61
61
|
SnapshotCreationError,
|
|
62
62
|
TimeoutError,
|
|
63
63
|
)
|
|
64
|
-
from
|
|
64
|
+
from ..io_streams import (
|
|
65
65
|
StreamReader,
|
|
66
66
|
StreamWriter,
|
|
67
67
|
_StreamReader,
|
|
@@ -71,13 +71,13 @@ from .io_streams import (
|
|
|
71
71
|
_StreamWriterThroughCommandRouterSandboxParams,
|
|
72
72
|
_StreamWriterThroughServerParams,
|
|
73
73
|
)
|
|
74
|
-
from
|
|
75
|
-
from
|
|
76
|
-
from
|
|
77
|
-
from
|
|
78
|
-
from
|
|
79
|
-
from
|
|
80
|
-
from
|
|
74
|
+
from ..network_file_system import _NetworkFileSystem, network_file_system_mount_protos
|
|
75
|
+
from ..proxy import _Proxy
|
|
76
|
+
from ..sandbox_fs import _SandboxFilesystem
|
|
77
|
+
from ..secret import _Secret
|
|
78
|
+
from ..snapshot import _SandboxSnapshot
|
|
79
|
+
from ..stream_type import StreamType
|
|
80
|
+
from ..types import SandboxConnectCredentials, SandboxRuntime
|
|
81
81
|
|
|
82
82
|
_default_image: _Image = _Image.debian_slim()
|
|
83
83
|
_EXIT_SNAPSHOT_NOT_FOUND_ERROR_CODES = frozenset(
|
|
@@ -307,11 +307,11 @@ class DefaultSandboxNameOverride(str):
|
|
|
307
307
|
"""
|
|
308
308
|
|
|
309
309
|
def __repr__(self) -> str:
|
|
310
|
-
# NOTE: this must match the instance var name below in order for type stubs to work 😬
|
|
311
|
-
return "_DEFAULT_SANDBOX_NAME_OVERRIDE"
|
|
310
|
+
# NOTE: this must match the qualified instance var name below in order for type stubs to work 😬
|
|
311
|
+
return "modal.sandbox._sandbox._DEFAULT_SANDBOX_NAME_OVERRIDE"
|
|
312
312
|
|
|
313
313
|
|
|
314
|
-
_DEFAULT_SANDBOX_NAME_OVERRIDE = DefaultSandboxNameOverride()
|
|
314
|
+
_DEFAULT_SANDBOX_NAME_OVERRIDE: DefaultSandboxNameOverride = DefaultSandboxNameOverride()
|
|
315
315
|
|
|
316
316
|
|
|
317
317
|
@dataclass(frozen=True)
|
|
@@ -386,7 +386,7 @@ def _resolve_app_id_and_client(
|
|
|
386
386
|
client: "_Client | None",
|
|
387
387
|
) -> "tuple[str | None, _Client | None]":
|
|
388
388
|
"""Resolve the App id and client for Sandbox creation, validating that an App is available."""
|
|
389
|
-
from
|
|
389
|
+
from ..app import _App
|
|
390
390
|
|
|
391
391
|
if app is not None:
|
|
392
392
|
if app.app_id is None:
|
|
@@ -1116,9 +1116,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1116
1116
|
dep_tasks.append(resolver.load(proxy, load_context))
|
|
1117
1117
|
dep_timings = await _gather_load_with_timings(dep_tasks) if dep_tasks else []
|
|
1118
1118
|
|
|
1119
|
-
# `env` takes precedence over environment variables from secrets
|
|
1120
|
-
env_dict = _local_secret_env(secrets) | ephemeral_env
|
|
1121
|
-
|
|
1122
1119
|
validate_volumes_by_object_id(validated_volumes)
|
|
1123
1120
|
|
|
1124
1121
|
volume_mounts = [_volume_to_mount_proto(path, volume) for path, volume in validated_volumes]
|
|
@@ -1165,7 +1162,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1165
1162
|
create_req = api_pb2.SandboxCreateV2Request(
|
|
1166
1163
|
app_id=load_context.app_id,
|
|
1167
1164
|
definition=definition,
|
|
1168
|
-
|
|
1165
|
+
secret_sources=_secret_sources(secrets, ephemeral_env),
|
|
1169
1166
|
tags=tag_protos,
|
|
1170
1167
|
cloud_bucket_mount_credentials=cloud_bucket_credientials,
|
|
1171
1168
|
)
|
|
@@ -2306,8 +2303,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2306
2303
|
secret_coros = [secret.hydrate(client=self._client) for secret in resolvable_secrets]
|
|
2307
2304
|
await TaskContext.gather(*secret_coros)
|
|
2308
2305
|
|
|
2309
|
-
env_dict =
|
|
2310
|
-
env_dict |= {k: v for k, v in (env or {}).items() if v is not None}
|
|
2306
|
+
env_dict = {k: v for k, v in (env or {}).items() if v is not None}
|
|
2311
2307
|
|
|
2312
2308
|
task_id = await self._get_task_id(raise_if_task_complete=True)
|
|
2313
2309
|
|
|
@@ -2324,8 +2320,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2324
2320
|
stderr=stderr,
|
|
2325
2321
|
timeout=timeout,
|
|
2326
2322
|
workdir=workdir,
|
|
2327
|
-
|
|
2328
|
-
env=env_dict,
|
|
2323
|
+
secret_sources=_secret_sources(secrets, env_dict),
|
|
2329
2324
|
text=text,
|
|
2330
2325
|
bufsize=bufsize,
|
|
2331
2326
|
runtime_debug=config.get("function_runtime_debug"),
|
|
@@ -2342,8 +2337,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2342
2337
|
stderr: StreamType = StreamType.PIPE,
|
|
2343
2338
|
timeout: int | None = None,
|
|
2344
2339
|
workdir: str | None = None,
|
|
2345
|
-
|
|
2346
|
-
env: dict[str, str] | None = None,
|
|
2340
|
+
secret_sources: Collection[api_pb2.SecretSource] | None = None,
|
|
2347
2341
|
text: bool = True,
|
|
2348
2342
|
bufsize: Literal[-1, 1] = -1,
|
|
2349
2343
|
runtime_debug: bool = False,
|
|
@@ -2382,11 +2376,10 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2382
2376
|
stderr_config=stderr_config,
|
|
2383
2377
|
timeout_secs=timeout,
|
|
2384
2378
|
workdir=workdir,
|
|
2385
|
-
|
|
2379
|
+
secret_sources=secret_sources,
|
|
2386
2380
|
pty_info=pty_info,
|
|
2387
2381
|
runtime_debug=runtime_debug,
|
|
2388
2382
|
container_id=container_id or "",
|
|
2389
|
-
env=env or {},
|
|
2390
2383
|
)
|
|
2391
2384
|
_ = await command_router_client.exec_start(start_req)
|
|
2392
2385
|
|
|
@@ -3233,8 +3226,7 @@ class _SidecarManager:
|
|
|
3233
3226
|
*(resolver.load(dependency, load_context) for dependency in dependencies if not dependency._is_hydrated)
|
|
3234
3227
|
)
|
|
3235
3228
|
|
|
3236
|
-
|
|
3237
|
-
env_dict = _local_secret_env(secrets) | (env or {})
|
|
3229
|
+
secret_sources = _secret_sources(secrets, env)
|
|
3238
3230
|
|
|
3239
3231
|
# Validate that the same volume (by object_id) isn't mounted at multiple paths. This relies on
|
|
3240
3232
|
# the volumes being hydrated above, since it compares object_ids.
|
|
@@ -3271,7 +3263,7 @@ class _SidecarManager:
|
|
|
3271
3263
|
sandbox_id=self._sandbox.object_id,
|
|
3272
3264
|
container_name=name,
|
|
3273
3265
|
definition=definition,
|
|
3274
|
-
|
|
3266
|
+
secret_sources=secret_sources,
|
|
3275
3267
|
cloud_bucket_mount_credentials=cloud_bucket_credentials,
|
|
3276
3268
|
)
|
|
3277
3269
|
client = self._sandbox._client
|
|
@@ -3286,9 +3278,8 @@ class _SidecarManager:
|
|
|
3286
3278
|
container_name=name,
|
|
3287
3279
|
image_id=image.object_id,
|
|
3288
3280
|
args=list(args),
|
|
3289
|
-
env=env_dict,
|
|
3290
3281
|
workdir=workdir or "",
|
|
3291
|
-
|
|
3282
|
+
secret_sources=secret_sources,
|
|
3292
3283
|
volume_mounts=volume_mounts,
|
|
3293
3284
|
network_access=network_access,
|
|
3294
3285
|
pty_info=pty_info,
|
|
@@ -3328,7 +3319,6 @@ class _SidecarManager:
|
|
|
3328
3319
|
]
|
|
3329
3320
|
|
|
3330
3321
|
|
|
3331
|
-
@synchronizer.create_blocking
|
|
3332
3322
|
async def _container_exec(
|
|
3333
3323
|
pty: bool,
|
|
3334
3324
|
container_id: str = "",
|
|
@@ -3386,8 +3376,3 @@ async def _container_exec(
|
|
|
3386
3376
|
stdout=StreamType.STDOUT,
|
|
3387
3377
|
stderr=StreamType.STDOUT,
|
|
3388
3378
|
).wait()
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
SidecarContainer = synchronize_api(_SidecarContainer)
|
|
3392
|
-
SidecarManager = synchronize_api(_SidecarManager)
|
|
3393
|
-
Sandbox = synchronize_api(_Sandbox)
|
modal/sandbox/sandbox.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
from .._utils.async_utils import synchronize_api, synchronizer
|
|
3
|
+
from . import _sandbox
|
|
4
|
+
|
|
5
|
+
SidecarContainer = synchronize_api(_sandbox._SidecarContainer, target_module=__name__)
|
|
6
|
+
SidecarManager = synchronize_api(_sandbox._SidecarManager, target_module=__name__)
|
|
7
|
+
Sandbox = synchronize_api(_sandbox._Sandbox, target_module=__name__)
|
|
8
|
+
|
|
9
|
+
_container_exec = synchronizer.create_blocking(_sandbox._container_exec, "_container_exec", target_module=__name__)
|