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/network_file_system.pyi
CHANGED
@@ -1,165 +1,207 @@
|
|
1
|
+
import collections.abc
|
1
2
|
import modal.client
|
2
3
|
import modal.object
|
4
|
+
import modal.volume
|
3
5
|
import modal_proto.api_pb2
|
4
6
|
import pathlib
|
5
7
|
import synchronicity.combined_types
|
6
8
|
import typing
|
7
9
|
import typing_extensions
|
8
10
|
|
9
|
-
def network_file_system_mount_protos(
|
10
|
-
|
11
|
-
|
11
|
+
def network_file_system_mount_protos(
|
12
|
+
validated_network_file_systems: list[tuple[str, _NetworkFileSystem]], allow_cross_region_volumes: bool
|
13
|
+
) -> list[modal_proto.api_pb2.SharedVolumeMount]: ...
|
12
14
|
|
13
15
|
class _NetworkFileSystem(modal.object._Object):
|
14
16
|
@staticmethod
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
@staticmethod
|
19
|
-
def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> _NetworkFileSystem:
|
20
|
-
...
|
21
|
-
|
17
|
+
def from_name(
|
18
|
+
name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
|
19
|
+
) -> _NetworkFileSystem: ...
|
22
20
|
@classmethod
|
23
|
-
def ephemeral(
|
24
|
-
|
25
|
-
|
21
|
+
def ephemeral(
|
22
|
+
cls: type[_NetworkFileSystem],
|
23
|
+
client: typing.Optional[modal.client._Client] = None,
|
24
|
+
environment_name: typing.Optional[str] = None,
|
25
|
+
_heartbeat_sleep: float = 300,
|
26
|
+
) -> typing.AsyncContextManager[_NetworkFileSystem]: ...
|
26
27
|
@staticmethod
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
async def lookup(
|
29
|
+
name: str,
|
30
|
+
namespace=1,
|
31
|
+
client: typing.Optional[modal.client._Client] = None,
|
32
|
+
environment_name: typing.Optional[str] = None,
|
33
|
+
create_if_missing: bool = False,
|
34
|
+
) -> _NetworkFileSystem: ...
|
33
35
|
@staticmethod
|
34
|
-
async def
|
35
|
-
|
36
|
-
|
36
|
+
async def create_deployed(
|
37
|
+
deployment_name: str,
|
38
|
+
namespace=1,
|
39
|
+
client: typing.Optional[modal.client._Client] = None,
|
40
|
+
environment_name: typing.Optional[str] = None,
|
41
|
+
) -> str: ...
|
37
42
|
@staticmethod
|
38
|
-
async def
|
39
|
-
|
40
|
-
|
41
|
-
async def write_file(
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
43
|
+
async def delete(
|
44
|
+
name: str, client: typing.Optional[modal.client._Client] = None, environment_name: typing.Optional[str] = None
|
45
|
+
): ...
|
46
|
+
async def write_file(
|
47
|
+
self,
|
48
|
+
remote_path: str,
|
49
|
+
fp: typing.BinaryIO,
|
50
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
51
|
+
) -> int: ...
|
52
|
+
def read_file(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
|
53
|
+
def iterdir(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
|
54
|
+
async def add_local_file(
|
55
|
+
self,
|
56
|
+
local_path: typing.Union[pathlib.Path, str],
|
57
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
58
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
59
|
+
): ...
|
60
|
+
async def add_local_dir(
|
61
|
+
self,
|
62
|
+
local_path: typing.Union[pathlib.Path, str],
|
63
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
64
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
65
|
+
): ...
|
66
|
+
async def listdir(self, path: str) -> list[modal.volume.FileEntry]: ...
|
67
|
+
async def remove_file(self, path: str, recursive=False): ...
|
62
68
|
|
63
69
|
class NetworkFileSystem(modal.object.Object):
|
64
|
-
def __init__(self, *args, **kwargs):
|
65
|
-
...
|
66
|
-
|
70
|
+
def __init__(self, *args, **kwargs): ...
|
67
71
|
@staticmethod
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
@staticmethod
|
72
|
-
def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> NetworkFileSystem:
|
73
|
-
...
|
74
|
-
|
72
|
+
def from_name(
|
73
|
+
name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
|
74
|
+
) -> NetworkFileSystem: ...
|
75
75
|
@classmethod
|
76
|
-
def ephemeral(
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
def persist(self, label: str, namespace=1, environment_name: typing.Union[str, None] = None, cloud: typing.Union[str, None] = None):
|
84
|
-
...
|
76
|
+
def ephemeral(
|
77
|
+
cls: type[NetworkFileSystem],
|
78
|
+
client: typing.Optional[modal.client.Client] = None,
|
79
|
+
environment_name: typing.Optional[str] = None,
|
80
|
+
_heartbeat_sleep: float = 300,
|
81
|
+
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]: ...
|
85
82
|
|
86
83
|
class __lookup_spec(typing_extensions.Protocol):
|
87
|
-
def __call__(
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
84
|
+
def __call__(
|
85
|
+
self,
|
86
|
+
name: str,
|
87
|
+
namespace=1,
|
88
|
+
client: typing.Optional[modal.client.Client] = None,
|
89
|
+
environment_name: typing.Optional[str] = None,
|
90
|
+
create_if_missing: bool = False,
|
91
|
+
) -> NetworkFileSystem: ...
|
92
|
+
async def aio(
|
93
|
+
self,
|
94
|
+
name: str,
|
95
|
+
namespace=1,
|
96
|
+
client: typing.Optional[modal.client.Client] = None,
|
97
|
+
environment_name: typing.Optional[str] = None,
|
98
|
+
create_if_missing: bool = False,
|
99
|
+
) -> NetworkFileSystem: ...
|
92
100
|
|
93
101
|
lookup: __lookup_spec
|
94
102
|
|
95
103
|
class __create_deployed_spec(typing_extensions.Protocol):
|
96
|
-
def __call__(
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
104
|
+
def __call__(
|
105
|
+
self,
|
106
|
+
deployment_name: str,
|
107
|
+
namespace=1,
|
108
|
+
client: typing.Optional[modal.client.Client] = None,
|
109
|
+
environment_name: typing.Optional[str] = None,
|
110
|
+
) -> str: ...
|
111
|
+
async def aio(
|
112
|
+
self,
|
113
|
+
deployment_name: str,
|
114
|
+
namespace=1,
|
115
|
+
client: typing.Optional[modal.client.Client] = None,
|
116
|
+
environment_name: typing.Optional[str] = None,
|
117
|
+
) -> str: ...
|
101
118
|
|
102
119
|
create_deployed: __create_deployed_spec
|
103
120
|
|
104
|
-
class
|
105
|
-
def __call__(
|
106
|
-
|
121
|
+
class __delete_spec(typing_extensions.Protocol):
|
122
|
+
def __call__(
|
123
|
+
self,
|
124
|
+
name: str,
|
125
|
+
client: typing.Optional[modal.client.Client] = None,
|
126
|
+
environment_name: typing.Optional[str] = None,
|
127
|
+
): ...
|
128
|
+
async def aio(
|
129
|
+
self,
|
130
|
+
name: str,
|
131
|
+
client: typing.Optional[modal.client.Client] = None,
|
132
|
+
environment_name: typing.Optional[str] = None,
|
133
|
+
): ...
|
134
|
+
|
135
|
+
delete: __delete_spec
|
107
136
|
|
108
|
-
|
109
|
-
|
137
|
+
class __write_file_spec(typing_extensions.Protocol):
|
138
|
+
def __call__(
|
139
|
+
self,
|
140
|
+
remote_path: str,
|
141
|
+
fp: typing.BinaryIO,
|
142
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
143
|
+
) -> int: ...
|
144
|
+
async def aio(
|
145
|
+
self,
|
146
|
+
remote_path: str,
|
147
|
+
fp: typing.BinaryIO,
|
148
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
149
|
+
) -> int: ...
|
110
150
|
|
111
151
|
write_file: __write_file_spec
|
112
152
|
|
113
153
|
class __read_file_spec(typing_extensions.Protocol):
|
114
|
-
def __call__(self, path: str) -> typing.Iterator[bytes]:
|
115
|
-
|
116
|
-
|
117
|
-
def aio(self, path: str) -> typing.AsyncIterator[bytes]:
|
118
|
-
...
|
154
|
+
def __call__(self, path: str) -> typing.Iterator[bytes]: ...
|
155
|
+
def aio(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
|
119
156
|
|
120
157
|
read_file: __read_file_spec
|
121
158
|
|
122
159
|
class __iterdir_spec(typing_extensions.Protocol):
|
123
|
-
def __call__(self, path: str) -> typing.Iterator[
|
124
|
-
|
125
|
-
|
126
|
-
def aio(self, path: str) -> typing.AsyncIterator[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
|
127
|
-
...
|
160
|
+
def __call__(self, path: str) -> typing.Iterator[modal.volume.FileEntry]: ...
|
161
|
+
def aio(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
|
128
162
|
|
129
163
|
iterdir: __iterdir_spec
|
130
164
|
|
131
165
|
class __add_local_file_spec(typing_extensions.Protocol):
|
132
|
-
def __call__(
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
166
|
+
def __call__(
|
167
|
+
self,
|
168
|
+
local_path: typing.Union[pathlib.Path, str],
|
169
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
170
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
171
|
+
): ...
|
172
|
+
async def aio(
|
173
|
+
self,
|
174
|
+
local_path: typing.Union[pathlib.Path, str],
|
175
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
176
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
177
|
+
): ...
|
137
178
|
|
138
179
|
add_local_file: __add_local_file_spec
|
139
180
|
|
140
181
|
class __add_local_dir_spec(typing_extensions.Protocol):
|
141
|
-
def __call__(
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
182
|
+
def __call__(
|
183
|
+
self,
|
184
|
+
local_path: typing.Union[pathlib.Path, str],
|
185
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
186
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
187
|
+
): ...
|
188
|
+
async def aio(
|
189
|
+
self,
|
190
|
+
local_path: typing.Union[pathlib.Path, str],
|
191
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
192
|
+
progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
193
|
+
): ...
|
146
194
|
|
147
195
|
add_local_dir: __add_local_dir_spec
|
148
196
|
|
149
197
|
class __listdir_spec(typing_extensions.Protocol):
|
150
|
-
def __call__(self, path: str) ->
|
151
|
-
|
152
|
-
|
153
|
-
async def aio(self, *args, **kwargs) -> typing.List[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
|
154
|
-
...
|
198
|
+
def __call__(self, path: str) -> list[modal.volume.FileEntry]: ...
|
199
|
+
async def aio(self, path: str) -> list[modal.volume.FileEntry]: ...
|
155
200
|
|
156
201
|
listdir: __listdir_spec
|
157
202
|
|
158
203
|
class __remove_file_spec(typing_extensions.Protocol):
|
159
|
-
def __call__(self, path: str, recursive=False):
|
160
|
-
|
161
|
-
|
162
|
-
async def aio(self, *args, **kwargs):
|
163
|
-
...
|
204
|
+
def __call__(self, path: str, recursive=False): ...
|
205
|
+
async def aio(self, path: str, recursive=False): ...
|
164
206
|
|
165
207
|
remove_file: __remove_file_spec
|
modal/object.py
CHANGED
@@ -1,24 +1,27 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
import uuid
|
3
|
+
from collections.abc import Awaitable, Hashable, Sequence
|
3
4
|
from functools import wraps
|
4
|
-
from typing import
|
5
|
+
from typing import Callable, ClassVar, Optional, TypeVar
|
5
6
|
|
6
7
|
from google.protobuf.message import Message
|
7
8
|
|
9
|
+
from modal._utils.async_utils import aclosing
|
10
|
+
|
8
11
|
from ._resolver import Resolver
|
9
12
|
from ._utils.async_utils import synchronize_api
|
10
13
|
from .client import _Client
|
11
|
-
from .config import config
|
14
|
+
from .config import config, logger
|
12
15
|
from .exception import ExecutionError, InvalidError
|
13
16
|
|
14
17
|
O = TypeVar("O", bound="_Object")
|
15
18
|
|
16
19
|
_BLOCKING_O = synchronize_api(O)
|
17
20
|
|
18
|
-
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP = 300
|
21
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
|
19
22
|
|
20
23
|
|
21
|
-
def _get_environment_name(environment_name: Optional[str], resolver: Optional[Resolver] = None) -> Optional[str]:
|
24
|
+
def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
|
22
25
|
if environment_name:
|
23
26
|
return environment_name
|
24
27
|
elif resolver and resolver.environment_name:
|
@@ -29,7 +32,7 @@ def _get_environment_name(environment_name: Optional[str], resolver: Optional[Re
|
|
29
32
|
|
30
33
|
class _Object:
|
31
34
|
_type_prefix: ClassVar[Optional[str]] = None
|
32
|
-
_prefix_to_type: ClassVar[
|
35
|
+
_prefix_to_type: ClassVar[dict[str, type]] = {}
|
33
36
|
|
34
37
|
# For constructors
|
35
38
|
_load: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]]
|
@@ -37,13 +40,14 @@ class _Object:
|
|
37
40
|
_rep: str
|
38
41
|
_is_another_app: bool
|
39
42
|
_hydrate_lazily: bool
|
40
|
-
_deps: Optional[Callable[...,
|
43
|
+
_deps: Optional[Callable[..., list["_Object"]]]
|
41
44
|
_deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
|
42
45
|
|
43
46
|
# For hydrated objects
|
44
47
|
_object_id: str
|
45
48
|
_client: _Client
|
46
49
|
_is_hydrated: bool
|
50
|
+
_is_rehydrated: bool
|
47
51
|
|
48
52
|
@classmethod
|
49
53
|
def __init_subclass__(cls, type_prefix: Optional[str] = None):
|
@@ -62,7 +66,7 @@ class _Object:
|
|
62
66
|
is_another_app: bool = False,
|
63
67
|
preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
|
64
68
|
hydrate_lazily: bool = False,
|
65
|
-
deps: Optional[Callable[...,
|
69
|
+
deps: Optional[Callable[..., list["_Object"]]] = None,
|
66
70
|
deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
|
67
71
|
):
|
68
72
|
self._local_uuid = str(uuid.uuid4())
|
@@ -77,6 +81,7 @@ class _Object:
|
|
77
81
|
self._object_id = None
|
78
82
|
self._client = None
|
79
83
|
self._is_hydrated = False
|
84
|
+
self._is_rehydrated = False
|
80
85
|
|
81
86
|
self._initialize_from_empty()
|
82
87
|
|
@@ -91,7 +96,9 @@ class _Object:
|
|
91
96
|
|
92
97
|
def _initialize_from_other(self, other):
|
93
98
|
# default implementation, can be overriden in subclasses
|
94
|
-
|
99
|
+
self._object_id = other._object_id
|
100
|
+
self._is_hydrated = other._is_hydrated
|
101
|
+
self._client = other._client
|
95
102
|
|
96
103
|
def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
|
97
104
|
assert isinstance(object_id, str)
|
@@ -116,17 +123,25 @@ class _Object:
|
|
116
123
|
# the object_id is already provided by other means
|
117
124
|
return
|
118
125
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
126
|
+
def _validate_is_hydrated(self: O):
|
127
|
+
if not self._is_hydrated:
|
128
|
+
object_type = self.__class__.__name__.strip("_")
|
129
|
+
if hasattr(self, "_app") and getattr(self._app, "_running_app", "") is None:
|
130
|
+
# The most common cause of this error: e.g., user called a Function without using App.run()
|
131
|
+
reason = ", because the App it is defined on is not running"
|
132
|
+
else:
|
133
|
+
# Technically possible, but with an ambiguous cause.
|
134
|
+
reason = ""
|
135
|
+
raise ExecutionError(
|
136
|
+
f"{object_type} has not been hydrated with the metadata it needs to run on Modal{reason}."
|
137
|
+
)
|
122
138
|
|
123
139
|
def clone(self: O) -> O:
|
124
140
|
"""mdmd:hidden Clone a given hydrated object."""
|
125
141
|
|
126
142
|
# Object to clone must already be hydrated, otherwise from_loader is more suitable.
|
127
|
-
|
128
|
-
|
129
|
-
obj = _Object.__new__(type(self))
|
143
|
+
self._validate_is_hydrated()
|
144
|
+
obj = type(self).__new__(type(self))
|
130
145
|
obj._initialize_from_other(self)
|
131
146
|
return obj
|
132
147
|
|
@@ -138,7 +153,7 @@ class _Object:
|
|
138
153
|
is_another_app: bool = False,
|
139
154
|
preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
|
140
155
|
hydrate_lazily: bool = False,
|
141
|
-
deps: Optional[Callable[...,
|
156
|
+
deps: Optional[Callable[..., Sequence["_Object"]]] = None,
|
142
157
|
deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
|
143
158
|
):
|
144
159
|
# TODO(erikbern): flip the order of the two first arguments
|
@@ -146,26 +161,34 @@ class _Object:
|
|
146
161
|
obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
|
147
162
|
return obj
|
148
163
|
|
164
|
+
@classmethod
|
165
|
+
def _get_type_from_id(cls: type[O], object_id: str) -> type[O]:
|
166
|
+
parts = object_id.split("-")
|
167
|
+
if len(parts) != 2:
|
168
|
+
raise InvalidError(f"Object id {object_id} has no dash in it")
|
169
|
+
prefix = parts[0]
|
170
|
+
if prefix not in cls._prefix_to_type:
|
171
|
+
raise InvalidError(f"Object prefix {prefix} does not correspond to a type")
|
172
|
+
return cls._prefix_to_type[prefix]
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def _is_id_type(cls: type[O], object_id) -> bool:
|
176
|
+
return cls._get_type_from_id(object_id) == cls
|
177
|
+
|
149
178
|
@classmethod
|
150
179
|
def _new_hydrated(
|
151
|
-
cls:
|
180
|
+
cls: type[O], object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
|
152
181
|
) -> O:
|
153
182
|
if cls._type_prefix is not None:
|
154
183
|
# This is called directly on a subclass, e.g. Secret.from_id
|
155
184
|
if not object_id.startswith(cls._type_prefix + "-"):
|
156
185
|
raise InvalidError(f"Object {object_id} does not start with {cls._type_prefix}")
|
157
|
-
|
186
|
+
obj_cls = cls
|
158
187
|
else:
|
159
188
|
# This is called on the base class, e.g. Handle.from_id
|
160
|
-
|
161
|
-
if len(parts) != 2:
|
162
|
-
raise InvalidError(f"Object id {object_id} has no dash in it")
|
163
|
-
prefix = parts[0]
|
164
|
-
if prefix not in cls._prefix_to_type:
|
165
|
-
raise InvalidError(f"Object prefix {prefix} does not correspond to a type")
|
189
|
+
obj_cls = cls._get_type_from_id(object_id)
|
166
190
|
|
167
191
|
# Instantiate provider
|
168
|
-
obj_cls = cls._prefix_to_type[prefix]
|
169
192
|
obj = _Object.__new__(obj_cls)
|
170
193
|
rep = f"Object({object_id})" # TODO(erikbern): dumb
|
171
194
|
obj._init(rep, is_another_app=is_another_app)
|
@@ -185,7 +208,7 @@ class _Object:
|
|
185
208
|
return self._local_uuid
|
186
209
|
|
187
210
|
@property
|
188
|
-
def object_id(self):
|
211
|
+
def object_id(self) -> str:
|
189
212
|
"""mdmd:hidden"""
|
190
213
|
return self._object_id
|
191
214
|
|
@@ -195,23 +218,30 @@ class _Object:
|
|
195
218
|
return self._is_hydrated
|
196
219
|
|
197
220
|
@property
|
198
|
-
def deps(self) -> Callable[...,
|
221
|
+
def deps(self) -> Callable[..., list["_Object"]]:
|
199
222
|
"""mdmd:hidden"""
|
200
223
|
return self._deps if self._deps is not None else lambda: []
|
201
224
|
|
202
|
-
async def resolve(self):
|
225
|
+
async def resolve(self, client: Optional[_Client] = None):
|
203
226
|
"""mdmd:hidden"""
|
204
227
|
if self._is_hydrated:
|
228
|
+
# memory snapshots capture references which must be rehydrated
|
229
|
+
# on restore to handle staleness.
|
230
|
+
if self._client._snapshotted and not self._is_rehydrated:
|
231
|
+
logger.debug(f"rehydrating {self} after snapshot")
|
232
|
+
self._is_hydrated = False # un-hydrate and re-resolve
|
233
|
+
c = client if client is not None else await _Client.from_env()
|
234
|
+
resolver = Resolver(c)
|
235
|
+
await resolver.load(self)
|
236
|
+
self._is_rehydrated = True
|
237
|
+
logger.debug(f"rehydrated {self} with client {id(c)}")
|
205
238
|
return
|
206
239
|
elif not self._hydrate_lazily:
|
207
|
-
|
208
|
-
"Object has not been hydrated and doesn't support lazy hydration."
|
209
|
-
" This might happen if an object is defined on a different stub,"
|
210
|
-
" or if it's on the same stub but it didn't get created because it"
|
211
|
-
" wasn't defined in global scope."
|
212
|
-
)
|
240
|
+
self._validate_is_hydrated()
|
213
241
|
else:
|
214
|
-
|
242
|
+
# TODO: this client and/or resolver can't be changed by a caller to X.from_name()
|
243
|
+
c = client if client is not None else await _Client.from_env()
|
244
|
+
resolver = Resolver(c)
|
215
245
|
await resolver.load(self)
|
216
246
|
|
217
247
|
|
@@ -231,7 +261,8 @@ def live_method_gen(method):
|
|
231
261
|
@wraps(method)
|
232
262
|
async def wrapped(self, *args, **kwargs):
|
233
263
|
await self.resolve()
|
234
|
-
async
|
235
|
-
|
264
|
+
async with aclosing(method(self, *args, **kwargs)) as stream:
|
265
|
+
async for item in stream:
|
266
|
+
yield item
|
236
267
|
|
237
268
|
return wrapped
|