modal 0.62.16__py3-none-any.whl → 0.72.11__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/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/stub.py
DELETED
@@ -1,783 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import inspect
|
3
|
-
import os
|
4
|
-
import typing
|
5
|
-
from pathlib import PurePosixPath
|
6
|
-
from typing import Any, AsyncGenerator, Callable, ClassVar, Dict, List, Optional, Sequence, Tuple, Union
|
7
|
-
|
8
|
-
from synchronicity.async_wrap import asynccontextmanager
|
9
|
-
|
10
|
-
from modal_proto import api_pb2
|
11
|
-
|
12
|
-
from ._ipython import is_notebook
|
13
|
-
from ._output import OutputManager
|
14
|
-
from ._resolver import Resolver
|
15
|
-
from ._utils.async_utils import synchronize_api
|
16
|
-
from ._utils.function_utils import FunctionInfo
|
17
|
-
from ._utils.mount_utils import validate_volumes
|
18
|
-
from .app import _ContainerApp, _LocalApp
|
19
|
-
from .client import _Client
|
20
|
-
from .cls import _Cls
|
21
|
-
from .config import logger
|
22
|
-
from .exception import InvalidError, deprecation_error, deprecation_warning
|
23
|
-
from .functions import _Function
|
24
|
-
from .gpu import GPU_T
|
25
|
-
from .image import _Image
|
26
|
-
from .mount import _Mount
|
27
|
-
from .network_file_system import _NetworkFileSystem
|
28
|
-
from .object import _Object
|
29
|
-
from .partial_function import PartialFunction, _find_callables_for_cls, _PartialFunction, _PartialFunctionFlags
|
30
|
-
from .proxy import _Proxy
|
31
|
-
from .retries import Retries
|
32
|
-
from .runner import _run_stub
|
33
|
-
from .sandbox import _Sandbox
|
34
|
-
from .schedule import Schedule
|
35
|
-
from .scheduler_placement import SchedulerPlacement
|
36
|
-
from .secret import _Secret
|
37
|
-
from .volume import _Volume
|
38
|
-
|
39
|
-
_default_image: _Image = _Image.debian_slim()
|
40
|
-
|
41
|
-
|
42
|
-
class _LocalEntrypoint:
|
43
|
-
_info: FunctionInfo
|
44
|
-
_stub: "_Stub"
|
45
|
-
|
46
|
-
def __init__(self, info, stub):
|
47
|
-
self._info = info # type: ignore
|
48
|
-
self._stub = stub
|
49
|
-
|
50
|
-
def __call__(self, *args, **kwargs):
|
51
|
-
return self._info.raw_f(*args, **kwargs)
|
52
|
-
|
53
|
-
@property
|
54
|
-
def info(self) -> FunctionInfo:
|
55
|
-
return self._info
|
56
|
-
|
57
|
-
@property
|
58
|
-
def stub(self) -> "_Stub":
|
59
|
-
return self._stub
|
60
|
-
|
61
|
-
|
62
|
-
LocalEntrypoint = synchronize_api(_LocalEntrypoint)
|
63
|
-
|
64
|
-
|
65
|
-
def check_sequence(items: typing.Sequence[typing.Any], item_type: typing.Type[typing.Any], error_msg: str):
|
66
|
-
if not isinstance(items, (list, tuple)):
|
67
|
-
raise InvalidError(error_msg)
|
68
|
-
if not all(isinstance(v, item_type) for v in items):
|
69
|
-
raise InvalidError(error_msg)
|
70
|
-
|
71
|
-
|
72
|
-
CLS_T = typing.TypeVar("CLS_T", bound=typing.Type)
|
73
|
-
|
74
|
-
|
75
|
-
class _Stub:
|
76
|
-
"""A `Stub` is a description of how to create a Modal application.
|
77
|
-
|
78
|
-
The stub object principally describes Modal objects (`Function`, `Image`,
|
79
|
-
`Secret`, etc.) associated with the application. It has three responsibilities:
|
80
|
-
|
81
|
-
* Syncing of identities across processes (your local Python interpreter and
|
82
|
-
every Modal worker active in your application).
|
83
|
-
* Making Objects stay alive and not be garbage collected for as long as the
|
84
|
-
app lives (see App lifetime below).
|
85
|
-
* Manage log collection for everything that happens inside your code.
|
86
|
-
|
87
|
-
**Registering functions with an app**
|
88
|
-
|
89
|
-
The most common way to explicitly register an Object with an app is through the
|
90
|
-
`@stub.function()` decorator. It both registers the annotated function itself and
|
91
|
-
other passed objects, like schedules and secrets, with the app:
|
92
|
-
|
93
|
-
```python
|
94
|
-
import modal
|
95
|
-
|
96
|
-
stub = modal.Stub()
|
97
|
-
|
98
|
-
@stub.function(
|
99
|
-
secrets=[modal.Secret.from_name("some_secret")],
|
100
|
-
schedule=modal.Period(days=1),
|
101
|
-
)
|
102
|
-
def foo():
|
103
|
-
pass
|
104
|
-
```
|
105
|
-
|
106
|
-
In this example, the secret and schedule are registered with the app.
|
107
|
-
"""
|
108
|
-
|
109
|
-
_name: Optional[str]
|
110
|
-
_description: Optional[str]
|
111
|
-
_indexed_objects: Dict[str, _Object]
|
112
|
-
_function_mounts: Dict[str, _Mount]
|
113
|
-
_mounts: Sequence[_Mount]
|
114
|
-
_secrets: Sequence[_Secret]
|
115
|
-
_volumes: Dict[Union[str, PurePosixPath], _Volume]
|
116
|
-
_web_endpoints: List[str] # Used by the CLI
|
117
|
-
_local_entrypoints: Dict[str, _LocalEntrypoint]
|
118
|
-
_container_app: Optional[_ContainerApp]
|
119
|
-
_local_app: Optional[_LocalApp]
|
120
|
-
_all_stubs: ClassVar[Dict[Optional[str], List["_Stub"]]] = {}
|
121
|
-
|
122
|
-
def __init__(
|
123
|
-
self,
|
124
|
-
name: Optional[str] = None,
|
125
|
-
*,
|
126
|
-
image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
|
127
|
-
mounts: Sequence[_Mount] = [], # default mounts for all functions
|
128
|
-
secrets: Sequence[_Secret] = [], # default secrets for all functions
|
129
|
-
volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
|
130
|
-
**indexed_objects: _Object, # any Modal Object dependencies (Dict, Queue, etc.)
|
131
|
-
) -> None:
|
132
|
-
"""Construct a new app stub, optionally with default image, mounts, secrets
|
133
|
-
|
134
|
-
Any "indexed_objects" objects are loaded as part of running or deploying the app,
|
135
|
-
and are accessible by name on the running container app, e.g.:
|
136
|
-
```python
|
137
|
-
stub = modal.Stub(key_value_store=modal.Dict.new())
|
138
|
-
|
139
|
-
@stub.function()
|
140
|
-
def store_something(key: str, value: str):
|
141
|
-
stub.app.key_value_store.put(key, value)
|
142
|
-
```
|
143
|
-
"""
|
144
|
-
|
145
|
-
self._name = name
|
146
|
-
self._description = name
|
147
|
-
|
148
|
-
check_sequence(mounts, _Mount, "mounts has to be a list or tuple of Mount objects")
|
149
|
-
check_sequence(secrets, _Secret, "secrets has to be a list or tuple of Secret objects")
|
150
|
-
validate_volumes(volumes)
|
151
|
-
|
152
|
-
if image is not None and not isinstance(image, _Image):
|
153
|
-
raise InvalidError("image has to be a modal Image or AioImage object")
|
154
|
-
|
155
|
-
if indexed_objects:
|
156
|
-
deprecation_warning(
|
157
|
-
(2023, 12, 13),
|
158
|
-
"Passing **kwargs to a stub is deprecated. In most cases, you can just define the objects in global scope.",
|
159
|
-
)
|
160
|
-
|
161
|
-
for k, v in indexed_objects.items():
|
162
|
-
self._validate_blueprint_value(k, v)
|
163
|
-
|
164
|
-
self._indexed_objects = indexed_objects
|
165
|
-
if image is not None:
|
166
|
-
self._indexed_objects["image"] = image # backward compatibility since "image" used to be on the blueprint
|
167
|
-
|
168
|
-
self._mounts = mounts
|
169
|
-
|
170
|
-
self._secrets = secrets
|
171
|
-
self._volumes = volumes
|
172
|
-
self._local_entrypoints = {}
|
173
|
-
self._web_endpoints = []
|
174
|
-
self._local_app = None # when this is the launcher process
|
175
|
-
self._container_app = None # when this is inside a container
|
176
|
-
|
177
|
-
# Register this stub. This is used to look up the stub in the container, when we can't get it from the function
|
178
|
-
_Stub._all_stubs.setdefault(self._name, []).append(self)
|
179
|
-
|
180
|
-
@property
|
181
|
-
def name(self) -> Optional[str]:
|
182
|
-
"""The user-provided name of the Stub."""
|
183
|
-
return self._name
|
184
|
-
|
185
|
-
@property
|
186
|
-
def is_interactive(self) -> bool:
|
187
|
-
"""Whether the current app for the stub is running in interactive mode."""
|
188
|
-
# return self._name
|
189
|
-
if self._local_app:
|
190
|
-
return self._local_app.is_interactive
|
191
|
-
else:
|
192
|
-
return False
|
193
|
-
|
194
|
-
@property
|
195
|
-
def app_id(self) -> Optional[str]:
|
196
|
-
"""Return the app_id, if the stub is running."""
|
197
|
-
if self._container_app:
|
198
|
-
return self._container_app._app_id
|
199
|
-
elif self._local_app:
|
200
|
-
return self._local_app._app_id
|
201
|
-
else:
|
202
|
-
return None
|
203
|
-
|
204
|
-
@property
|
205
|
-
def description(self) -> Optional[str]:
|
206
|
-
"""The Stub's `name`, if available, or a fallback descriptive identifier."""
|
207
|
-
return self._description
|
208
|
-
|
209
|
-
def set_description(self, description: str):
|
210
|
-
self._description = description
|
211
|
-
|
212
|
-
def _validate_blueprint_value(self, key: str, value: Any):
|
213
|
-
if not isinstance(value, _Object):
|
214
|
-
raise InvalidError(f"Stub attribute {key} with value {value} is not a valid Modal object")
|
215
|
-
|
216
|
-
def _add_object(self, tag, obj):
|
217
|
-
if self._container_app:
|
218
|
-
# If this is inside a container, then objects can be defined after app initialization.
|
219
|
-
# So we may have to initialize objects once they get bound to the stub.
|
220
|
-
if self._container_app._has_object(tag):
|
221
|
-
self._container_app._hydrate_object(obj, tag)
|
222
|
-
|
223
|
-
self._indexed_objects[tag] = obj
|
224
|
-
|
225
|
-
def __getitem__(self, tag: str):
|
226
|
-
"""Stub assignments of the form `stub.x` or `stub["x"]` are deprecated!
|
227
|
-
|
228
|
-
The only use cases for these assignments is in conjunction with `.new()`, which is now
|
229
|
-
in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
|
230
|
-
need to assign those objects to the stub. Example:
|
231
|
-
|
232
|
-
```python
|
233
|
-
d = modal.Dict.from_name("my-dict", create_if_missing=True)
|
234
|
-
|
235
|
-
@stub.function()
|
236
|
-
def f(x, y):
|
237
|
-
d[x] = y # Refer to d in global scope
|
238
|
-
```
|
239
|
-
"""
|
240
|
-
deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
|
241
|
-
return self._indexed_objects[tag]
|
242
|
-
|
243
|
-
def __setitem__(self, tag: str, obj: _Object):
|
244
|
-
deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
|
245
|
-
self._validate_blueprint_value(tag, obj)
|
246
|
-
# Deprecated ?
|
247
|
-
self._add_object(tag, obj)
|
248
|
-
|
249
|
-
def __getattr__(self, tag: str) -> _Object:
|
250
|
-
# TODO(erikbern): remove this method later
|
251
|
-
assert isinstance(tag, str)
|
252
|
-
if tag.startswith("__"):
|
253
|
-
# Hacky way to avoid certain issues, e.g. pickle will try to look this up
|
254
|
-
raise AttributeError(f"Stub has no member {tag}")
|
255
|
-
if tag not in self._indexed_objects:
|
256
|
-
# Primarily to make hasattr work
|
257
|
-
raise AttributeError(f"Stub has no member {tag}")
|
258
|
-
obj: _Object = self._indexed_objects[tag]
|
259
|
-
deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
|
260
|
-
return obj
|
261
|
-
|
262
|
-
def __setattr__(self, tag: str, obj: _Object):
|
263
|
-
# TODO(erikbern): remove this method later
|
264
|
-
# Note that only attributes defined in __annotations__ are set on the object itself,
|
265
|
-
# everything else is registered on the indexed_objects
|
266
|
-
if tag in self.__annotations__:
|
267
|
-
object.__setattr__(self, tag, obj)
|
268
|
-
elif tag == "image":
|
269
|
-
self._indexed_objects["image"] = obj
|
270
|
-
else:
|
271
|
-
self._validate_blueprint_value(tag, obj)
|
272
|
-
deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
|
273
|
-
self._add_object(tag, obj)
|
274
|
-
|
275
|
-
@property
|
276
|
-
def image(self) -> _Image:
|
277
|
-
# Exists to get the type inference working for `stub.image`
|
278
|
-
# Will also keep this one after we remove [get/set][item/attr]
|
279
|
-
return self._indexed_objects["image"]
|
280
|
-
|
281
|
-
@image.setter
|
282
|
-
def image(self, value):
|
283
|
-
self._indexed_objects["image"] = value
|
284
|
-
|
285
|
-
def get_objects(self) -> List[Tuple[str, _Object]]:
|
286
|
-
"""Used by the container app to initialize objects."""
|
287
|
-
return list(self._indexed_objects.items())
|
288
|
-
|
289
|
-
def _uncreate_all_objects(self):
|
290
|
-
# TODO(erikbern): this doesn't unhydrate objects that aren't tagged
|
291
|
-
for obj in self._indexed_objects.values():
|
292
|
-
obj._unhydrate()
|
293
|
-
|
294
|
-
def is_inside(self, image: Optional[_Image] = None):
|
295
|
-
"""Deprecated: use `Image.imports()` instead! Usage:
|
296
|
-
```
|
297
|
-
my_image = modal.Image.debian_slim().pip_install("torch")
|
298
|
-
with my_image.imports():
|
299
|
-
import torch
|
300
|
-
```
|
301
|
-
"""
|
302
|
-
deprecation_error((2023, 11, 8), _Stub.is_inside.__doc__)
|
303
|
-
|
304
|
-
@asynccontextmanager
|
305
|
-
async def _set_local_app(self, app: _LocalApp) -> AsyncGenerator[None, None]:
|
306
|
-
self._local_app = app
|
307
|
-
try:
|
308
|
-
yield
|
309
|
-
finally:
|
310
|
-
self._local_app = None
|
311
|
-
|
312
|
-
@asynccontextmanager
|
313
|
-
async def run(
|
314
|
-
self,
|
315
|
-
client: Optional[_Client] = None,
|
316
|
-
stdout=None,
|
317
|
-
show_progress: bool = True,
|
318
|
-
detach: bool = False,
|
319
|
-
output_mgr: Optional[OutputManager] = None,
|
320
|
-
) -> AsyncGenerator["_Stub", None]:
|
321
|
-
"""Context manager that runs an app on Modal.
|
322
|
-
|
323
|
-
Use this as the main entry point for your Modal application. All calls
|
324
|
-
to Modal functions should be made within the scope of this context
|
325
|
-
manager, and they will correspond to the current app.
|
326
|
-
|
327
|
-
Note that this method used to return a separate "App" object. This is
|
328
|
-
no longer useful since you can use the stub itself for access to all
|
329
|
-
objects. For backwards compatibility reasons, it returns the same stub.
|
330
|
-
"""
|
331
|
-
# TODO(erikbern): deprecate this one too?
|
332
|
-
async with _run_stub(self, client, stdout, show_progress, detach, output_mgr):
|
333
|
-
yield self
|
334
|
-
|
335
|
-
def _get_default_image(self):
|
336
|
-
if "image" in self._indexed_objects:
|
337
|
-
return self._indexed_objects["image"]
|
338
|
-
else:
|
339
|
-
return _default_image
|
340
|
-
|
341
|
-
def _get_watch_mounts(self):
|
342
|
-
all_mounts = [
|
343
|
-
*self._mounts,
|
344
|
-
]
|
345
|
-
for function in self.registered_functions.values():
|
346
|
-
all_mounts.extend(function._all_mounts)
|
347
|
-
|
348
|
-
return [m for m in all_mounts if m.is_local()]
|
349
|
-
|
350
|
-
def _add_function(self, function: _Function):
|
351
|
-
if function.tag in self._indexed_objects:
|
352
|
-
old_function = self._indexed_objects[function.tag]
|
353
|
-
if isinstance(old_function, _Function):
|
354
|
-
if not is_notebook():
|
355
|
-
logger.warning(
|
356
|
-
f"Warning: Tag '{function.tag}' collision!"
|
357
|
-
f" Overriding existing function [{old_function._info.module_name}].{old_function._info.function_name}"
|
358
|
-
f" with new function [{function._info.module_name}].{function._info.function_name}"
|
359
|
-
)
|
360
|
-
else:
|
361
|
-
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
362
|
-
|
363
|
-
self._add_object(function.tag, function)
|
364
|
-
|
365
|
-
@property
|
366
|
-
def registered_functions(self) -> Dict[str, _Function]:
|
367
|
-
"""All modal.Function objects registered on the stub."""
|
368
|
-
return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Function)}
|
369
|
-
|
370
|
-
@property
|
371
|
-
def registered_classes(self) -> Dict[str, _Function]:
|
372
|
-
"""All modal.Cls objects registered on the stub."""
|
373
|
-
return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Cls)}
|
374
|
-
|
375
|
-
@property
|
376
|
-
def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
|
377
|
-
"""All local CLI entrypoints registered on the stub."""
|
378
|
-
return self._local_entrypoints
|
379
|
-
|
380
|
-
@property
|
381
|
-
def indexed_objects(self) -> Dict[str, _Object]:
|
382
|
-
return self._indexed_objects
|
383
|
-
|
384
|
-
@property
|
385
|
-
def registered_web_endpoints(self) -> List[str]:
|
386
|
-
"""Names of web endpoint (ie. webhook) functions registered on the stub."""
|
387
|
-
return self._web_endpoints
|
388
|
-
|
389
|
-
def local_entrypoint(
|
390
|
-
self, _warn_parentheses_missing=None, *, name: Optional[str] = None
|
391
|
-
) -> Callable[[Callable[..., Any]], None]:
|
392
|
-
"""Decorate a function to be used as a CLI entrypoint for a Modal App.
|
393
|
-
|
394
|
-
These functions can be used to define code that runs locally to set up the app,
|
395
|
-
and act as an entrypoint to start Modal functions from. Note that regular
|
396
|
-
Modal functions can also be used as CLI entrypoints, but unlike `local_entrypoint`,
|
397
|
-
those functions are executed remotely directly.
|
398
|
-
|
399
|
-
**Example**
|
400
|
-
|
401
|
-
```python
|
402
|
-
@stub.local_entrypoint()
|
403
|
-
def main():
|
404
|
-
some_modal_function.remote()
|
405
|
-
```
|
406
|
-
|
407
|
-
You can call the function using `modal run` directly from the CLI:
|
408
|
-
|
409
|
-
```shell
|
410
|
-
modal run stub_module.py
|
411
|
-
```
|
412
|
-
|
413
|
-
Note that an explicit [`stub.run()`](/docs/reference/modal.Stub#run) is not needed, as an
|
414
|
-
[app](/docs/guide/apps) is automatically created for you.
|
415
|
-
|
416
|
-
**Multiple Entrypoints**
|
417
|
-
|
418
|
-
If you have multiple `local_entrypoint` functions, you can qualify the name of your stub and function:
|
419
|
-
|
420
|
-
```shell
|
421
|
-
modal run stub_module.py::stub.some_other_function
|
422
|
-
```
|
423
|
-
|
424
|
-
**Parsing Arguments**
|
425
|
-
|
426
|
-
If your entrypoint function take arguments with primitive types, `modal run` automatically parses them as
|
427
|
-
CLI options. For example, the following function can be called with `modal run stub_module.py --foo 1 --bar "hello"`:
|
428
|
-
|
429
|
-
```python
|
430
|
-
@stub.local_entrypoint()
|
431
|
-
def main(foo: int, bar: str):
|
432
|
-
some_modal_function.call(foo, bar)
|
433
|
-
```
|
434
|
-
|
435
|
-
Currently, `str`, `int`, `float`, `bool`, and `datetime.datetime` are supported. Use `modal run stub_module.py --help` for more
|
436
|
-
information on usage.
|
437
|
-
|
438
|
-
"""
|
439
|
-
if _warn_parentheses_missing:
|
440
|
-
raise InvalidError("Did you forget parentheses? Suggestion: `@stub.local_entrypoint()`.")
|
441
|
-
if name is not None and not isinstance(name, str):
|
442
|
-
raise InvalidError("Invalid value for `name`: Must be string.")
|
443
|
-
|
444
|
-
def wrapped(raw_f: Callable[..., Any]) -> None:
|
445
|
-
info = FunctionInfo(raw_f)
|
446
|
-
tag = name if name is not None else raw_f.__qualname__
|
447
|
-
if tag in self._local_entrypoints:
|
448
|
-
# TODO: get rid of this limitation.
|
449
|
-
raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
|
450
|
-
entrypoint = self._local_entrypoints[tag] = _LocalEntrypoint(info, self)
|
451
|
-
return entrypoint
|
452
|
-
|
453
|
-
return wrapped
|
454
|
-
|
455
|
-
def function(
|
456
|
-
self,
|
457
|
-
_warn_parentheses_missing=None,
|
458
|
-
*,
|
459
|
-
image: Optional[_Image] = None, # The image to run as the container for the function
|
460
|
-
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
461
|
-
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
462
|
-
gpu: GPU_T = None, # GPU specification as string ("any", "T4", "A10G", ...) or object (`modal.GPU.A100()`, ...)
|
463
|
-
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
464
|
-
mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
|
465
|
-
network_file_systems: Dict[
|
466
|
-
Union[str, PurePosixPath], _NetworkFileSystem
|
467
|
-
] = {}, # Mountpoints for Modal NetworkFileSystems
|
468
|
-
volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # Mountpoints for Modal Volumes
|
469
|
-
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
470
|
-
cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
|
471
|
-
memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
|
472
|
-
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
473
|
-
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
474
|
-
concurrency_limit: Optional[
|
475
|
-
int
|
476
|
-
] = None, # An optional maximum number of concurrent containers running the function (use keep_warm for minimum).
|
477
|
-
allow_concurrent_inputs: Optional[int] = None, # Number of inputs the container may fetch to run concurrently.
|
478
|
-
container_idle_timeout: Optional[int] = None, # Timeout for idle containers waiting for inputs to shut down.
|
479
|
-
timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
|
480
|
-
keep_warm: Optional[
|
481
|
-
int
|
482
|
-
] = None, # An optional minimum number of containers to always keep warm (use concurrency_limit for maximum).
|
483
|
-
name: Optional[str] = None, # Sets the Modal name of the function within the stub
|
484
|
-
is_generator: Optional[
|
485
|
-
bool
|
486
|
-
] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
|
487
|
-
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
488
|
-
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
489
|
-
checkpointing_enabled: Optional[bool] = None, # Deprecated
|
490
|
-
block_network: bool = False, # Whether to block network access
|
491
|
-
max_inputs: Optional[
|
492
|
-
int
|
493
|
-
] = None, # Maximum number of inputs a container should handle before shutting down. With `max_inputs = 1`, containers will be single-use.
|
494
|
-
# The next group of parameters are deprecated; do not use in any new code
|
495
|
-
interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
|
496
|
-
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
|
497
|
-
# Parameters below here are experimental. Use with caution!
|
498
|
-
_allow_background_volume_commits: bool = False, # Experimental flag
|
499
|
-
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
|
500
|
-
_experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
|
501
|
-
_experimental_scheduler_placement: Optional[
|
502
|
-
SchedulerPlacement
|
503
|
-
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
504
|
-
) -> Callable[..., _Function]:
|
505
|
-
"""Decorator to register a new Modal function with this stub."""
|
506
|
-
if isinstance(_warn_parentheses_missing, _Image):
|
507
|
-
# Handle edge case where maybe (?) some users passed image as a positional arg
|
508
|
-
raise InvalidError("`image` needs to be a keyword argument: `@stub.function(image=image)`.")
|
509
|
-
if _warn_parentheses_missing:
|
510
|
-
raise InvalidError("Did you forget parentheses? Suggestion: `@stub.function()`.")
|
511
|
-
|
512
|
-
if interactive:
|
513
|
-
deprecation_error(
|
514
|
-
(2024, 2, 29), "interactive=True has been deprecated. Set MODAL_INTERACTIVE_FUNCTIONS=1 instead."
|
515
|
-
)
|
516
|
-
|
517
|
-
if image is None:
|
518
|
-
image = self._get_default_image()
|
519
|
-
|
520
|
-
secrets = [*self._secrets, *secrets]
|
521
|
-
|
522
|
-
def wrapped(
|
523
|
-
f: Union[_PartialFunction, Callable[..., Any]],
|
524
|
-
_cls: Optional[type] = None, # Used for methods only
|
525
|
-
) -> _Function:
|
526
|
-
nonlocal keep_warm, is_generator
|
527
|
-
|
528
|
-
if isinstance(f, _PartialFunction):
|
529
|
-
f.wrapped = True
|
530
|
-
info = FunctionInfo(f.raw_f, serialized=serialized, name_override=name, cls=_cls)
|
531
|
-
raw_f = f.raw_f
|
532
|
-
webhook_config = f.webhook_config
|
533
|
-
is_generator = f.is_generator
|
534
|
-
keep_warm = f.keep_warm or keep_warm
|
535
|
-
|
536
|
-
if webhook_config:
|
537
|
-
if interactive:
|
538
|
-
raise InvalidError("interactive=True is not supported with web endpoint functions")
|
539
|
-
self._web_endpoints.append(info.get_tag())
|
540
|
-
else:
|
541
|
-
info = FunctionInfo(f, serialized=serialized, name_override=name, cls=_cls)
|
542
|
-
webhook_config = None
|
543
|
-
raw_f = f
|
544
|
-
|
545
|
-
if not _cls and not info.is_serialized() and "." in info.function_name: # This is a method
|
546
|
-
raise InvalidError(
|
547
|
-
"`stub.function` on methods is not allowed. See https://modal.com/docs/guide/lifecycle-functions instead"
|
548
|
-
)
|
549
|
-
|
550
|
-
if is_generator is None:
|
551
|
-
is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
|
552
|
-
|
553
|
-
function = _Function.from_args(
|
554
|
-
info,
|
555
|
-
stub=self,
|
556
|
-
image=image,
|
557
|
-
secret=secret,
|
558
|
-
secrets=secrets,
|
559
|
-
schedule=schedule,
|
560
|
-
is_generator=is_generator,
|
561
|
-
gpu=gpu,
|
562
|
-
mounts=[*self._mounts, *mounts],
|
563
|
-
network_file_systems=network_file_systems,
|
564
|
-
allow_cross_region_volumes=allow_cross_region_volumes,
|
565
|
-
volumes={**self._volumes, **volumes},
|
566
|
-
memory=memory,
|
567
|
-
proxy=proxy,
|
568
|
-
retries=retries,
|
569
|
-
concurrency_limit=concurrency_limit,
|
570
|
-
allow_concurrent_inputs=allow_concurrent_inputs,
|
571
|
-
container_idle_timeout=container_idle_timeout,
|
572
|
-
timeout=timeout,
|
573
|
-
cpu=cpu,
|
574
|
-
keep_warm=keep_warm,
|
575
|
-
cloud=cloud,
|
576
|
-
webhook_config=webhook_config,
|
577
|
-
enable_memory_snapshot=enable_memory_snapshot,
|
578
|
-
checkpointing_enabled=checkpointing_enabled,
|
579
|
-
allow_background_volume_commits=_allow_background_volume_commits,
|
580
|
-
block_network=block_network,
|
581
|
-
max_inputs=max_inputs,
|
582
|
-
_experimental_boost=_experimental_boost,
|
583
|
-
_experimental_scheduler=_experimental_scheduler,
|
584
|
-
_experimental_scheduler_placement=_experimental_scheduler_placement,
|
585
|
-
)
|
586
|
-
|
587
|
-
self._add_function(function)
|
588
|
-
return function
|
589
|
-
|
590
|
-
return wrapped
|
591
|
-
|
592
|
-
def cls(
|
593
|
-
self,
|
594
|
-
_warn_parentheses_missing=None,
|
595
|
-
*,
|
596
|
-
image: Optional[_Image] = None, # The image to run as the container for the function
|
597
|
-
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
598
|
-
gpu: GPU_T = None, # GPU specification as string ("any", "T4", "A10G", ...) or object (`modal.GPU.A100()`, ...)
|
599
|
-
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
600
|
-
mounts: Sequence[_Mount] = (),
|
601
|
-
network_file_systems: Dict[
|
602
|
-
Union[str, PurePosixPath], _NetworkFileSystem
|
603
|
-
] = {}, # Mountpoints for Modal NetworkFileSystems
|
604
|
-
volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # Mountpoints for Modal Volumes
|
605
|
-
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
606
|
-
cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
|
607
|
-
memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
|
608
|
-
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
609
|
-
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
610
|
-
concurrency_limit: Optional[int] = None, # Limit for max concurrent containers running the function.
|
611
|
-
allow_concurrent_inputs: Optional[int] = None, # Number of inputs the container may fetch to run concurrently.
|
612
|
-
container_idle_timeout: Optional[int] = None, # Timeout for idle containers waiting for inputs to shut down.
|
613
|
-
timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
|
614
|
-
keep_warm: Optional[int] = None, # An optional number of containers to always keep warm.
|
615
|
-
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
616
|
-
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
617
|
-
checkpointing_enabled: Optional[bool] = None, # Deprecated
|
618
|
-
block_network: bool = False, # Whether to block network access
|
619
|
-
_allow_background_volume_commits: bool = False,
|
620
|
-
max_inputs: Optional[
|
621
|
-
int
|
622
|
-
] = None, # Limits the number of inputs a container handles before shutting down. Use `max_inputs = 1` for single-use containers.
|
623
|
-
# The next group of parameters are deprecated; do not use in any new code
|
624
|
-
interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
|
625
|
-
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
|
626
|
-
# Parameters below here are experimental. Use with caution!
|
627
|
-
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
|
628
|
-
_experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
|
629
|
-
_experimental_scheduler_placement: Optional[
|
630
|
-
SchedulerPlacement
|
631
|
-
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
632
|
-
) -> Callable[[CLS_T], _Cls]:
|
633
|
-
if _warn_parentheses_missing:
|
634
|
-
raise InvalidError("Did you forget parentheses? Suggestion: `@stub.cls()`.")
|
635
|
-
|
636
|
-
decorator: Callable[[PartialFunction, type], _Function] = self.function(
|
637
|
-
image=image,
|
638
|
-
secret=secret,
|
639
|
-
secrets=secrets,
|
640
|
-
gpu=gpu,
|
641
|
-
serialized=serialized,
|
642
|
-
mounts=mounts,
|
643
|
-
network_file_systems=network_file_systems,
|
644
|
-
allow_cross_region_volumes=allow_cross_region_volumes,
|
645
|
-
volumes=volumes,
|
646
|
-
cpu=cpu,
|
647
|
-
memory=memory,
|
648
|
-
proxy=proxy,
|
649
|
-
retries=retries,
|
650
|
-
concurrency_limit=concurrency_limit,
|
651
|
-
allow_concurrent_inputs=allow_concurrent_inputs,
|
652
|
-
container_idle_timeout=container_idle_timeout,
|
653
|
-
timeout=timeout,
|
654
|
-
interactive=interactive,
|
655
|
-
keep_warm=keep_warm,
|
656
|
-
cloud=cloud,
|
657
|
-
enable_memory_snapshot=enable_memory_snapshot,
|
658
|
-
checkpointing_enabled=checkpointing_enabled,
|
659
|
-
block_network=block_network,
|
660
|
-
_allow_background_volume_commits=_allow_background_volume_commits,
|
661
|
-
max_inputs=max_inputs,
|
662
|
-
_experimental_boost=_experimental_boost,
|
663
|
-
_experimental_scheduler=_experimental_scheduler,
|
664
|
-
_experimental_scheduler_placement=_experimental_scheduler_placement,
|
665
|
-
)
|
666
|
-
|
667
|
-
def wrapper(user_cls: CLS_T) -> _Cls:
|
668
|
-
cls: _Cls = _Cls.from_local(user_cls, self, decorator)
|
669
|
-
|
670
|
-
if (
|
671
|
-
_find_callables_for_cls(user_cls, _PartialFunctionFlags.ENTER_PRE_CHECKPOINT)
|
672
|
-
and not enable_memory_snapshot
|
673
|
-
):
|
674
|
-
raise InvalidError("A class must have `enable_memory_snapshot=True` to use `snap=True` on its methods.")
|
675
|
-
|
676
|
-
if len(cls._functions) > 1 and keep_warm is not None:
|
677
|
-
deprecation_warning(
|
678
|
-
(2023, 10, 20),
|
679
|
-
"`@stub.cls(keep_warm=...)` is deprecated when there is more than 1 method."
|
680
|
-
" Use `@method(keep_warm=...)` on each method instead!",
|
681
|
-
)
|
682
|
-
|
683
|
-
tag: str = user_cls.__name__
|
684
|
-
self._add_object(tag, cls)
|
685
|
-
return cls
|
686
|
-
|
687
|
-
return wrapper
|
688
|
-
|
689
|
-
async def spawn_sandbox(
|
690
|
-
self,
|
691
|
-
*entrypoint_args: str,
|
692
|
-
image: Optional[_Image] = None, # The image to run as the container for the sandbox.
|
693
|
-
mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
|
694
|
-
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
695
|
-
network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
696
|
-
timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
|
697
|
-
workdir: Optional[str] = None, # Working directory of the sandbox.
|
698
|
-
gpu: GPU_T = None,
|
699
|
-
cloud: Optional[str] = None,
|
700
|
-
cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
|
701
|
-
memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
|
702
|
-
block_network: bool = False, # Whether to block network access
|
703
|
-
volumes: Dict[Union[str, os.PathLike], _Volume] = {}, # Volumes to mount in the sandbox.
|
704
|
-
_allow_background_volume_commits: bool = False,
|
705
|
-
pty_info: Optional[api_pb2.PTYInfo] = None,
|
706
|
-
) -> _Sandbox:
|
707
|
-
"""Sandboxes are a way to run arbitrary commands in dynamically defined environments.
|
708
|
-
|
709
|
-
This function returns a [SandboxHandle](/docs/reference/modal.Sandbox#modalsandboxsandbox), which can be used to interact with the running sandbox.
|
710
|
-
|
711
|
-
Refer to the [docs](/docs/guide/sandbox) on how to spawn and use sandboxes.
|
712
|
-
"""
|
713
|
-
from .sandbox import _Sandbox
|
714
|
-
from .stub import _default_image
|
715
|
-
|
716
|
-
if self._local_app:
|
717
|
-
app_id = self._local_app.app_id
|
718
|
-
environment_name = self._local_app._environment_name
|
719
|
-
client = self._local_app.client
|
720
|
-
elif self._container_app:
|
721
|
-
app_id = self._container_app.app_id
|
722
|
-
environment_name = self._container_app._environment_name
|
723
|
-
client = self._container_app.client
|
724
|
-
else:
|
725
|
-
raise InvalidError("`stub.spawn_sandbox` requires a running app.")
|
726
|
-
|
727
|
-
# TODO(erikbern): pulling a lot of app internals here, let's clean up shortly
|
728
|
-
resolver = Resolver(client, environment_name=environment_name, app_id=app_id)
|
729
|
-
obj = _Sandbox._new(
|
730
|
-
entrypoint_args,
|
731
|
-
image=image or _default_image,
|
732
|
-
mounts=mounts,
|
733
|
-
secrets=secrets,
|
734
|
-
timeout=timeout,
|
735
|
-
workdir=workdir,
|
736
|
-
gpu=gpu,
|
737
|
-
cloud=cloud,
|
738
|
-
cpu=cpu,
|
739
|
-
memory=memory,
|
740
|
-
network_file_systems=network_file_systems,
|
741
|
-
block_network=block_network,
|
742
|
-
volumes=volumes,
|
743
|
-
allow_background_volume_commits=_allow_background_volume_commits,
|
744
|
-
pty_info=pty_info,
|
745
|
-
)
|
746
|
-
await resolver.load(obj)
|
747
|
-
return obj
|
748
|
-
|
749
|
-
def include(self, /, other_stub: "_Stub"):
|
750
|
-
"""Include another stub's objects in this one.
|
751
|
-
|
752
|
-
Useful splitting up Modal apps across different self-contained files
|
753
|
-
|
754
|
-
```python
|
755
|
-
stub_a = modal.Stub("a")
|
756
|
-
@stub.function()
|
757
|
-
def foo():
|
758
|
-
...
|
759
|
-
|
760
|
-
stub_b = modal.Stub("b")
|
761
|
-
@stub.function()
|
762
|
-
def bar():
|
763
|
-
...
|
764
|
-
|
765
|
-
stub_a.include(stub_b)
|
766
|
-
|
767
|
-
@stub_a.local_entrypoint()
|
768
|
-
def main():
|
769
|
-
# use function declared on the included stub
|
770
|
-
bar.remote()
|
771
|
-
```
|
772
|
-
"""
|
773
|
-
for tag, object in other_stub._indexed_objects.items():
|
774
|
-
existing_object = self._indexed_objects.get(tag)
|
775
|
-
if existing_object and existing_object != object:
|
776
|
-
logger.warning(
|
777
|
-
f"Named app object {tag} with existing value {existing_object} is being overwritten by a different object {object}"
|
778
|
-
)
|
779
|
-
|
780
|
-
self._add_object(tag, object)
|
781
|
-
|
782
|
-
|
783
|
-
Stub = synchronize_api(_Stub)
|