modal 0.72.21__py3-none-any.whl → 0.72.23__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/_container_entrypoint.py +1 -1
- modal/_object.py +279 -0
- modal/_resolver.py +7 -5
- modal/_runtime/user_code_imports.py +7 -7
- modal/_serialization.py +4 -3
- modal/app.py +1 -1
- modal/app.pyi +4 -3
- modal/cli/app.py +1 -1
- modal/cli/container.py +1 -1
- modal/client.py +1 -0
- modal/client.pyi +4 -2
- modal/cls.py +1 -1
- modal/cls.pyi +2 -1
- modal/dict.py +1 -1
- modal/dict.pyi +2 -1
- modal/environments.py +1 -1
- modal/environments.pyi +2 -1
- modal/functions.py +18 -20
- modal/functions.pyi +13 -12
- modal/image.py +6 -6
- modal/image.pyi +2 -1
- modal/mount.py +1 -1
- modal/mount.pyi +2 -1
- modal/network_file_system.py +7 -7
- modal/network_file_system.pyi +2 -1
- modal/object.py +2 -265
- modal/object.pyi +30 -122
- modal/proxy.py +1 -1
- modal/proxy.pyi +2 -1
- modal/queue.py +1 -1
- modal/queue.pyi +2 -1
- modal/runner.py +2 -2
- modal/sandbox.py +1 -1
- modal/sandbox.pyi +2 -1
- modal/secret.py +1 -1
- modal/secret.pyi +2 -1
- modal/volume.py +1 -1
- modal/volume.pyi +2 -1
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/METADATA +1 -1
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/RECORD +48 -47
- modal_proto/api.proto +1 -1
- modal_proto/api_pb2.py +246 -246
- modal_proto/api_pb2.pyi +5 -2
- modal_version/_version_generated.py +1 -1
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/LICENSE +0 -0
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/WHEEL +0 -0
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/entry_points.txt +0 -0
- {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/top_level.txt +0 -0
modal/functions.py
CHANGED
@@ -26,6 +26,7 @@ from modal_proto import api_pb2
|
|
26
26
|
from modal_proto.modal_api_grpc import ModalClientModal
|
27
27
|
|
28
28
|
from ._location import parse_cloud_provider
|
29
|
+
from ._object import _get_environment_name, _Object, live_method, live_method_gen
|
29
30
|
from ._pty import get_pty_info
|
30
31
|
from ._resolver import Resolver
|
31
32
|
from ._resources import convert_fn_config_to_resources_config
|
@@ -71,7 +72,6 @@ from .gpu import GPU_T, parse_gpu_config
|
|
71
72
|
from .image import _Image
|
72
73
|
from .mount import _get_client_mount, _Mount, get_auto_mounts
|
73
74
|
from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
|
74
|
-
from .object import _get_environment_name, _Object, live_method, live_method_gen
|
75
75
|
from .output import _get_output_manager
|
76
76
|
from .parallel_map import (
|
77
77
|
_for_each_async,
|
@@ -388,7 +388,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
388
388
|
_is_method: bool
|
389
389
|
_spec: Optional[_FunctionSpec] = None
|
390
390
|
_tag: str
|
391
|
-
_raw_f: Callable[..., Any]
|
391
|
+
_raw_f: Optional[Callable[..., Any]] # this is set to None for a "class service [function]"
|
392
392
|
_build_args: dict
|
393
393
|
|
394
394
|
_is_generator: Optional[bool] = None
|
@@ -474,7 +474,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
474
474
|
_experimental_buffer_containers: Optional[int] = None,
|
475
475
|
_experimental_proxy_ip: Optional[str] = None,
|
476
476
|
_experimental_custom_scaling_factor: Optional[float] = None,
|
477
|
-
) ->
|
477
|
+
) -> "_Function":
|
478
478
|
"""mdmd:hidden"""
|
479
479
|
# Needed to avoid circular imports
|
480
480
|
from .partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
|
@@ -573,7 +573,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
573
573
|
)
|
574
574
|
image = _Image._from_args(
|
575
575
|
base_images={"base": image},
|
576
|
-
build_function=snapshot_function,
|
576
|
+
build_function=snapshot_function, # type: ignore # TODO: separate functions.py and _functions.py
|
577
577
|
force_build=image.force_build or pf.force_build,
|
578
578
|
)
|
579
579
|
|
@@ -962,7 +962,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
962
962
|
f"The {identity} has not been hydrated with the metadata it needs to run on Modal{reason}."
|
963
963
|
)
|
964
964
|
|
965
|
-
assert parent._client.stub
|
965
|
+
assert parent._client and parent._client.stub
|
966
966
|
|
967
967
|
if can_use_parent:
|
968
968
|
# We can end up here if parent wasn't hydrated when class was instantiated, but has been since.
|
@@ -983,9 +983,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
983
983
|
else:
|
984
984
|
serialized_params = serialize((args, kwargs))
|
985
985
|
environment_name = _get_environment_name(None, resolver)
|
986
|
-
assert parent is not None
|
986
|
+
assert parent is not None and parent.is_hydrated
|
987
987
|
req = api_pb2.FunctionBindParamsRequest(
|
988
|
-
function_id=parent.
|
988
|
+
function_id=parent.object_id,
|
989
989
|
serialized_params=serialized_params,
|
990
990
|
function_options=options,
|
991
991
|
environment_name=environment_name
|
@@ -1032,11 +1032,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1032
1032
|
"""
|
1033
1033
|
)
|
1034
1034
|
)
|
1035
|
-
assert self._client and self._client.stub
|
1036
1035
|
request = api_pb2.FunctionUpdateSchedulingParamsRequest(
|
1037
|
-
function_id=self.
|
1036
|
+
function_id=self.object_id, warm_pool_size_override=warm_pool_size
|
1038
1037
|
)
|
1039
|
-
await retry_transient_errors(self.
|
1038
|
+
await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
|
1040
1039
|
|
1041
1040
|
@classmethod
|
1042
1041
|
@renamed_parameter((2024, 12, 18), "tag", "name")
|
@@ -1142,7 +1141,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1142
1141
|
"""mdmd:hidden"""
|
1143
1142
|
# Plaintext source and arg definition for the function, so it's part of the image
|
1144
1143
|
# hash. We can't use the cloudpickle hash because it's not very stable.
|
1145
|
-
assert hasattr(self, "_raw_f") and hasattr(self, "_build_args")
|
1144
|
+
assert hasattr(self, "_raw_f") and hasattr(self, "_build_args") and self._raw_f is not None
|
1146
1145
|
return f"{inspect.getsource(self._raw_f)}\n{repr(self._build_args)}"
|
1147
1146
|
|
1148
1147
|
# Live handle methods
|
@@ -1248,7 +1247,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1248
1247
|
_map_invocation(
|
1249
1248
|
self, # type: ignore
|
1250
1249
|
input_queue,
|
1251
|
-
self.
|
1250
|
+
self.client,
|
1252
1251
|
order_outputs,
|
1253
1252
|
return_exceptions,
|
1254
1253
|
count_update_callback,
|
@@ -1266,7 +1265,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1266
1265
|
self,
|
1267
1266
|
args,
|
1268
1267
|
kwargs,
|
1269
|
-
client=self.
|
1268
|
+
client=self.client,
|
1270
1269
|
function_call_invocation_type=function_call_invocation_type,
|
1271
1270
|
)
|
1272
1271
|
|
@@ -1276,7 +1275,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1276
1275
|
self, args, kwargs, function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
|
1277
1276
|
) -> _Invocation:
|
1278
1277
|
return await _Invocation.create(
|
1279
|
-
self, args, kwargs, client=self.
|
1278
|
+
self, args, kwargs, client=self.client, function_call_invocation_type=function_call_invocation_type
|
1280
1279
|
)
|
1281
1280
|
|
1282
1281
|
@warn_if_generator_is_not_consumed()
|
@@ -1287,7 +1286,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1287
1286
|
self,
|
1288
1287
|
args,
|
1289
1288
|
kwargs,
|
1290
|
-
client=self.
|
1289
|
+
client=self.client,
|
1291
1290
|
function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
|
1292
1291
|
)
|
1293
1292
|
async for res in invocation.run_generator():
|
@@ -1303,7 +1302,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1303
1302
|
self,
|
1304
1303
|
args,
|
1305
1304
|
kwargs,
|
1306
|
-
client=self.
|
1305
|
+
client=self.client,
|
1307
1306
|
function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC_LEGACY,
|
1308
1307
|
)
|
1309
1308
|
|
@@ -1452,14 +1451,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1452
1451
|
|
1453
1452
|
def get_raw_f(self) -> Callable[..., Any]:
|
1454
1453
|
"""Return the inner Python object wrapped by this Modal Function."""
|
1454
|
+
assert self._raw_f is not None
|
1455
1455
|
return self._raw_f
|
1456
1456
|
|
1457
1457
|
@live_method
|
1458
1458
|
async def get_current_stats(self) -> FunctionStats:
|
1459
1459
|
"""Return a `FunctionStats` object describing the current function's queue and runner counts."""
|
1460
|
-
assert self._client.stub
|
1461
1460
|
resp = await retry_transient_errors(
|
1462
|
-
self.
|
1461
|
+
self.client.stub.FunctionGetCurrentStats,
|
1463
1462
|
api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
|
1464
1463
|
total_timeout=10.0,
|
1465
1464
|
)
|
@@ -1491,8 +1490,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1491
1490
|
_is_generator: bool = False
|
1492
1491
|
|
1493
1492
|
def _invocation(self):
|
1494
|
-
|
1495
|
-
return _Invocation(self._client.stub, self.object_id, self._client)
|
1493
|
+
return _Invocation(self.client.stub, self.object_id, self.client)
|
1496
1494
|
|
1497
1495
|
async def get(self, timeout: Optional[float] = None) -> ReturnType:
|
1498
1496
|
"""Get the result of the function call.
|
modal/functions.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._utils.async_utils
|
4
5
|
import modal._utils.function_utils
|
5
6
|
import modal.app
|
@@ -133,7 +134,7 @@ ReturnType = typing.TypeVar("ReturnType", covariant=True)
|
|
133
134
|
|
134
135
|
OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
|
135
136
|
|
136
|
-
class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.
|
137
|
+
class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal._object._Object):
|
137
138
|
_info: typing.Optional[modal._utils.function_utils.FunctionInfo]
|
138
139
|
_serve_mounts: frozenset[modal.mount._Mount]
|
139
140
|
_app: typing.Optional[modal.app._App]
|
@@ -143,7 +144,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
143
144
|
_is_method: bool
|
144
145
|
_spec: typing.Optional[_FunctionSpec]
|
145
146
|
_tag: str
|
146
|
-
_raw_f: typing.Callable[..., typing.Any]
|
147
|
+
_raw_f: typing.Optional[typing.Callable[..., typing.Any]]
|
147
148
|
_build_args: dict
|
148
149
|
_is_generator: typing.Optional[bool]
|
149
150
|
_cluster_size: typing.Optional[int]
|
@@ -197,7 +198,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
197
198
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
198
199
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
199
200
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
200
|
-
) ->
|
201
|
+
) -> _Function: ...
|
201
202
|
def _bind_parameters(
|
202
203
|
self,
|
203
204
|
obj: modal.cls._Obj,
|
@@ -311,7 +312,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
311
312
|
_is_method: bool
|
312
313
|
_spec: typing.Optional[_FunctionSpec]
|
313
314
|
_tag: str
|
314
|
-
_raw_f: typing.Callable[..., typing.Any]
|
315
|
+
_raw_f: typing.Optional[typing.Callable[..., typing.Any]]
|
315
316
|
_build_args: dict
|
316
317
|
_is_generator: typing.Optional[bool]
|
317
318
|
_cluster_size: typing.Optional[int]
|
@@ -366,7 +367,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
366
367
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
367
368
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
368
369
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
369
|
-
) ->
|
370
|
+
) -> Function: ...
|
370
371
|
def _bind_parameters(
|
371
372
|
self,
|
372
373
|
obj: modal.cls.Obj,
|
@@ -462,11 +463,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
462
463
|
|
463
464
|
_call_generator_nowait: ___call_generator_nowait_spec
|
464
465
|
|
465
|
-
class __remote_spec(typing_extensions.Protocol[
|
466
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
466
467
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
467
468
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
468
469
|
|
469
|
-
remote: __remote_spec[
|
470
|
+
remote: __remote_spec[ReturnType, P]
|
470
471
|
|
471
472
|
class __remote_gen_spec(typing_extensions.Protocol):
|
472
473
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -479,17 +480,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
479
480
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
480
481
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
481
482
|
|
482
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
483
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
483
484
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
484
485
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
485
486
|
|
486
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
487
|
+
_experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
|
487
488
|
|
488
|
-
class __spawn_spec(typing_extensions.Protocol[
|
489
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
489
490
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
490
491
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
491
492
|
|
492
|
-
spawn: __spawn_spec[
|
493
|
+
spawn: __spawn_spec[ReturnType, P]
|
493
494
|
|
494
495
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
495
496
|
|
@@ -539,7 +540,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
539
540
|
|
540
541
|
for_each: __for_each_spec
|
541
542
|
|
542
|
-
class _FunctionCall(typing.Generic[ReturnType], modal.
|
543
|
+
class _FunctionCall(typing.Generic[ReturnType], modal._object._Object):
|
543
544
|
_is_generator: bool
|
544
545
|
|
545
546
|
def _invocation(self): ...
|
modal/image.py
CHANGED
@@ -26,6 +26,7 @@ from grpclib.exceptions import GRPCError, StreamTerminatedError
|
|
26
26
|
|
27
27
|
from modal_proto import api_pb2
|
28
28
|
|
29
|
+
from ._object import _Object, live_method_gen
|
29
30
|
from ._resolver import Resolver
|
30
31
|
from ._serialization import serialize
|
31
32
|
from ._utils.async_utils import synchronize_api
|
@@ -46,7 +47,6 @@ from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_
|
|
46
47
|
from .gpu import GPU_T, parse_gpu_config
|
47
48
|
from .mount import _Mount, python_standalone_mount_name
|
48
49
|
from .network_file_system import _NetworkFileSystem
|
49
|
-
from .object import _Object, live_method_gen
|
50
50
|
from .output import _get_output_manager
|
51
51
|
from .scheduler_placement import SchedulerPlacement
|
52
52
|
from .secret import _Secret
|
@@ -2053,11 +2053,11 @@ class _Image(_Object, type_prefix="im"):
|
|
2053
2053
|
try:
|
2054
2054
|
yield
|
2055
2055
|
except Exception as exc:
|
2056
|
-
if self.
|
2057
|
-
# Might be
|
2056
|
+
if not self.is_hydrated:
|
2057
|
+
# Might be hydrated later
|
2058
2058
|
self.inside_exceptions.append(exc)
|
2059
2059
|
elif env_image_id == self.object_id:
|
2060
|
-
# Image is already
|
2060
|
+
# Image is already hydrated (we can remove this case later
|
2061
2061
|
# when we don't hydrate objects so early)
|
2062
2062
|
raise
|
2063
2063
|
if not isinstance(exc, ImportError):
|
@@ -2072,9 +2072,9 @@ class _Image(_Object, type_prefix="im"):
|
|
2072
2072
|
last_entry_id: str = ""
|
2073
2073
|
|
2074
2074
|
request = api_pb2.ImageJoinStreamingRequest(
|
2075
|
-
image_id=self.
|
2075
|
+
image_id=self.object_id, timeout=55, last_entry_id=last_entry_id, include_logs_for_finished=True
|
2076
2076
|
)
|
2077
|
-
async for response in self.
|
2077
|
+
async for response in self.client.stub.ImageJoinStreaming.unary_stream(request):
|
2078
2078
|
if response.result.status:
|
2079
2079
|
return
|
2080
2080
|
if response.entry_id:
|
modal/image.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.client
|
4
5
|
import modal.cloud_bucket_mount
|
5
6
|
import modal.functions
|
@@ -78,7 +79,7 @@ async def _image_await_build_result(
|
|
78
79
|
image_id: str, client: modal.client._Client
|
79
80
|
) -> modal_proto.api_pb2.ImageJoinStreamingResponse: ...
|
80
81
|
|
81
|
-
class _Image(modal.
|
82
|
+
class _Image(modal._object._Object):
|
82
83
|
force_build: bool
|
83
84
|
inside_exceptions: list[Exception]
|
84
85
|
_serve_mounts: frozenset[modal.mount._Mount]
|
modal/mount.py
CHANGED
@@ -20,6 +20,7 @@ import modal.file_pattern_matcher
|
|
20
20
|
from modal_proto import api_pb2
|
21
21
|
from modal_version import __version__
|
22
22
|
|
23
|
+
from ._object import _get_environment_name, _Object
|
23
24
|
from ._resolver import Resolver
|
24
25
|
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
25
26
|
from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
|
@@ -31,7 +32,6 @@ from .client import _Client
|
|
31
32
|
from .config import config, logger
|
32
33
|
from .exception import InvalidError, ModuleNotMountable
|
33
34
|
from .file_pattern_matcher import FilePatternMatcher
|
34
|
-
from .object import _get_environment_name, _Object
|
35
35
|
|
36
36
|
ROOT_DIR: PurePosixPath = PurePosixPath("/root")
|
37
37
|
MOUNT_PUT_FILE_CLIENT_TIMEOUT = 10 * 60 # 10 min max for transferring files
|
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
|
@@ -76,7 +77,7 @@ class _MountedPythonModule(_MountEntry):
|
|
76
77
|
|
77
78
|
class NonLocalMountError(Exception): ...
|
78
79
|
|
79
|
-
class _Mount(modal.
|
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]
|
modal/network_file_system.py
CHANGED
@@ -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 = (
|
modal/network_file_system.pyi
CHANGED
@@ -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.
|
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
|
modal/object.py
CHANGED
@@ -1,268 +1,5 @@
|
|
1
|
-
# Copyright Modal Labs
|
2
|
-
import
|
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
|