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.
Files changed (48) hide show
  1. modal/_container_entrypoint.py +1 -1
  2. modal/_object.py +279 -0
  3. modal/_resolver.py +7 -5
  4. modal/_runtime/user_code_imports.py +7 -7
  5. modal/_serialization.py +4 -3
  6. modal/app.py +1 -1
  7. modal/app.pyi +4 -3
  8. modal/cli/app.py +1 -1
  9. modal/cli/container.py +1 -1
  10. modal/client.py +1 -0
  11. modal/client.pyi +4 -2
  12. modal/cls.py +1 -1
  13. modal/cls.pyi +2 -1
  14. modal/dict.py +1 -1
  15. modal/dict.pyi +2 -1
  16. modal/environments.py +1 -1
  17. modal/environments.pyi +2 -1
  18. modal/functions.py +18 -20
  19. modal/functions.pyi +13 -12
  20. modal/image.py +6 -6
  21. modal/image.pyi +2 -1
  22. modal/mount.py +1 -1
  23. modal/mount.pyi +2 -1
  24. modal/network_file_system.py +7 -7
  25. modal/network_file_system.pyi +2 -1
  26. modal/object.py +2 -265
  27. modal/object.pyi +30 -122
  28. modal/proxy.py +1 -1
  29. modal/proxy.pyi +2 -1
  30. modal/queue.py +1 -1
  31. modal/queue.pyi +2 -1
  32. modal/runner.py +2 -2
  33. modal/sandbox.py +1 -1
  34. modal/sandbox.pyi +2 -1
  35. modal/secret.py +1 -1
  36. modal/secret.pyi +2 -1
  37. modal/volume.py +1 -1
  38. modal/volume.pyi +2 -1
  39. {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/METADATA +1 -1
  40. {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/RECORD +48 -47
  41. modal_proto/api.proto +1 -1
  42. modal_proto/api_pb2.py +246 -246
  43. modal_proto/api_pb2.pyi +5 -2
  44. modal_version/_version_generated.py +1 -1
  45. {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/LICENSE +0 -0
  46. {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/WHEEL +0 -0
  47. {modal-0.72.21.dist-info → modal-0.72.23.dist-info}/entry_points.txt +0 -0
  48. {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
- ) -> None:
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._object_id,
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._object_id, warm_pool_size_override=warm_pool_size
1036
+ function_id=self.object_id, warm_pool_size_override=warm_pool_size
1038
1037
  )
1039
- await retry_transient_errors(self._client.stub.FunctionUpdateSchedulingParams, request)
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._client,
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._client,
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._client, function_call_invocation_type=function_call_invocation_type
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._client,
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._client,
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._client.stub.FunctionGetCurrentStats,
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
- assert self._client.stub
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.object._Object):
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
- ) -> None: ...
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
- ) -> None: ...
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[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
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[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
487
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
487
488
 
488
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
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[P, ReturnType]
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.object._Object):
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.object_id is None:
2057
- # Might be initialized later
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 initialized (we can remove this case later
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._object_id, timeout=55, last_entry_id=last_entry_id, include_logs_for_finished=True
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._client.stub.ImageJoinStreaming.unary_stream(request):
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.object._Object):
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.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]
@@ -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
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