modal 0.62.115__py3-none-any.whl → 0.72.13__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 (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/queue.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
@@ -5,196 +6,270 @@ import typing
5
6
  import typing_extensions
6
7
 
7
8
  class _Queue(modal.object._Object):
9
+ def __init__(self): ...
8
10
  @staticmethod
9
- def new():
10
- ...
11
-
12
- def __init__(self):
13
- ...
14
-
15
- @staticmethod
16
- def validate_partition_key(partition: typing.Union[str, None]) -> bytes:
17
- ...
18
-
11
+ def validate_partition_key(partition: typing.Optional[str]) -> bytes: ...
19
12
  @classmethod
20
- def ephemeral(cls: typing.Type[_Queue], client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None, _heartbeat_sleep: float = 300) -> typing.AsyncContextManager[_Queue]:
21
- ...
22
-
13
+ def ephemeral(
14
+ cls: type[_Queue],
15
+ client: typing.Optional[modal.client._Client] = None,
16
+ environment_name: typing.Optional[str] = None,
17
+ _heartbeat_sleep: float = 300,
18
+ ) -> typing.AsyncContextManager[_Queue]: ...
23
19
  @staticmethod
24
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> _Queue:
25
- ...
26
-
20
+ def from_name(
21
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
22
+ ) -> _Queue: ...
27
23
  @staticmethod
28
- def persisted(label: str, namespace=1, environment_name: typing.Union[str, None] = None) -> _Queue:
29
- ...
30
-
31
- @staticmethod
32
- async def lookup(label: str, namespace=1, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> _Queue:
33
- ...
34
-
24
+ async def lookup(
25
+ name: str,
26
+ namespace=1,
27
+ client: typing.Optional[modal.client._Client] = None,
28
+ environment_name: typing.Optional[str] = None,
29
+ create_if_missing: bool = False,
30
+ ) -> _Queue: ...
35
31
  @staticmethod
36
- async def delete(label: str, *, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None):
37
- ...
38
-
39
- async def _get_nonblocking(self, partition: typing.Union[str, None], n_values: int) -> typing.List[typing.Any]:
40
- ...
41
-
42
- async def _get_blocking(self, partition: typing.Union[str, None], timeout: typing.Union[float, None], n_values: int) -> typing.List[typing.Any]:
43
- ...
44
-
45
- async def get(self, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None) -> typing.Union[typing.Any, None]:
46
- ...
47
-
48
- async def get_many(self, n_values: int, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None) -> typing.List[typing.Any]:
49
- ...
50
-
51
- async def put(self, v: typing.Any, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None, partition_ttl: int = 86400) -> None:
52
- ...
53
-
54
- async def put_many(self, vs: typing.List[typing.Any], block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None, partition_ttl: int = 86400) -> None:
55
- ...
56
-
57
- async def _put_many_blocking(self, partition: typing.Union[str, None], partition_ttl: int, vs: typing.List[typing.Any], timeout: typing.Union[float, None] = None):
58
- ...
59
-
60
- async def _put_many_nonblocking(self, partition: typing.Union[str, None], partition_ttl: int, vs: typing.List[typing.Any]):
61
- ...
62
-
63
- async def len(self, *, partition: typing.Union[str, None] = None) -> int:
64
- ...
65
-
66
- def iterate(self, *, partition: typing.Union[str, None] = None, item_poll_timeout: float = 0.0) -> typing.AsyncGenerator[typing.Any, None]:
67
- ...
68
-
32
+ async def delete(
33
+ name: str,
34
+ *,
35
+ client: typing.Optional[modal.client._Client] = None,
36
+ environment_name: typing.Optional[str] = None,
37
+ ): ...
38
+ async def _get_nonblocking(self, partition: typing.Optional[str], n_values: int) -> list[typing.Any]: ...
39
+ async def _get_blocking(
40
+ self, partition: typing.Optional[str], timeout: typing.Optional[float], n_values: int
41
+ ) -> list[typing.Any]: ...
42
+ async def clear(self, *, partition: typing.Optional[str] = None, all: bool = False) -> None: ...
43
+ async def get(
44
+ self, block: bool = True, timeout: typing.Optional[float] = None, *, partition: typing.Optional[str] = None
45
+ ) -> typing.Optional[typing.Any]: ...
46
+ async def get_many(
47
+ self,
48
+ n_values: int,
49
+ block: bool = True,
50
+ timeout: typing.Optional[float] = None,
51
+ *,
52
+ partition: typing.Optional[str] = None,
53
+ ) -> list[typing.Any]: ...
54
+ async def put(
55
+ self,
56
+ v: typing.Any,
57
+ block: bool = True,
58
+ timeout: typing.Optional[float] = None,
59
+ *,
60
+ partition: typing.Optional[str] = None,
61
+ partition_ttl: int = 86400,
62
+ ) -> None: ...
63
+ async def put_many(
64
+ self,
65
+ vs: list[typing.Any],
66
+ block: bool = True,
67
+ timeout: typing.Optional[float] = None,
68
+ *,
69
+ partition: typing.Optional[str] = None,
70
+ partition_ttl: int = 86400,
71
+ ) -> None: ...
72
+ async def _put_many_blocking(
73
+ self,
74
+ partition: typing.Optional[str],
75
+ partition_ttl: int,
76
+ vs: list[typing.Any],
77
+ timeout: typing.Optional[float] = None,
78
+ ): ...
79
+ async def _put_many_nonblocking(
80
+ self, partition: typing.Optional[str], partition_ttl: int, vs: list[typing.Any]
81
+ ): ...
82
+ async def len(self, *, partition: typing.Optional[str] = None, total: bool = False) -> int: ...
83
+ def iterate(
84
+ self, *, partition: typing.Optional[str] = None, item_poll_timeout: float = 0.0
85
+ ) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
69
86
 
70
87
  class Queue(modal.object.Object):
71
- def __init__(self):
72
- ...
73
-
88
+ def __init__(self): ...
74
89
  @staticmethod
75
- def new():
76
- ...
77
-
78
- @staticmethod
79
- def validate_partition_key(partition: typing.Union[str, None]) -> bytes:
80
- ...
81
-
90
+ def validate_partition_key(partition: typing.Optional[str]) -> bytes: ...
82
91
  @classmethod
83
- def ephemeral(cls: typing.Type[Queue], client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None, _heartbeat_sleep: float = 300) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Queue]:
84
- ...
85
-
86
- @staticmethod
87
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> Queue:
88
- ...
89
-
92
+ def ephemeral(
93
+ cls: type[Queue],
94
+ client: typing.Optional[modal.client.Client] = None,
95
+ environment_name: typing.Optional[str] = None,
96
+ _heartbeat_sleep: float = 300,
97
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Queue]: ...
90
98
  @staticmethod
91
- def persisted(label: str, namespace=1, environment_name: typing.Union[str, None] = None) -> Queue:
92
- ...
99
+ def from_name(
100
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
101
+ ) -> Queue: ...
93
102
 
94
103
  class __lookup_spec(typing_extensions.Protocol):
95
- def __call__(self, label: str, namespace=1, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> Queue:
96
- ...
97
-
98
- async def aio(self, *args, **kwargs) -> Queue:
99
- ...
104
+ def __call__(
105
+ self,
106
+ name: str,
107
+ namespace=1,
108
+ client: typing.Optional[modal.client.Client] = None,
109
+ environment_name: typing.Optional[str] = None,
110
+ create_if_missing: bool = False,
111
+ ) -> Queue: ...
112
+ async def aio(
113
+ self,
114
+ name: str,
115
+ namespace=1,
116
+ client: typing.Optional[modal.client.Client] = None,
117
+ environment_name: typing.Optional[str] = None,
118
+ create_if_missing: bool = False,
119
+ ) -> Queue: ...
100
120
 
101
121
  lookup: __lookup_spec
102
122
 
103
123
  class __delete_spec(typing_extensions.Protocol):
104
- def __call__(self, label: str, *, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None):
105
- ...
106
-
107
- async def aio(self, *args, **kwargs):
108
- ...
124
+ def __call__(
125
+ self,
126
+ name: str,
127
+ *,
128
+ client: typing.Optional[modal.client.Client] = None,
129
+ environment_name: typing.Optional[str] = None,
130
+ ): ...
131
+ async def aio(
132
+ self,
133
+ name: str,
134
+ *,
135
+ client: typing.Optional[modal.client.Client] = None,
136
+ environment_name: typing.Optional[str] = None,
137
+ ): ...
109
138
 
110
139
  delete: __delete_spec
111
140
 
112
141
  class ___get_nonblocking_spec(typing_extensions.Protocol):
113
- def __call__(self, partition: typing.Union[str, None], n_values: int) -> typing.List[typing.Any]:
114
- ...
115
-
116
- async def aio(self, *args, **kwargs) -> typing.List[typing.Any]:
117
- ...
142
+ def __call__(self, partition: typing.Optional[str], n_values: int) -> list[typing.Any]: ...
143
+ async def aio(self, partition: typing.Optional[str], n_values: int) -> list[typing.Any]: ...
118
144
 
119
145
  _get_nonblocking: ___get_nonblocking_spec
120
146
 
121
147
  class ___get_blocking_spec(typing_extensions.Protocol):
122
- def __call__(self, partition: typing.Union[str, None], timeout: typing.Union[float, None], n_values: int) -> typing.List[typing.Any]:
123
- ...
124
-
125
- async def aio(self, *args, **kwargs) -> typing.List[typing.Any]:
126
- ...
148
+ def __call__(
149
+ self, partition: typing.Optional[str], timeout: typing.Optional[float], n_values: int
150
+ ) -> list[typing.Any]: ...
151
+ async def aio(
152
+ self, partition: typing.Optional[str], timeout: typing.Optional[float], n_values: int
153
+ ) -> list[typing.Any]: ...
127
154
 
128
155
  _get_blocking: ___get_blocking_spec
129
156
 
130
- class __get_spec(typing_extensions.Protocol):
131
- def __call__(self, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None) -> typing.Union[typing.Any, None]:
132
- ...
157
+ class __clear_spec(typing_extensions.Protocol):
158
+ def __call__(self, *, partition: typing.Optional[str] = None, all: bool = False) -> None: ...
159
+ async def aio(self, *, partition: typing.Optional[str] = None, all: bool = False) -> None: ...
160
+
161
+ clear: __clear_spec
133
162
 
134
- async def aio(self, *args, **kwargs) -> typing.Union[typing.Any, None]:
135
- ...
163
+ class __get_spec(typing_extensions.Protocol):
164
+ def __call__(
165
+ self, block: bool = True, timeout: typing.Optional[float] = None, *, partition: typing.Optional[str] = None
166
+ ) -> typing.Optional[typing.Any]: ...
167
+ async def aio(
168
+ self, block: bool = True, timeout: typing.Optional[float] = None, *, partition: typing.Optional[str] = None
169
+ ) -> typing.Optional[typing.Any]: ...
136
170
 
137
171
  get: __get_spec
138
172
 
139
173
  class __get_many_spec(typing_extensions.Protocol):
140
- def __call__(self, n_values: int, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None) -> typing.List[typing.Any]:
141
- ...
142
-
143
- async def aio(self, *args, **kwargs) -> typing.List[typing.Any]:
144
- ...
174
+ def __call__(
175
+ self,
176
+ n_values: int,
177
+ block: bool = True,
178
+ timeout: typing.Optional[float] = None,
179
+ *,
180
+ partition: typing.Optional[str] = None,
181
+ ) -> list[typing.Any]: ...
182
+ async def aio(
183
+ self,
184
+ n_values: int,
185
+ block: bool = True,
186
+ timeout: typing.Optional[float] = None,
187
+ *,
188
+ partition: typing.Optional[str] = None,
189
+ ) -> list[typing.Any]: ...
145
190
 
146
191
  get_many: __get_many_spec
147
192
 
148
193
  class __put_spec(typing_extensions.Protocol):
149
- def __call__(self, v: typing.Any, block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None, partition_ttl: int = 86400) -> None:
150
- ...
151
-
152
- async def aio(self, *args, **kwargs) -> None:
153
- ...
194
+ def __call__(
195
+ self,
196
+ v: typing.Any,
197
+ block: bool = True,
198
+ timeout: typing.Optional[float] = None,
199
+ *,
200
+ partition: typing.Optional[str] = None,
201
+ partition_ttl: int = 86400,
202
+ ) -> None: ...
203
+ async def aio(
204
+ self,
205
+ v: typing.Any,
206
+ block: bool = True,
207
+ timeout: typing.Optional[float] = None,
208
+ *,
209
+ partition: typing.Optional[str] = None,
210
+ partition_ttl: int = 86400,
211
+ ) -> None: ...
154
212
 
155
213
  put: __put_spec
156
214
 
157
215
  class __put_many_spec(typing_extensions.Protocol):
158
- def __call__(self, vs: typing.List[typing.Any], block: bool = True, timeout: typing.Union[float, None] = None, *, partition: typing.Union[str, None] = None, partition_ttl: int = 86400) -> None:
159
- ...
160
-
161
- async def aio(self, *args, **kwargs) -> None:
162
- ...
216
+ def __call__(
217
+ self,
218
+ vs: list[typing.Any],
219
+ block: bool = True,
220
+ timeout: typing.Optional[float] = None,
221
+ *,
222
+ partition: typing.Optional[str] = None,
223
+ partition_ttl: int = 86400,
224
+ ) -> None: ...
225
+ async def aio(
226
+ self,
227
+ vs: list[typing.Any],
228
+ block: bool = True,
229
+ timeout: typing.Optional[float] = None,
230
+ *,
231
+ partition: typing.Optional[str] = None,
232
+ partition_ttl: int = 86400,
233
+ ) -> None: ...
163
234
 
164
235
  put_many: __put_many_spec
165
236
 
166
237
  class ___put_many_blocking_spec(typing_extensions.Protocol):
167
- def __call__(self, partition: typing.Union[str, None], partition_ttl: int, vs: typing.List[typing.Any], timeout: typing.Union[float, None] = None):
168
- ...
169
-
170
- async def aio(self, *args, **kwargs):
171
- ...
238
+ def __call__(
239
+ self,
240
+ partition: typing.Optional[str],
241
+ partition_ttl: int,
242
+ vs: list[typing.Any],
243
+ timeout: typing.Optional[float] = None,
244
+ ): ...
245
+ async def aio(
246
+ self,
247
+ partition: typing.Optional[str],
248
+ partition_ttl: int,
249
+ vs: list[typing.Any],
250
+ timeout: typing.Optional[float] = None,
251
+ ): ...
172
252
 
173
253
  _put_many_blocking: ___put_many_blocking_spec
174
254
 
175
255
  class ___put_many_nonblocking_spec(typing_extensions.Protocol):
176
- def __call__(self, partition: typing.Union[str, None], partition_ttl: int, vs: typing.List[typing.Any]):
177
- ...
178
-
179
- async def aio(self, *args, **kwargs):
180
- ...
256
+ def __call__(self, partition: typing.Optional[str], partition_ttl: int, vs: list[typing.Any]): ...
257
+ async def aio(self, partition: typing.Optional[str], partition_ttl: int, vs: list[typing.Any]): ...
181
258
 
182
259
  _put_many_nonblocking: ___put_many_nonblocking_spec
183
260
 
184
261
  class __len_spec(typing_extensions.Protocol):
185
- def __call__(self, *, partition: typing.Union[str, None] = None) -> int:
186
- ...
187
-
188
- async def aio(self, *args, **kwargs) -> int:
189
- ...
262
+ def __call__(self, *, partition: typing.Optional[str] = None, total: bool = False) -> int: ...
263
+ async def aio(self, *, partition: typing.Optional[str] = None, total: bool = False) -> int: ...
190
264
 
191
265
  len: __len_spec
192
266
 
193
267
  class __iterate_spec(typing_extensions.Protocol):
194
- def __call__(self, *, partition: typing.Union[str, None] = None, item_poll_timeout: float = 0.0) -> typing.Generator[typing.Any, None, None]:
195
- ...
196
-
197
- def aio(self, *, partition: typing.Union[str, None] = None, item_poll_timeout: float = 0.0) -> typing.AsyncGenerator[typing.Any, None]:
198
- ...
268
+ def __call__(
269
+ self, *, partition: typing.Optional[str] = None, item_poll_timeout: float = 0.0
270
+ ) -> typing.Generator[typing.Any, None, None]: ...
271
+ def aio(
272
+ self, *, partition: typing.Optional[str] = None, item_poll_timeout: float = 0.0
273
+ ) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
199
274
 
200
275
  iterate: __iterate_spec
@@ -17,10 +17,11 @@ idna==3.6
17
17
  markdown-it-py==3.0.0
18
18
  mdurl==0.1.2
19
19
  multidict==6.0.5
20
- protobuf==5.26.1
20
+ protobuf==4.25.3
21
21
  pydantic==2.6.4
22
22
  pydantic_core==2.16.3
23
23
  Pygments==2.17.2
24
+ python-multipart==0.0.9
24
25
  rich==13.7.1
25
26
  sniffio==1.3.1
26
27
  starlette==0.36.3
@@ -0,0 +1,16 @@
1
+ aiohappyeyeballs==2.4.3
2
+ aiohttp==3.10.8
3
+ aiosignal==1.3.1
4
+ async-timeout==4.0.3 ; python_version < "3.11"
5
+ attrs==24.2.0
6
+ certifi==2024.8.30
7
+ frozenlist==1.4.1
8
+ grpclib==0.4.7
9
+ h2==4.1.0
10
+ hpack==4.0.0
11
+ hyperframe==6.0.1
12
+ idna==3.10
13
+ multidict==6.1.0
14
+ protobuf>=3.20,<6
15
+ typing_extensions==4.12.2
16
+ yarl==1.13.1
@@ -0,0 +1,21 @@
1
+ # Modal Image builder configuration
2
+
3
+ This directory contains `modal.Image` specifications that vary across
4
+ "image builder" versions.
5
+
6
+ The `base-images.json` file specifies the versions used for Modal's
7
+ various `Image` constructor methods.
8
+
9
+ The versioned requirements files enumerate the dependencies needed by
10
+ the Modal client library when it is running inside a Modal container.
11
+
12
+ The container requirements are a subset of the dependencies required by the
13
+ client for local operation (i.e., to run or deploy Modal apps). Additionally,
14
+ we aim to pin specific versions rather than allowing a range as we do for the
15
+ installation dependencies.
16
+
17
+ From version `2024.04`, the requirements specify the entire dependency tree,
18
+ and not just the first-order dependencies.
19
+
20
+ Note that for `2023.12`, there is a separate requirements file that is used for
21
+ Python 3.12.
@@ -0,0 +1,22 @@
1
+ {
2
+ "debian": {
3
+ "2024.10": "bookworm",
4
+ "2024.04": "bookworm",
5
+ "2023.12": "bullseye"
6
+ },
7
+ "python": {
8
+ "2024.10": ["3.9.20", "3.10.15", "3.11.10", "3.12.6", "3.13.0"],
9
+ "2024.04": ["3.9.19", "3.10.14", "3.11.8", "3.12.2"],
10
+ "2023.12": ["3.9.15", "3.10.8", "3.11.0", "3.12.1"]
11
+ },
12
+ "micromamba": {
13
+ "2024.10": "1.5.10",
14
+ "2024.04": "1.5.8",
15
+ "2023.12": "1.3.1"
16
+ },
17
+ "package_tools": {
18
+ "2024.10": "pip wheel uv",
19
+ "2024.04": "pip wheel uv",
20
+ "2023.12": "pip"
21
+ }
22
+ }
modal/retries.py CHANGED
@@ -1,10 +1,14 @@
1
1
  # Copyright Modal Labs 2022
2
+ import asyncio
2
3
  from datetime import timedelta
3
4
 
4
5
  from modal_proto import api_pb2
5
6
 
6
7
  from .exception import InvalidError
7
8
 
9
+ MIN_INPUT_RETRY_DELAY_MS = 1000
10
+ MAX_INPUT_RETRY_DELAY_MS = 24 * 60 * 60 * 1000
11
+
8
12
 
9
13
  class Retries:
10
14
  """Adds a retry policy to a Modal function.
@@ -13,7 +17,7 @@ class Retries:
13
17
 
14
18
  ```python
15
19
  import modal
16
- app = modal.App() # Note: called "stub" up until April 2024
20
+ app = modal.App()
17
21
 
18
22
  # Basic configuration.
19
23
  # This sets a policy of max 4 retries with 1-second delay between failures.
@@ -53,14 +57,16 @@ class Retries:
53
57
  # The maximum number of retries that can be made in the presence of failures.
54
58
  max_retries: int,
55
59
  # Coefficent controlling how much the retry delay increases each retry attempt.
56
- # A backoff coefficient of 1.0 creates fixed-delay retries where the delay period will always equal the initial delay.
60
+ # A backoff coefficient of 1.0 creates fixed-delay where the delay period always equals the initial delay.
57
61
  backoff_coefficient: float = 2.0,
58
62
  # Number of seconds that must elapse before the first retry occurs.
59
63
  initial_delay: float = 1.0,
60
64
  # Maximum length of retry delay in seconds, preventing the delay from growing infinitely.
61
65
  max_delay: float = 60.0,
62
66
  ):
63
- """Construct a new retries policy, supporting exponential and fixed-interval delays via a backoff coefficient."""
67
+ """
68
+ Construct a new retries policy, supporting exponential and fixed-interval delays via a backoff coefficient.
69
+ """
64
70
  if max_retries < 0:
65
71
  raise InvalidError(f"Invalid retries number: {max_retries}. Function retries must be non-negative.")
66
72
 
@@ -84,7 +90,8 @@ class Retries:
84
90
 
85
91
  if not 1.0 <= backoff_coefficient <= 10.0:
86
92
  raise InvalidError(
87
- f"Invalid backoff_coefficient: {backoff_coefficient}. Coefficient must be between 1.0 (fixed-interval backoff) and 10.0"
93
+ f"Invalid backoff_coefficient: {backoff_coefficient}. "
94
+ "Coefficient must be between 1.0 (fixed-interval backoff) and 10.0"
88
95
  )
89
96
 
90
97
  self.max_retries = max_retries
@@ -100,3 +107,37 @@ class Retries:
100
107
  initial_delay_ms=self.initial_delay // timedelta(milliseconds=1),
101
108
  max_delay_ms=self.max_delay // timedelta(milliseconds=1),
102
109
  )
110
+
111
+
112
+ class RetryManager:
113
+ """
114
+ Helper class to apply the specified retry policy.
115
+ """
116
+
117
+ def __init__(self, retry_policy: api_pb2.FunctionRetryPolicy):
118
+ self.retry_policy = retry_policy
119
+ self.attempt_count = 0
120
+
121
+ async def raise_or_sleep(self, exc: Exception):
122
+ """
123
+ Raises an exception if the maximum retry count has been reached, otherwise sleeps for calculated delay.
124
+ """
125
+ self.attempt_count += 1
126
+ if self.attempt_count > self.retry_policy.retries:
127
+ raise exc
128
+ delay_ms = self._retry_delay_ms(self.attempt_count, self.retry_policy)
129
+ await asyncio.sleep(delay_ms / 1000)
130
+
131
+ @staticmethod
132
+ def _retry_delay_ms(attempt_count: int, retry_policy: api_pb2.FunctionRetryPolicy) -> float:
133
+ """
134
+ Computes the amount of time to sleep before retrying based on the backend_coefficient and initial_delay_ms args.
135
+ """
136
+ if attempt_count < 1:
137
+ raise ValueError(f"Cannot compute retry delay. attempt_count must be at least 1, but was {attempt_count}")
138
+ delay_ms = retry_policy.initial_delay_ms * (retry_policy.backoff_coefficient ** (attempt_count - 1))
139
+ if delay_ms < MIN_INPUT_RETRY_DELAY_MS:
140
+ return MIN_INPUT_RETRY_DELAY_MS
141
+ if delay_ms > MAX_INPUT_RETRY_DELAY_MS:
142
+ return MAX_INPUT_RETRY_DELAY_MS
143
+ return delay_ms