modal 0.67.6__py3-none-any.whl → 0.67.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. modal/_clustered_functions.py +2 -2
  2. modal/_clustered_functions.pyi +2 -2
  3. modal/_container_entrypoint.py +5 -4
  4. modal/_output.py +29 -28
  5. modal/_pty.py +2 -2
  6. modal/_resolver.py +6 -5
  7. modal/_resources.py +3 -3
  8. modal/_runtime/asgi.py +7 -6
  9. modal/_runtime/container_io_manager.py +22 -26
  10. modal/_runtime/execution_context.py +2 -2
  11. modal/_runtime/telemetry.py +1 -2
  12. modal/_runtime/user_code_imports.py +12 -14
  13. modal/_serialization.py +3 -7
  14. modal/_traceback.py +5 -5
  15. modal/_tunnel.py +4 -3
  16. modal/_tunnel.pyi +2 -2
  17. modal/_utils/async_utils.py +8 -15
  18. modal/_utils/blob_utils.py +4 -3
  19. modal/_utils/function_utils.py +14 -10
  20. modal/_utils/grpc_testing.py +7 -6
  21. modal/_utils/grpc_utils.py +2 -3
  22. modal/_utils/hash_utils.py +2 -2
  23. modal/_utils/mount_utils.py +5 -4
  24. modal/_utils/package_utils.py +2 -3
  25. modal/_utils/pattern_matcher.py +6 -6
  26. modal/_utils/rand_pb_testing.py +3 -3
  27. modal/_utils/shell_utils.py +2 -1
  28. modal/_vendor/a2wsgi_wsgi.py +62 -72
  29. modal/_vendor/cloudpickle.py +1 -1
  30. modal/_watcher.py +8 -7
  31. modal/app.py +29 -34
  32. modal/app.pyi +102 -97
  33. modal/call_graph.py +6 -6
  34. modal/cli/_download.py +3 -2
  35. modal/cli/_traceback.py +4 -4
  36. modal/cli/app.py +4 -4
  37. modal/cli/container.py +4 -4
  38. modal/cli/dict.py +1 -1
  39. modal/cli/environment.py +2 -3
  40. modal/cli/launch.py +2 -2
  41. modal/cli/network_file_system.py +1 -1
  42. modal/cli/profile.py +1 -1
  43. modal/cli/programs/run_jupyter.py +2 -2
  44. modal/cli/programs/vscode.py +3 -3
  45. modal/cli/queues.py +1 -1
  46. modal/cli/run.py +6 -6
  47. modal/cli/secret.py +3 -3
  48. modal/cli/utils.py +2 -1
  49. modal/cli/volume.py +3 -3
  50. modal/client.py +6 -11
  51. modal/client.pyi +18 -27
  52. modal/cloud_bucket_mount.py +3 -3
  53. modal/cloud_bucket_mount.pyi +2 -2
  54. modal/cls.py +30 -30
  55. modal/cls.pyi +35 -34
  56. modal/config.py +3 -2
  57. modal/dict.py +4 -3
  58. modal/dict.pyi +10 -9
  59. modal/environments.py +3 -3
  60. modal/environments.pyi +3 -3
  61. modal/exception.py +2 -3
  62. modal/functions.py +105 -35
  63. modal/functions.pyi +71 -48
  64. modal/image.py +45 -48
  65. modal/image.pyi +102 -101
  66. modal/io_streams.py +4 -7
  67. modal/io_streams.pyi +14 -13
  68. modal/mount.py +23 -22
  69. modal/mount.pyi +28 -29
  70. modal/network_file_system.py +7 -6
  71. modal/network_file_system.pyi +12 -11
  72. modal/object.py +9 -8
  73. modal/object.pyi +47 -34
  74. modal/output.py +2 -1
  75. modal/parallel_map.py +4 -4
  76. modal/partial_function.py +9 -13
  77. modal/partial_function.pyi +17 -18
  78. modal/queue.py +9 -8
  79. modal/queue.pyi +23 -22
  80. modal/retries.py +38 -0
  81. modal/runner.py +8 -7
  82. modal/runner.pyi +8 -14
  83. modal/running_app.py +3 -3
  84. modal/sandbox.py +14 -13
  85. modal/sandbox.pyi +67 -72
  86. modal/scheduler_placement.py +2 -1
  87. modal/secret.py +7 -7
  88. modal/secret.pyi +12 -12
  89. modal/serving.py +4 -3
  90. modal/serving.pyi +5 -4
  91. modal/token_flow.py +3 -2
  92. modal/token_flow.pyi +3 -3
  93. modal/volume.py +7 -12
  94. modal/volume.pyi +17 -16
  95. {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/METADATA +1 -1
  96. modal-0.67.11.dist-info/RECORD +168 -0
  97. modal_docs/mdmd/signatures.py +1 -2
  98. modal_version/_version_generated.py +1 -1
  99. modal-0.67.6.dist-info/RECORD +0 -168
  100. {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/LICENSE +0 -0
  101. {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/WHEEL +0 -0
  102. {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/entry_points.txt +0 -0
  103. {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/top_level.txt +0 -0
modal/cls.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import collections.abc
1
2
  import google.protobuf.message
2
3
  import inspect
3
4
  import modal.app
@@ -20,10 +21,10 @@ def _use_annotation_parameters(user_cls) -> bool: ...
20
21
  def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
21
22
 
22
23
  class _Obj:
23
- _functions: typing.Dict[str, modal.functions._Function]
24
- _entered: bool
24
+ _functions: dict[str, modal.functions._Function]
25
+ _has_entered: bool
25
26
  _user_cls_instance: typing.Optional[typing.Any]
26
- _construction_args: typing.Tuple[tuple, typing.Dict[str, typing.Any]]
27
+ _construction_args: tuple[tuple, dict[str, typing.Any]]
27
28
  _instance_service_function: typing.Optional[modal.functions._Function]
28
29
 
29
30
  def _uses_common_service_function(self): ...
@@ -31,7 +32,7 @@ class _Obj:
31
32
  self,
32
33
  user_cls: type,
33
34
  class_service_function: typing.Optional[modal.functions._Function],
34
- classbound_methods: typing.Dict[str, modal.functions._Function],
35
+ classbound_methods: dict[str, modal.functions._Function],
35
36
  from_other_workspace: bool,
36
37
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
37
38
  args,
@@ -40,26 +41,26 @@ class _Obj:
40
41
  def _new_user_cls_instance(self): ...
41
42
  async def keep_warm(self, warm_pool_size: int) -> None: ...
42
43
  def _cached_user_cls_instance(self): ...
43
- def enter(self): ...
44
+ def _enter(self): ...
44
45
  @property
45
- def entered(self): ...
46
- @entered.setter
47
- def entered(self, val): ...
48
- async def aenter(self): ...
46
+ def _entered(self) -> bool: ...
47
+ @_entered.setter
48
+ def _entered(self, val: bool): ...
49
+ async def _aenter(self): ...
49
50
  def __getattr__(self, k): ...
50
51
 
51
52
  class Obj:
52
- _functions: typing.Dict[str, modal.functions.Function]
53
- _entered: bool
53
+ _functions: dict[str, modal.functions.Function]
54
+ _has_entered: bool
54
55
  _user_cls_instance: typing.Optional[typing.Any]
55
- _construction_args: typing.Tuple[tuple, typing.Dict[str, typing.Any]]
56
+ _construction_args: tuple[tuple, dict[str, typing.Any]]
56
57
  _instance_service_function: typing.Optional[modal.functions.Function]
57
58
 
58
59
  def __init__(
59
60
  self,
60
61
  user_cls: type,
61
62
  class_service_function: typing.Optional[modal.functions.Function],
62
- classbound_methods: typing.Dict[str, modal.functions.Function],
63
+ classbound_methods: dict[str, modal.functions.Function],
63
64
  from_other_workspace: bool,
64
65
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
65
66
  args,
@@ -75,26 +76,26 @@ class Obj:
75
76
  keep_warm: __keep_warm_spec
76
77
 
77
78
  def _cached_user_cls_instance(self): ...
78
- def enter(self): ...
79
+ def _enter(self): ...
79
80
  @property
80
- def entered(self): ...
81
- @entered.setter
82
- def entered(self, val): ...
83
- async def aenter(self): ...
81
+ def _entered(self) -> bool: ...
82
+ @_entered.setter
83
+ def _entered(self, val: bool): ...
84
+ async def _aenter(self): ...
84
85
  def __getattr__(self, k): ...
85
86
 
86
87
  class _Cls(modal.object._Object):
87
88
  _user_cls: typing.Optional[type]
88
89
  _class_service_function: typing.Optional[modal.functions._Function]
89
- _method_functions: typing.Optional[typing.Dict[str, modal.functions._Function]]
90
+ _method_functions: typing.Optional[dict[str, modal.functions._Function]]
90
91
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
91
- _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
92
+ _callables: dict[str, typing.Callable[..., typing.Any]]
92
93
  _from_other_workspace: typing.Optional[bool]
93
94
  _app: typing.Optional[modal.app._App]
94
95
 
95
96
  def _initialize_from_empty(self): ...
96
97
  def _initialize_from_other(self, other: _Cls): ...
97
- def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function._PartialFunction]: ...
98
+ def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
98
99
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
99
100
  @staticmethod
100
101
  def validate_construction_mechanism(user_cls): ...
@@ -103,7 +104,7 @@ class _Cls(modal.object._Object):
103
104
  def _uses_common_service_function(self): ...
104
105
  @classmethod
105
106
  def from_name(
106
- cls: typing.Type[_Cls],
107
+ cls: type[_Cls],
107
108
  app_name: str,
108
109
  tag: str,
109
110
  namespace=1,
@@ -112,11 +113,11 @@ class _Cls(modal.object._Object):
112
113
  ) -> _Cls: ...
113
114
  def with_options(
114
115
  self: _Cls,
115
- cpu: typing.Union[float, typing.Tuple[float, float], None] = None,
116
- memory: typing.Union[int, typing.Tuple[int, int], None] = None,
116
+ cpu: typing.Union[float, tuple[float, float], None] = None,
117
+ memory: typing.Union[int, tuple[int, int], None] = None,
117
118
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
118
- secrets: typing.Collection[modal.secret._Secret] = (),
119
- volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
119
+ secrets: collections.abc.Collection[modal.secret._Secret] = (),
120
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
120
121
  retries: typing.Union[int, modal.retries.Retries, None] = None,
121
122
  timeout: typing.Optional[int] = None,
122
123
  concurrency_limit: typing.Optional[int] = None,
@@ -138,16 +139,16 @@ class _Cls(modal.object._Object):
138
139
  class Cls(modal.object.Object):
139
140
  _user_cls: typing.Optional[type]
140
141
  _class_service_function: typing.Optional[modal.functions.Function]
141
- _method_functions: typing.Optional[typing.Dict[str, modal.functions.Function]]
142
+ _method_functions: typing.Optional[dict[str, modal.functions.Function]]
142
143
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
143
- _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
144
+ _callables: dict[str, typing.Callable[..., typing.Any]]
144
145
  _from_other_workspace: typing.Optional[bool]
145
146
  _app: typing.Optional[modal.app.App]
146
147
 
147
148
  def __init__(self, *args, **kwargs): ...
148
149
  def _initialize_from_empty(self): ...
149
150
  def _initialize_from_other(self, other: Cls): ...
150
- def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function.PartialFunction]: ...
151
+ def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
151
152
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
152
153
  @staticmethod
153
154
  def validate_construction_mechanism(user_cls): ...
@@ -156,7 +157,7 @@ class Cls(modal.object.Object):
156
157
  def _uses_common_service_function(self): ...
157
158
  @classmethod
158
159
  def from_name(
159
- cls: typing.Type[Cls],
160
+ cls: type[Cls],
160
161
  app_name: str,
161
162
  tag: str,
162
163
  namespace=1,
@@ -165,11 +166,11 @@ class Cls(modal.object.Object):
165
166
  ) -> Cls: ...
166
167
  def with_options(
167
168
  self: Cls,
168
- cpu: typing.Union[float, typing.Tuple[float, float], None] = None,
169
- memory: typing.Union[int, typing.Tuple[int, int], None] = None,
169
+ cpu: typing.Union[float, tuple[float, float], None] = None,
170
+ memory: typing.Union[int, tuple[int, int], None] = None,
170
171
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
171
- secrets: typing.Collection[modal.secret.Secret] = (),
172
- volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
172
+ secrets: collections.abc.Collection[modal.secret.Secret] = (),
173
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
173
174
  retries: typing.Union[int, modal.retries.Retries, None] = None,
174
175
  timeout: typing.Optional[int] = None,
175
176
  concurrency_limit: typing.Optional[int] = None,
modal/config.py CHANGED
@@ -80,7 +80,7 @@ import os
80
80
  import typing
81
81
  import warnings
82
82
  from textwrap import dedent
83
- from typing import Any, Dict, Optional
83
+ from typing import Any, Optional
84
84
 
85
85
  from google.protobuf.empty_pb2 import Empty
86
86
 
@@ -221,6 +221,7 @@ _SETTINGS = {
221
221
  "image_builder_version": _Setting(),
222
222
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
223
223
  "snapshot_debug": _Setting(False, transform=_to_boolean),
224
+ "client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
224
225
  }
225
226
 
226
227
 
@@ -282,7 +283,7 @@ configure_logger(logger, config["loglevel"], config["log_format"])
282
283
 
283
284
 
284
285
  def _store_user_config(
285
- new_settings: Dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
286
+ new_settings: dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
286
287
  ):
287
288
  """Internal method, used by the CLI to set tokens."""
288
289
  if profile is None:
modal/dict.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
- from typing import Any, AsyncIterator, Optional, Tuple, Type
2
+ from collections.abc import AsyncIterator
3
+ from typing import Any, Optional
3
4
 
4
5
  from grpclib import GRPCError
5
6
  from synchronicity.async_wrap import asynccontextmanager
@@ -74,7 +75,7 @@ class _Dict(_Object, type_prefix="di"):
74
75
  @classmethod
75
76
  @asynccontextmanager
76
77
  async def ephemeral(
77
- cls: Type["_Dict"],
78
+ cls: type["_Dict"],
78
79
  data: Optional[dict] = None,
79
80
  client: Optional[_Client] = None,
80
81
  environment_name: Optional[str] = None,
@@ -316,7 +317,7 @@ class _Dict(_Object, type_prefix="di"):
316
317
  yield deserialize(resp.value, self._client)
317
318
 
318
319
  @live_method_gen
319
- async def items(self) -> AsyncIterator[Tuple[Any, Any]]:
320
+ async def items(self) -> AsyncIterator[tuple[Any, Any]]:
320
321
  """Return an iterator over the (key, value) tuples in this dictionary.
321
322
 
322
323
  Note that (unlike with Python dicts) the return value is a simple iterator,
modal/dict.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import collections.abc
1
2
  import modal.client
2
3
  import modal.object
3
4
  import synchronicity.combined_types
@@ -12,7 +13,7 @@ class _Dict(modal.object._Object):
12
13
  def __init__(self, data={}): ...
13
14
  @classmethod
14
15
  def ephemeral(
15
- cls: typing.Type[_Dict],
16
+ cls: type[_Dict],
16
17
  data: typing.Optional[dict] = None,
17
18
  client: typing.Optional[modal.client._Client] = None,
18
19
  environment_name: typing.Optional[str] = None,
@@ -53,9 +54,9 @@ class _Dict(modal.object._Object):
53
54
  async def pop(self, key: typing.Any) -> typing.Any: ...
54
55
  async def __delitem__(self, key: typing.Any) -> typing.Any: ...
55
56
  async def __contains__(self, key: typing.Any) -> bool: ...
56
- def keys(self) -> typing.AsyncIterator[typing.Any]: ...
57
- def values(self) -> typing.AsyncIterator[typing.Any]: ...
58
- def items(self) -> typing.AsyncIterator[typing.Tuple[typing.Any, typing.Any]]: ...
57
+ def keys(self) -> collections.abc.AsyncIterator[typing.Any]: ...
58
+ def values(self) -> collections.abc.AsyncIterator[typing.Any]: ...
59
+ def items(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
59
60
 
60
61
  class Dict(modal.object.Object):
61
62
  def __init__(self, data={}): ...
@@ -63,7 +64,7 @@ class Dict(modal.object.Object):
63
64
  def new(data: typing.Optional[dict] = None): ...
64
65
  @classmethod
65
66
  def ephemeral(
66
- cls: typing.Type[Dict],
67
+ cls: type[Dict],
67
68
  data: typing.Optional[dict] = None,
68
69
  client: typing.Optional[modal.client.Client] = None,
69
70
  environment_name: typing.Optional[str] = None,
@@ -186,18 +187,18 @@ class Dict(modal.object.Object):
186
187
 
187
188
  class __keys_spec(typing_extensions.Protocol):
188
189
  def __call__(self) -> typing.Iterator[typing.Any]: ...
189
- def aio(self) -> typing.AsyncIterator[typing.Any]: ...
190
+ def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
190
191
 
191
192
  keys: __keys_spec
192
193
 
193
194
  class __values_spec(typing_extensions.Protocol):
194
195
  def __call__(self) -> typing.Iterator[typing.Any]: ...
195
- def aio(self) -> typing.AsyncIterator[typing.Any]: ...
196
+ def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
196
197
 
197
198
  values: __values_spec
198
199
 
199
200
  class __items_spec(typing_extensions.Protocol):
200
- def __call__(self) -> typing.Iterator[typing.Tuple[typing.Any, typing.Any]]: ...
201
- def aio(self) -> typing.AsyncIterator[typing.Tuple[typing.Any, typing.Any]]: ...
201
+ def __call__(self) -> typing.Iterator[tuple[typing.Any, typing.Any]]: ...
202
+ def aio(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
202
203
 
203
204
  items: __items_spec
modal/environments.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # Copyright Modal Labs 2023
2
2
  from dataclasses import dataclass
3
- from typing import Dict, List, Optional
3
+ from typing import Optional
4
4
 
5
5
  from google.protobuf.empty_pb2 import Empty
6
6
  from google.protobuf.message import Message
@@ -98,7 +98,7 @@ Environment = synchronize_api(_Environment)
98
98
 
99
99
 
100
100
  # Needs to be after definition; synchronicity interferes with forward references?
101
- ENVIRONMENT_CACHE: Dict[str, _Environment] = {}
101
+ ENVIRONMENT_CACHE: dict[str, _Environment] = {}
102
102
 
103
103
 
104
104
  async def _get_environment_cached(name: str, client: _Client) -> _Environment:
@@ -151,7 +151,7 @@ async def create_environment(name: str, client: Optional[_Client] = None):
151
151
 
152
152
 
153
153
  @synchronizer.create_blocking
154
- async def list_environments(client: Optional[_Client] = None) -> List[api_pb2.EnvironmentListItem]:
154
+ async def list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
155
155
  if client is None:
156
156
  client = await _Client.from_env()
157
157
  resp = await client.stub.EnvironmentList(Empty())
modal/environments.pyi CHANGED
@@ -87,13 +87,13 @@ create_environment: __create_environment_spec
87
87
  class __list_environments_spec(typing_extensions.Protocol):
88
88
  def __call__(
89
89
  self, client: typing.Optional[modal.client.Client] = None
90
- ) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]: ...
90
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
91
91
  async def aio(
92
92
  self, client: typing.Optional[modal.client.Client] = None
93
- ) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]: ...
93
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
94
94
 
95
95
  list_environments: __list_environments_spec
96
96
 
97
97
  def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
98
98
 
99
- ENVIRONMENT_CACHE: typing.Dict[str, _Environment]
99
+ ENVIRONMENT_CACHE: dict[str, _Environment]
modal/exception.py CHANGED
@@ -4,7 +4,6 @@ import signal
4
4
  import sys
5
5
  import warnings
6
6
  from datetime import date
7
- from typing import Tuple
8
7
 
9
8
 
10
9
  class Error(Exception):
@@ -132,12 +131,12 @@ def _is_internal_frame(frame):
132
131
  return module in _INTERNAL_MODULES
133
132
 
134
133
 
135
- def deprecation_error(deprecated_on: Tuple[int, int, int], msg: str):
134
+ def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
136
135
  raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
137
136
 
138
137
 
139
138
  def deprecation_warning(
140
- deprecated_on: Tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
139
+ deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
141
140
  ) -> None:
142
141
  """Utility for getting the proper stack entry.
143
142
 
modal/functions.py CHANGED
@@ -1,24 +1,18 @@
1
1
  # Copyright Modal Labs 2023
2
+ import dataclasses
2
3
  import inspect
3
4
  import textwrap
4
5
  import time
5
6
  import typing
6
7
  import warnings
8
+ from collections.abc import AsyncGenerator, Collection, Sequence, Sized
7
9
  from dataclasses import dataclass
8
10
  from pathlib import PurePosixPath
9
11
  from typing import (
10
12
  TYPE_CHECKING,
11
13
  Any,
12
- AsyncGenerator,
13
14
  Callable,
14
- Collection,
15
- Dict,
16
- List,
17
15
  Optional,
18
- Sequence,
19
- Sized,
20
- Tuple,
21
- Type,
22
16
  Union,
23
17
  )
24
18
 
@@ -26,6 +20,7 @@ import typing_extensions
26
20
  from google.protobuf.message import Message
27
21
  from grpclib import GRPCError, Status
28
22
  from synchronicity.combined_types import MethodWithAio
23
+ from synchronicity.exceptions import UserCodeException
29
24
 
30
25
  from modal._utils.async_utils import aclosing
31
26
  from modal_proto import api_pb2
@@ -64,6 +59,7 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
64
59
  from .config import config
65
60
  from .exception import (
66
61
  ExecutionError,
62
+ FunctionTimeoutError,
67
63
  InvalidError,
68
64
  NotFoundError,
69
65
  OutputExpiredError,
@@ -86,7 +82,7 @@ from .parallel_map import (
86
82
  _SynchronizedQueue,
87
83
  )
88
84
  from .proxy import _Proxy
89
- from .retries import Retries
85
+ from .retries import Retries, RetryManager
90
86
  from .schedule import Schedule
91
87
  from .scheduler_placement import SchedulerPlacement
92
88
  from .secret import _Secret
@@ -98,15 +94,32 @@ if TYPE_CHECKING:
98
94
  import modal.partial_function
99
95
 
100
96
 
97
+ @dataclasses.dataclass
98
+ class _RetryContext:
99
+ function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
100
+ retry_policy: api_pb2.FunctionRetryPolicy
101
+ function_call_jwt: str
102
+ input_jwt: str
103
+ input_id: str
104
+ item: api_pb2.FunctionPutInputsItem
105
+
106
+
101
107
  class _Invocation:
102
108
  """Internal client representation of a single-input call to a Modal Function or Generator"""
103
109
 
104
110
  stub: ModalClientModal
105
111
 
106
- def __init__(self, stub: ModalClientModal, function_call_id: str, client: _Client):
112
+ def __init__(
113
+ self,
114
+ stub: ModalClientModal,
115
+ function_call_id: str,
116
+ client: _Client,
117
+ retry_context: Optional[_RetryContext] = None,
118
+ ):
107
119
  self.stub = stub
108
120
  self.client = client # Used by the deserializer.
109
121
  self.function_call_id = function_call_id # TODO: remove and use only input_id
122
+ self._retry_context = retry_context
110
123
 
111
124
  @staticmethod
112
125
  async def create(
@@ -132,7 +145,17 @@ class _Invocation:
132
145
  function_call_id = response.function_call_id
133
146
 
134
147
  if response.pipelined_inputs:
135
- return _Invocation(client.stub, function_call_id, client)
148
+ assert len(response.pipelined_inputs) == 1
149
+ input = response.pipelined_inputs[0]
150
+ retry_context = _RetryContext(
151
+ function_call_invocation_type=function_call_invocation_type,
152
+ retry_policy=response.retry_policy,
153
+ function_call_jwt=response.function_call_jwt,
154
+ input_jwt=input.input_jwt,
155
+ input_id=input.input_id,
156
+ item=item,
157
+ )
158
+ return _Invocation(client.stub, function_call_id, client, retry_context)
136
159
 
137
160
  request_put = api_pb2.FunctionPutInputsRequest(
138
161
  function_id=function_id, inputs=[item], function_call_id=function_call_id
@@ -144,7 +167,16 @@ class _Invocation:
144
167
  processed_inputs = inputs_response.inputs
145
168
  if not processed_inputs:
146
169
  raise Exception("Could not create function call - the input queue seems to be full")
147
- return _Invocation(client.stub, function_call_id, client)
170
+ input = inputs_response.inputs[0]
171
+ retry_context = _RetryContext(
172
+ function_call_invocation_type=function_call_invocation_type,
173
+ retry_policy=response.retry_policy,
174
+ function_call_jwt=response.function_call_jwt,
175
+ input_jwt=input.input_jwt,
176
+ input_id=input.input_id,
177
+ item=item,
178
+ )
179
+ return _Invocation(client.stub, function_call_id, client, retry_context)
148
180
 
149
181
  async def pop_function_call_outputs(
150
182
  self, timeout: Optional[float], clear_on_success: bool
@@ -180,13 +212,46 @@ class _Invocation:
180
212
  # return the last response to check for state of num_unfinished_inputs
181
213
  return response
182
214
 
183
- async def run_function(self) -> Any:
215
+ async def _retry_input(self) -> None:
216
+ ctx = self._retry_context
217
+ if not ctx:
218
+ raise ValueError("Cannot retry input when _retry_context is empty.")
219
+
220
+ item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
221
+ request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
222
+ await retry_transient_errors(
223
+ self.client.stub.FunctionRetryInputs,
224
+ request,
225
+ )
226
+
227
+ async def _get_single_output(self) -> Any:
184
228
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
185
229
  item: api_pb2.FunctionGetOutputsItem = (
186
230
  await self.pop_function_call_outputs(timeout=None, clear_on_success=True)
187
231
  ).outputs[0]
188
232
  return await _process_result(item.result, item.data_format, self.stub, self.client)
189
233
 
234
+ async def run_function(self) -> Any:
235
+ # Use retry logic only if retry policy is specified and
236
+ ctx = self._retry_context
237
+ if (
238
+ not ctx
239
+ or not ctx.retry_policy
240
+ or ctx.retry_policy.retries == 0
241
+ or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
242
+ ):
243
+ return await self._get_single_output()
244
+
245
+ # User errors including timeouts are managed by the user specified retry policy.
246
+ user_retry_manager = RetryManager(ctx.retry_policy)
247
+
248
+ while True:
249
+ try:
250
+ return await self._get_single_output()
251
+ except (UserCodeException, FunctionTimeoutError) as exc:
252
+ await user_retry_manager.raise_or_sleep(exc)
253
+ await self._retry_input()
254
+
190
255
  async def poll_function(self, timeout: Optional[float] = None):
191
256
  """Waits up to timeout for a result from a function.
192
257
 
@@ -278,12 +343,12 @@ class _FunctionSpec:
278
343
  image: Optional[_Image]
279
344
  mounts: Sequence[_Mount]
280
345
  secrets: Sequence[_Secret]
281
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem]
282
- volumes: Dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
283
- gpus: Union[GPU_T, List[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
346
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
347
+ volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
348
+ gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
284
349
  cloud: Optional[str]
285
350
  cpu: Optional[float]
286
- memory: Optional[Union[int, Tuple[int, int]]]
351
+ memory: Optional[Union[int, tuple[int, int]]]
287
352
  ephemeral_disk: Optional[int]
288
353
  scheduler_placement: Optional[SchedulerPlacement]
289
354
 
@@ -304,7 +369,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
304
369
 
305
370
  # TODO: more type annotations
306
371
  _info: Optional[FunctionInfo]
307
- _serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
372
+ _serve_mounts: frozenset[_Mount] # set at load time, only by loader
308
373
  _app: Optional["modal.app._App"] = None
309
374
  _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
310
375
  _web_url: Optional[str]
@@ -323,7 +388,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
323
388
  _use_method_name: str = ""
324
389
 
325
390
  _class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
326
- _method_handle_metadata: Optional[Dict[str, "api_pb2.FunctionHandleMetadata"]] = None
391
+ _method_handle_metadata: Optional[dict[str, "api_pb2.FunctionHandleMetadata"]] = None
327
392
 
328
393
  def _bind_method(
329
394
  self,
@@ -429,14 +494,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
429
494
  secrets: Sequence[_Secret] = (),
430
495
  schedule: Optional[Schedule] = None,
431
496
  is_generator=False,
432
- gpu: Union[GPU_T, List[GPU_T]] = None,
497
+ gpu: Union[GPU_T, list[GPU_T]] = None,
433
498
  # TODO: maybe break this out into a separate decorator for notebooks.
434
499
  mounts: Collection[_Mount] = (),
435
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
500
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
436
501
  allow_cross_region_volumes: bool = False,
437
- volumes: Dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
502
+ volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
438
503
  webhook_config: Optional[api_pb2.WebhookConfig] = None,
439
- memory: Optional[Union[int, Tuple[int, int]]] = None,
504
+ memory: Optional[Union[int, tuple[int, int]]] = None,
440
505
  proxy: Optional[_Proxy] = None,
441
506
  retries: Optional[Union[int, Retries]] = None,
442
507
  timeout: Optional[int] = None,
@@ -623,8 +688,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
623
688
  if image is not None and not isinstance(image, _Image):
624
689
  raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
625
690
 
626
- method_definitions: Optional[Dict[str, api_pb2.MethodDefinition]] = None
627
- partial_functions: Dict[str, "modal.partial_function._PartialFunction"] = {}
691
+ method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
692
+ partial_functions: dict[str, "modal.partial_function._PartialFunction"] = {}
628
693
  if info.user_cls:
629
694
  method_definitions = {}
630
695
  partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
@@ -640,8 +705,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
640
705
 
641
706
  function_type = get_function_type(is_generator)
642
707
 
643
- def _deps(only_explicit_mounts=False) -> List[_Object]:
644
- deps: List[_Object] = list(secrets)
708
+ def _deps(only_explicit_mounts=False) -> list[_Object]:
709
+ deps: list[_Object] = list(secrets)
645
710
  if only_explicit_mounts:
646
711
  # TODO: this is a bit hacky, but all_mounts may differ in the container vs locally
647
712
  # We don't want the function dependencies to change, so we have this way to force it to
@@ -878,7 +943,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
878
943
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
879
944
  raise
880
945
  function_creation_status.set_response(response)
881
- serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
946
+ serve_mounts = {m for m in all_mounts if m.is_local()} # needed for modal.serve file watching
882
947
  serve_mounts |= image._serve_mounts
883
948
  obj._serve_mounts = frozenset(serve_mounts)
884
949
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
@@ -897,7 +962,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
897
962
  obj._spec = function_spec # needed for modal shell
898
963
 
899
964
  # Used to check whether we should rebuild a modal.Image which uses `run_function`.
900
- gpus: List[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
965
+ gpus: list[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
901
966
  obj._build_args = dict( # See get_build_def
902
967
  secrets=repr(secrets),
903
968
  gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
@@ -919,7 +984,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
919
984
  from_other_workspace: bool,
920
985
  options: Optional[api_pb2.FunctionOptions],
921
986
  args: Sized,
922
- kwargs: Dict[str, Any],
987
+ kwargs: dict[str, Any],
923
988
  ) -> "_Function":
924
989
  """mdmd:hidden
925
990
 
@@ -1025,7 +1090,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1025
1090
 
1026
1091
  @classmethod
1027
1092
  def from_name(
1028
- cls: Type["_Function"],
1093
+ cls: type["_Function"],
1029
1094
  app_name: str,
1030
1095
  tag: str,
1031
1096
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
@@ -1232,13 +1297,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1232
1297
  yield item
1233
1298
 
1234
1299
  async def _call_function(self, args, kwargs) -> ReturnType:
1300
+ if config.get("client_retries"):
1301
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
1302
+ else:
1303
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
1235
1304
  invocation = await _Invocation.create(
1236
1305
  self,
1237
1306
  args,
1238
1307
  kwargs,
1239
1308
  client=self._client,
1240
- function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1309
+ function_call_invocation_type=function_call_invocation_type,
1241
1310
  )
1311
+
1242
1312
  return await invocation.run_function()
1243
1313
 
1244
1314
  async def _call_function_nowait(
@@ -1355,12 +1425,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1355
1425
  if is_async(info.raw_f):
1356
1426
  # We want to run __aenter__ and fun in the same coroutine
1357
1427
  async def coro():
1358
- await obj.aenter()
1428
+ await obj._aenter()
1359
1429
  return await fun(*args, **kwargs)
1360
1430
 
1361
1431
  return coro() # type: ignore
1362
1432
  else:
1363
- obj.enter()
1433
+ obj._enter()
1364
1434
  return fun(*args, **kwargs)
1365
1435
 
1366
1436
  @synchronizer.no_input_translation
@@ -1476,7 +1546,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1476
1546
  async for res in self._invocation().run_generator():
1477
1547
  yield res
1478
1548
 
1479
- async def get_call_graph(self) -> List[InputInfo]:
1549
+ async def get_call_graph(self) -> list[InputInfo]:
1480
1550
  """Returns a structure representing the call graph from a given root
1481
1551
  call ID, along with the status of execution for each node.
1482
1552