modal 0.72.4__py3-none-any.whl → 0.72.48__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.
Files changed (73) hide show
  1. modal/_container_entrypoint.py +5 -10
  2. modal/_object.py +297 -0
  3. modal/_resolver.py +7 -5
  4. modal/_runtime/container_io_manager.py +0 -11
  5. modal/_runtime/user_code_imports.py +7 -7
  6. modal/_serialization.py +4 -3
  7. modal/_tunnel.py +1 -1
  8. modal/app.py +14 -61
  9. modal/app.pyi +25 -25
  10. modal/cli/app.py +3 -2
  11. modal/cli/container.py +1 -1
  12. modal/cli/import_refs.py +185 -113
  13. modal/cli/launch.py +10 -5
  14. modal/cli/programs/run_jupyter.py +2 -2
  15. modal/cli/programs/vscode.py +3 -3
  16. modal/cli/run.py +134 -68
  17. modal/client.py +1 -0
  18. modal/client.pyi +18 -14
  19. modal/cloud_bucket_mount.py +4 -0
  20. modal/cloud_bucket_mount.pyi +4 -0
  21. modal/cls.py +33 -5
  22. modal/cls.pyi +20 -5
  23. modal/container_process.pyi +8 -6
  24. modal/dict.py +1 -1
  25. modal/dict.pyi +32 -29
  26. modal/environments.py +1 -1
  27. modal/environments.pyi +2 -1
  28. modal/experimental.py +47 -11
  29. modal/experimental.pyi +29 -0
  30. modal/file_io.pyi +30 -28
  31. modal/file_pattern_matcher.py +32 -25
  32. modal/functions.py +31 -23
  33. modal/functions.pyi +57 -50
  34. modal/gpu.py +19 -26
  35. modal/image.py +47 -19
  36. modal/image.pyi +28 -21
  37. modal/io_streams.pyi +14 -12
  38. modal/mount.py +14 -5
  39. modal/mount.pyi +28 -25
  40. modal/network_file_system.py +7 -7
  41. modal/network_file_system.pyi +27 -24
  42. modal/object.py +2 -265
  43. modal/object.pyi +46 -130
  44. modal/parallel_map.py +2 -2
  45. modal/parallel_map.pyi +10 -7
  46. modal/partial_function.py +22 -3
  47. modal/partial_function.pyi +45 -27
  48. modal/proxy.py +1 -1
  49. modal/proxy.pyi +2 -1
  50. modal/queue.py +1 -1
  51. modal/queue.pyi +26 -23
  52. modal/runner.py +14 -3
  53. modal/sandbox.py +11 -7
  54. modal/sandbox.pyi +30 -27
  55. modal/secret.py +1 -1
  56. modal/secret.pyi +2 -1
  57. modal/token_flow.pyi +6 -4
  58. modal/volume.py +1 -1
  59. modal/volume.pyi +36 -33
  60. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/METADATA +2 -2
  61. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/RECORD +73 -71
  62. modal_proto/api.proto +151 -4
  63. modal_proto/api_grpc.py +113 -0
  64. modal_proto/api_pb2.py +998 -795
  65. modal_proto/api_pb2.pyi +430 -11
  66. modal_proto/api_pb2_grpc.py +233 -1
  67. modal_proto/api_pb2_grpc.pyi +75 -3
  68. modal_proto/modal_api_grpc.py +7 -0
  69. modal_version/_version_generated.py +1 -1
  70. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/LICENSE +0 -0
  71. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/WHEEL +0 -0
  72. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/entry_points.txt +0 -0
  73. {modal-0.72.4.dist-info → modal-0.72.48.dist-info}/top_level.txt +0 -0
modal/mount.pyi CHANGED
@@ -1,5 +1,6 @@
1
1
  import collections.abc
2
2
  import google.protobuf.message
3
+ import modal._object
3
4
  import modal._resolver
4
5
  import modal._utils.blob_utils
5
6
  import modal.client
@@ -35,7 +36,7 @@ class _MountFile(_MountEntry):
35
36
  class _MountDir(_MountEntry):
36
37
  local_dir: pathlib.Path
37
38
  remote_path: pathlib.PurePosixPath
38
- ignore: typing.Callable[[pathlib.Path], bool]
39
+ ignore: collections.abc.Callable[[pathlib.Path], bool]
39
40
  recursive: bool
40
41
 
41
42
  def description(self): ...
@@ -46,7 +47,7 @@ class _MountDir(_MountEntry):
46
47
  self,
47
48
  local_dir: pathlib.Path,
48
49
  remote_path: pathlib.PurePosixPath,
49
- ignore: typing.Callable[[pathlib.Path], bool],
50
+ ignore: collections.abc.Callable[[pathlib.Path], bool],
50
51
  recursive: bool,
51
52
  ) -> None: ...
52
53
  def __repr__(self): ...
@@ -58,7 +59,7 @@ def module_mount_ignore_condition(module_base: pathlib.Path): ...
58
59
  class _MountedPythonModule(_MountEntry):
59
60
  module_name: str
60
61
  remote_dir: typing.Union[pathlib.PurePosixPath, str]
61
- ignore: typing.Optional[typing.Callable[[pathlib.Path], bool]]
62
+ ignore: typing.Optional[collections.abc.Callable[[pathlib.Path], bool]]
62
63
 
63
64
  def description(self) -> str: ...
64
65
  def _proxy_entries(self) -> list[_MountEntry]: ...
@@ -69,14 +70,14 @@ class _MountedPythonModule(_MountEntry):
69
70
  self,
70
71
  module_name: str,
71
72
  remote_dir: typing.Union[pathlib.PurePosixPath, str] = "/root",
72
- ignore: typing.Optional[typing.Callable[[pathlib.Path], bool]] = None,
73
+ ignore: typing.Optional[collections.abc.Callable[[pathlib.Path], bool]] = None,
73
74
  ) -> None: ...
74
75
  def __repr__(self): ...
75
76
  def __eq__(self, other): ...
76
77
 
77
78
  class NonLocalMountError(Exception): ...
78
79
 
79
- class _Mount(modal.object._Object):
80
+ class _Mount(modal._object._Object):
80
81
  _entries: typing.Optional[list[_MountEntry]]
81
82
  _deployment_name: typing.Optional[str]
82
83
  _namespace: typing.Optional[int]
@@ -95,14 +96,14 @@ class _Mount(modal.object._Object):
95
96
  def _add_local_dir(
96
97
  local_path: pathlib.Path,
97
98
  remote_path: pathlib.PurePosixPath,
98
- ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
99
+ ignore: collections.abc.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
99
100
  ): ...
100
101
  def add_local_dir(
101
102
  self,
102
103
  local_path: typing.Union[str, pathlib.Path],
103
104
  *,
104
105
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
105
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
106
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
106
107
  recursive: bool = True,
107
108
  ) -> _Mount: ...
108
109
  @staticmethod
@@ -110,7 +111,7 @@ class _Mount(modal.object._Object):
110
111
  local_path: typing.Union[str, pathlib.Path],
111
112
  *,
112
113
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
113
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
114
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
114
115
  recursive: bool = True,
115
116
  ) -> _Mount: ...
116
117
  @staticmethod
@@ -118,7 +119,7 @@ class _Mount(modal.object._Object):
118
119
  local_path: typing.Union[str, pathlib.Path],
119
120
  *,
120
121
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
121
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
122
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
122
123
  recursive: bool = True,
123
124
  ) -> _Mount: ...
124
125
  def add_local_file(
@@ -147,15 +148,15 @@ class _Mount(modal.object._Object):
147
148
  def from_local_python_packages(
148
149
  *module_names: str,
149
150
  remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
150
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
151
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
151
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
152
+ ignore: typing.Union[typing.Sequence[str], collections.abc.Callable[[pathlib.Path], bool], None] = None,
152
153
  ) -> _Mount: ...
153
154
  @staticmethod
154
155
  def _from_local_python_packages(
155
156
  *module_names: str,
156
157
  remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
157
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
158
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
158
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
159
+ ignore: typing.Union[typing.Sequence[str], collections.abc.Callable[[pathlib.Path], bool], None] = None,
159
160
  ) -> _Mount: ...
160
161
  @staticmethod
161
162
  def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> _Mount: ...
@@ -176,6 +177,8 @@ class _Mount(modal.object._Object):
176
177
  ) -> None: ...
177
178
  def _get_metadata(self) -> modal_proto.api_pb2.MountHandleMetadata: ...
178
179
 
180
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
181
+
179
182
  class Mount(modal.object.Object):
180
183
  _entries: typing.Optional[list[_MountEntry]]
181
184
  _deployment_name: typing.Optional[str]
@@ -196,14 +199,14 @@ class Mount(modal.object.Object):
196
199
  def _add_local_dir(
197
200
  local_path: pathlib.Path,
198
201
  remote_path: pathlib.PurePosixPath,
199
- ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
202
+ ignore: collections.abc.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
200
203
  ): ...
201
204
  def add_local_dir(
202
205
  self,
203
206
  local_path: typing.Union[str, pathlib.Path],
204
207
  *,
205
208
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
206
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
209
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
207
210
  recursive: bool = True,
208
211
  ) -> Mount: ...
209
212
  @staticmethod
@@ -211,7 +214,7 @@ class Mount(modal.object.Object):
211
214
  local_path: typing.Union[str, pathlib.Path],
212
215
  *,
213
216
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
214
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
217
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
215
218
  recursive: bool = True,
216
219
  ) -> Mount: ...
217
220
  @staticmethod
@@ -219,7 +222,7 @@ class Mount(modal.object.Object):
219
222
  local_path: typing.Union[str, pathlib.Path],
220
223
  *,
221
224
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
222
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
225
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
223
226
  recursive: bool = True,
224
227
  ) -> Mount: ...
225
228
  def add_local_file(
@@ -248,25 +251,25 @@ class Mount(modal.object.Object):
248
251
 
249
252
  _get_files: ___get_files_spec
250
253
 
251
- class ___load_mount_spec(typing_extensions.Protocol):
254
+ class ___load_mount_spec(typing_extensions.Protocol[SUPERSELF]):
252
255
  def __call__(self, resolver: modal._resolver.Resolver, existing_object_id: typing.Optional[str]): ...
253
256
  async def aio(self, resolver: modal._resolver.Resolver, existing_object_id: typing.Optional[str]): ...
254
257
 
255
- _load_mount: ___load_mount_spec
258
+ _load_mount: ___load_mount_spec[typing_extensions.Self]
256
259
 
257
260
  @staticmethod
258
261
  def from_local_python_packages(
259
262
  *module_names: str,
260
263
  remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
261
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
262
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
264
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
265
+ ignore: typing.Union[typing.Sequence[str], collections.abc.Callable[[pathlib.Path], bool], None] = None,
263
266
  ) -> Mount: ...
264
267
  @staticmethod
265
268
  def _from_local_python_packages(
266
269
  *module_names: str,
267
270
  remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
268
- condition: typing.Optional[typing.Callable[[str], bool]] = None,
269
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
271
+ condition: typing.Optional[collections.abc.Callable[[str], bool]] = None,
272
+ ignore: typing.Union[typing.Sequence[str], collections.abc.Callable[[pathlib.Path], bool], None] = None,
270
273
  ) -> Mount: ...
271
274
  @staticmethod
272
275
  def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> Mount: ...
@@ -279,7 +282,7 @@ class Mount(modal.object.Object):
279
282
  environment_name: typing.Optional[str] = None,
280
283
  ) -> Mount: ...
281
284
 
282
- class ___deploy_spec(typing_extensions.Protocol):
285
+ class ___deploy_spec(typing_extensions.Protocol[SUPERSELF]):
283
286
  def __call__(
284
287
  self,
285
288
  deployment_name: typing.Optional[str] = None,
@@ -295,7 +298,7 @@ class Mount(modal.object.Object):
295
298
  client: typing.Optional[modal.client.Client] = None,
296
299
  ) -> None: ...
297
300
 
298
- _deploy: ___deploy_spec
301
+ _deploy: ___deploy_spec[typing_extensions.Self]
299
302
 
300
303
  def _get_metadata(self) -> modal_proto.api_pb2.MountHandleMetadata: ...
301
304
 
@@ -12,6 +12,13 @@ from synchronicity.async_wrap import asynccontextmanager
12
12
  import modal
13
13
  from modal_proto import api_pb2
14
14
 
15
+ from ._object import (
16
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
17
+ _get_environment_name,
18
+ _Object,
19
+ live_method,
20
+ live_method_gen,
21
+ )
15
22
  from ._resolver import Resolver
16
23
  from ._utils.async_utils import TaskContext, aclosing, async_map, sync_or_async_iter, synchronize_api
17
24
  from ._utils.blob_utils import LARGE_FILE_LIMIT, blob_iter, blob_upload_file
@@ -21,13 +28,6 @@ from ._utils.hash_utils import get_sha256_hex
21
28
  from ._utils.name_utils import check_object_name
22
29
  from .client import _Client
23
30
  from .exception import InvalidError
24
- from .object import (
25
- EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
26
- _get_environment_name,
27
- _Object,
28
- live_method,
29
- live_method_gen,
30
- )
31
31
  from .volume import FileEntry
32
32
 
33
33
  NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT = (
@@ -1,4 +1,5 @@
1
1
  import collections.abc
2
+ import modal._object
2
3
  import modal.client
3
4
  import modal.object
4
5
  import modal.volume
@@ -12,7 +13,7 @@ def network_file_system_mount_protos(
12
13
  validated_network_file_systems: list[tuple[str, _NetworkFileSystem]], allow_cross_region_volumes: bool
13
14
  ) -> list[modal_proto.api_pb2.SharedVolumeMount]: ...
14
15
 
15
- class _NetworkFileSystem(modal.object._Object):
16
+ class _NetworkFileSystem(modal._object._Object):
16
17
  @staticmethod
17
18
  def from_name(
18
19
  name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
@@ -47,7 +48,7 @@ class _NetworkFileSystem(modal.object._Object):
47
48
  self,
48
49
  remote_path: str,
49
50
  fp: typing.BinaryIO,
50
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
51
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
51
52
  ) -> int: ...
52
53
  def read_file(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
53
54
  def iterdir(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
@@ -55,17 +56,19 @@ class _NetworkFileSystem(modal.object._Object):
55
56
  self,
56
57
  local_path: typing.Union[pathlib.Path, str],
57
58
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
58
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
59
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
59
60
  ): ...
60
61
  async def add_local_dir(
61
62
  self,
62
63
  local_path: typing.Union[pathlib.Path, str],
63
64
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
64
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
65
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
65
66
  ): ...
66
67
  async def listdir(self, path: str) -> list[modal.volume.FileEntry]: ...
67
68
  async def remove_file(self, path: str, recursive=False): ...
68
69
 
70
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
71
+
69
72
  class NetworkFileSystem(modal.object.Object):
70
73
  def __init__(self, *args, **kwargs): ...
71
74
  @staticmethod
@@ -134,74 +137,74 @@ class NetworkFileSystem(modal.object.Object):
134
137
 
135
138
  delete: __delete_spec
136
139
 
137
- class __write_file_spec(typing_extensions.Protocol):
140
+ class __write_file_spec(typing_extensions.Protocol[SUPERSELF]):
138
141
  def __call__(
139
142
  self,
140
143
  remote_path: str,
141
144
  fp: typing.BinaryIO,
142
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
145
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
143
146
  ) -> int: ...
144
147
  async def aio(
145
148
  self,
146
149
  remote_path: str,
147
150
  fp: typing.BinaryIO,
148
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
151
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
149
152
  ) -> int: ...
150
153
 
151
- write_file: __write_file_spec
154
+ write_file: __write_file_spec[typing_extensions.Self]
152
155
 
153
- class __read_file_spec(typing_extensions.Protocol):
156
+ class __read_file_spec(typing_extensions.Protocol[SUPERSELF]):
154
157
  def __call__(self, path: str) -> typing.Iterator[bytes]: ...
155
158
  def aio(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
156
159
 
157
- read_file: __read_file_spec
160
+ read_file: __read_file_spec[typing_extensions.Self]
158
161
 
159
- class __iterdir_spec(typing_extensions.Protocol):
162
+ class __iterdir_spec(typing_extensions.Protocol[SUPERSELF]):
160
163
  def __call__(self, path: str) -> typing.Iterator[modal.volume.FileEntry]: ...
161
164
  def aio(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
162
165
 
163
- iterdir: __iterdir_spec
166
+ iterdir: __iterdir_spec[typing_extensions.Self]
164
167
 
165
- class __add_local_file_spec(typing_extensions.Protocol):
168
+ class __add_local_file_spec(typing_extensions.Protocol[SUPERSELF]):
166
169
  def __call__(
167
170
  self,
168
171
  local_path: typing.Union[pathlib.Path, str],
169
172
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
170
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
173
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
171
174
  ): ...
172
175
  async def aio(
173
176
  self,
174
177
  local_path: typing.Union[pathlib.Path, str],
175
178
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
176
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
179
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
177
180
  ): ...
178
181
 
179
- add_local_file: __add_local_file_spec
182
+ add_local_file: __add_local_file_spec[typing_extensions.Self]
180
183
 
181
- class __add_local_dir_spec(typing_extensions.Protocol):
184
+ class __add_local_dir_spec(typing_extensions.Protocol[SUPERSELF]):
182
185
  def __call__(
183
186
  self,
184
187
  local_path: typing.Union[pathlib.Path, str],
185
188
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
186
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
189
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
187
190
  ): ...
188
191
  async def aio(
189
192
  self,
190
193
  local_path: typing.Union[pathlib.Path, str],
191
194
  remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
192
- progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
195
+ progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
193
196
  ): ...
194
197
 
195
- add_local_dir: __add_local_dir_spec
198
+ add_local_dir: __add_local_dir_spec[typing_extensions.Self]
196
199
 
197
- class __listdir_spec(typing_extensions.Protocol):
200
+ class __listdir_spec(typing_extensions.Protocol[SUPERSELF]):
198
201
  def __call__(self, path: str) -> list[modal.volume.FileEntry]: ...
199
202
  async def aio(self, path: str) -> list[modal.volume.FileEntry]: ...
200
203
 
201
- listdir: __listdir_spec
204
+ listdir: __listdir_spec[typing_extensions.Self]
202
205
 
203
- class __remove_file_spec(typing_extensions.Protocol):
206
+ class __remove_file_spec(typing_extensions.Protocol[SUPERSELF]):
204
207
  def __call__(self, path: str, recursive=False): ...
205
208
  async def aio(self, path: str, recursive=False): ...
206
209
 
207
- remove_file: __remove_file_spec
210
+ remove_file: __remove_file_spec[typing_extensions.Self]
modal/object.py CHANGED
@@ -1,268 +1,5 @@
1
- # Copyright Modal Labs 2022
2
- import uuid
3
- from collections.abc import Awaitable, Hashable, Sequence
4
- from functools import wraps
5
- from typing import Callable, ClassVar, Optional, TypeVar
6
-
7
- from google.protobuf.message import Message
8
-
9
- from modal._utils.async_utils import aclosing
10
-
11
- from ._resolver import Resolver
1
+ # Copyright Modal Labs 2025
2
+ from ._object import _Object
12
3
  from ._utils.async_utils import synchronize_api
13
- from .client import _Client
14
- from .config import config, logger
15
- from .exception import ExecutionError, InvalidError
16
-
17
- O = TypeVar("O", bound="_Object")
18
-
19
- _BLOCKING_O = synchronize_api(O)
20
-
21
- EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
22
-
23
-
24
- def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
25
- if environment_name:
26
- return environment_name
27
- elif resolver and resolver.environment_name:
28
- return resolver.environment_name
29
- else:
30
- return config.get("environment")
31
-
32
-
33
- class _Object:
34
- _type_prefix: ClassVar[Optional[str]] = None
35
- _prefix_to_type: ClassVar[dict[str, type]] = {}
36
-
37
- # For constructors
38
- _load: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]]
39
- _preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]]
40
- _rep: str
41
- _is_another_app: bool
42
- _hydrate_lazily: bool
43
- _deps: Optional[Callable[..., list["_Object"]]]
44
- _deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
45
-
46
- # For hydrated objects
47
- _object_id: str
48
- _client: _Client
49
- _is_hydrated: bool
50
- _is_rehydrated: bool
51
-
52
- @classmethod
53
- def __init_subclass__(cls, type_prefix: Optional[str] = None):
54
- super().__init_subclass__()
55
- if type_prefix is not None:
56
- cls._type_prefix = type_prefix
57
- cls._prefix_to_type[type_prefix] = cls
58
-
59
- def __init__(self, *args, **kwargs):
60
- raise InvalidError(f"Class {type(self).__name__} has no constructor. Use class constructor methods instead.")
61
-
62
- def _init(
63
- self,
64
- rep: str,
65
- load: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
66
- is_another_app: bool = False,
67
- preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
68
- hydrate_lazily: bool = False,
69
- deps: Optional[Callable[..., list["_Object"]]] = None,
70
- deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
71
- ):
72
- self._local_uuid = str(uuid.uuid4())
73
- self._load = load
74
- self._preload = preload
75
- self._rep = rep
76
- self._is_another_app = is_another_app
77
- self._hydrate_lazily = hydrate_lazily
78
- self._deps = deps
79
- self._deduplication_key = deduplication_key
80
-
81
- self._object_id = None
82
- self._client = None
83
- self._is_hydrated = False
84
- self._is_rehydrated = False
85
-
86
- self._initialize_from_empty()
87
-
88
- def _unhydrate(self):
89
- self._object_id = None
90
- self._client = None
91
- self._is_hydrated = False
92
-
93
- def _initialize_from_empty(self):
94
- # default implementation, can be overriden in subclasses
95
- pass
96
-
97
- def _initialize_from_other(self, other):
98
- # default implementation, can be overriden in subclasses
99
- self._object_id = other._object_id
100
- self._is_hydrated = other._is_hydrated
101
- self._client = other._client
102
-
103
- def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
104
- assert isinstance(object_id, str)
105
- if not object_id.startswith(self._type_prefix):
106
- raise ExecutionError(
107
- f"Can not hydrate {type(self)}:"
108
- f" it has type prefix {self._type_prefix}"
109
- f" but the object_id starts with {object_id[:3]}"
110
- )
111
- self._object_id = object_id
112
- self._client = client
113
- self._hydrate_metadata(metadata)
114
- self._is_hydrated = True
115
-
116
- def _hydrate_metadata(self, metadata: Optional[Message]):
117
- # override this is subclasses that need additional data (other than an object_id) for a functioning Handle
118
- pass
119
-
120
- def _get_metadata(self) -> Optional[Message]:
121
- # return the necessary metadata from this handle to be able to re-hydrate in another context if one is needed
122
- # used to provide a handle's handle_metadata for serializing/pickling a live handle
123
- # the object_id is already provided by other means
124
- return
125
-
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
- )
138
-
139
- def clone(self: O) -> O:
140
- """mdmd:hidden Clone a given hydrated object."""
141
-
142
- # Object to clone must already be hydrated, otherwise from_loader is more suitable.
143
- self._validate_is_hydrated()
144
- obj = type(self).__new__(type(self))
145
- obj._initialize_from_other(self)
146
- return obj
147
-
148
- @classmethod
149
- def _from_loader(
150
- cls,
151
- load: Callable[[O, Resolver, Optional[str]], Awaitable[None]],
152
- rep: str,
153
- is_another_app: bool = False,
154
- preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
155
- hydrate_lazily: bool = False,
156
- deps: Optional[Callable[..., Sequence["_Object"]]] = None,
157
- deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
158
- ):
159
- # TODO(erikbern): flip the order of the two first arguments
160
- obj = _Object.__new__(cls)
161
- obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
162
- return obj
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
-
178
- @classmethod
179
- def _new_hydrated(
180
- cls: type[O], object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
181
- ) -> O:
182
- if cls._type_prefix is not None:
183
- # This is called directly on a subclass, e.g. Secret.from_id
184
- if not object_id.startswith(cls._type_prefix + "-"):
185
- raise InvalidError(f"Object {object_id} does not start with {cls._type_prefix}")
186
- obj_cls = cls
187
- else:
188
- # This is called on the base class, e.g. Handle.from_id
189
- obj_cls = cls._get_type_from_id(object_id)
190
-
191
- # Instantiate provider
192
- obj = _Object.__new__(obj_cls)
193
- rep = f"Object({object_id})" # TODO(erikbern): dumb
194
- obj._init(rep, is_another_app=is_another_app)
195
- obj._hydrate(object_id, client, handle_metadata)
196
-
197
- return obj
198
-
199
- def _hydrate_from_other(self, other: O):
200
- self._hydrate(other._object_id, other._client, other._get_metadata())
201
-
202
- def __repr__(self):
203
- return self._rep
204
-
205
- @property
206
- def local_uuid(self):
207
- """mdmd:hidden"""
208
- return self._local_uuid
209
-
210
- @property
211
- def object_id(self) -> str:
212
- """mdmd:hidden"""
213
- return self._object_id
214
-
215
- @property
216
- def is_hydrated(self) -> bool:
217
- """mdmd:hidden"""
218
- return self._is_hydrated
219
-
220
- @property
221
- def deps(self) -> Callable[..., list["_Object"]]:
222
- """mdmd:hidden"""
223
- return self._deps if self._deps is not None else lambda: []
224
-
225
- async def resolve(self, client: Optional[_Client] = None):
226
- """mdmd:hidden"""
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)}")
238
- return
239
- elif not self._hydrate_lazily:
240
- self._validate_is_hydrated()
241
- else:
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)
245
- await resolver.load(self)
246
-
247
4
 
248
5
  Object = synchronize_api(_Object, target_module=__name__)
249
-
250
-
251
- def live_method(method):
252
- @wraps(method)
253
- async def wrapped(self, *args, **kwargs):
254
- await self.resolve()
255
- return await method(self, *args, **kwargs)
256
-
257
- return wrapped
258
-
259
-
260
- def live_method_gen(method):
261
- @wraps(method)
262
- async def wrapped(self, *args, **kwargs):
263
- await self.resolve()
264
- async with aclosing(method(self, *args, **kwargs)) as stream:
265
- async for item in stream:
266
- yield item
267
-
268
- return wrapped