modal 0.62.16__py3-none-any.whl → 0.72.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 (220) hide show
  1. modal/__init__.py +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -1,165 +1,207 @@
1
+ import collections.abc
1
2
  import modal.client
2
3
  import modal.object
4
+ import modal.volume
3
5
  import modal_proto.api_pb2
4
6
  import pathlib
5
7
  import synchronicity.combined_types
6
8
  import typing
7
9
  import typing_extensions
8
10
 
9
- def network_file_system_mount_protos(validated_network_file_systems: typing.List[typing.Tuple[str, _NetworkFileSystem]], allow_cross_region_volumes: bool) -> typing.List[modal_proto.api_pb2.SharedVolumeMount]:
10
- ...
11
-
11
+ def network_file_system_mount_protos(
12
+ validated_network_file_systems: list[tuple[str, _NetworkFileSystem]], allow_cross_region_volumes: bool
13
+ ) -> list[modal_proto.api_pb2.SharedVolumeMount]: ...
12
14
 
13
15
  class _NetworkFileSystem(modal.object._Object):
14
16
  @staticmethod
15
- def new(cloud: typing.Union[str, None] = None) -> _NetworkFileSystem:
16
- ...
17
-
18
- @staticmethod
19
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> _NetworkFileSystem:
20
- ...
21
-
17
+ def from_name(
18
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
19
+ ) -> _NetworkFileSystem: ...
22
20
  @classmethod
23
- def ephemeral(cls: typing.Type[_NetworkFileSystem], client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None, _heartbeat_sleep: float = 300) -> typing.AsyncContextManager[_NetworkFileSystem]:
24
- ...
25
-
21
+ def ephemeral(
22
+ cls: type[_NetworkFileSystem],
23
+ client: typing.Optional[modal.client._Client] = None,
24
+ environment_name: typing.Optional[str] = None,
25
+ _heartbeat_sleep: float = 300,
26
+ ) -> typing.AsyncContextManager[_NetworkFileSystem]: ...
26
27
  @staticmethod
27
- def persisted(label: str, namespace=1, environment_name: typing.Union[str, None] = None, cloud: typing.Union[str, None] = None) -> _NetworkFileSystem:
28
- ...
29
-
30
- def persist(self, label: str, namespace=1, environment_name: typing.Union[str, None] = None, cloud: typing.Union[str, None] = None):
31
- ...
32
-
28
+ async def lookup(
29
+ name: str,
30
+ namespace=1,
31
+ client: typing.Optional[modal.client._Client] = None,
32
+ environment_name: typing.Optional[str] = None,
33
+ create_if_missing: bool = False,
34
+ ) -> _NetworkFileSystem: ...
33
35
  @staticmethod
34
- 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) -> _NetworkFileSystem:
35
- ...
36
-
36
+ async def create_deployed(
37
+ deployment_name: str,
38
+ namespace=1,
39
+ client: typing.Optional[modal.client._Client] = None,
40
+ environment_name: typing.Optional[str] = None,
41
+ ) -> str: ...
37
42
  @staticmethod
38
- async def create_deployed(deployment_name: str, namespace=1, client: typing.Union[modal.client._Client, None] = None, environment_name: typing.Union[str, None] = None) -> str:
39
- ...
40
-
41
- async def write_file(self, remote_path: str, fp: typing.BinaryIO) -> int:
42
- ...
43
-
44
- def read_file(self, path: str) -> typing.AsyncIterator[bytes]:
45
- ...
46
-
47
- def iterdir(self, path: str) -> typing.AsyncIterator[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
48
- ...
49
-
50
- async def add_local_file(self, local_path: typing.Union[pathlib.Path, str], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None):
51
- ...
52
-
53
- async def add_local_dir(self, local_path: typing.Union[pathlib.Path, str], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None):
54
- ...
55
-
56
- async def listdir(self, path: str) -> typing.List[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
57
- ...
58
-
59
- async def remove_file(self, path: str, recursive=False):
60
- ...
61
-
43
+ async def delete(
44
+ name: str, client: typing.Optional[modal.client._Client] = None, environment_name: typing.Optional[str] = None
45
+ ): ...
46
+ async def write_file(
47
+ self,
48
+ remote_path: str,
49
+ fp: typing.BinaryIO,
50
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
51
+ ) -> int: ...
52
+ def read_file(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
53
+ def iterdir(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
54
+ async def add_local_file(
55
+ self,
56
+ local_path: typing.Union[pathlib.Path, str],
57
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
58
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
59
+ ): ...
60
+ async def add_local_dir(
61
+ self,
62
+ local_path: typing.Union[pathlib.Path, str],
63
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
64
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
65
+ ): ...
66
+ async def listdir(self, path: str) -> list[modal.volume.FileEntry]: ...
67
+ async def remove_file(self, path: str, recursive=False): ...
62
68
 
63
69
  class NetworkFileSystem(modal.object.Object):
64
- def __init__(self, *args, **kwargs):
65
- ...
66
-
70
+ def __init__(self, *args, **kwargs): ...
67
71
  @staticmethod
68
- def new(cloud: typing.Union[str, None] = None) -> NetworkFileSystem:
69
- ...
70
-
71
- @staticmethod
72
- def from_name(label: str, namespace=1, environment_name: typing.Union[str, None] = None, create_if_missing: bool = False) -> NetworkFileSystem:
73
- ...
74
-
72
+ def from_name(
73
+ name: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
74
+ ) -> NetworkFileSystem: ...
75
75
  @classmethod
76
- def ephemeral(cls: typing.Type[NetworkFileSystem], client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None, _heartbeat_sleep: float = 300) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]:
77
- ...
78
-
79
- @staticmethod
80
- def persisted(label: str, namespace=1, environment_name: typing.Union[str, None] = None, cloud: typing.Union[str, None] = None) -> NetworkFileSystem:
81
- ...
82
-
83
- def persist(self, label: str, namespace=1, environment_name: typing.Union[str, None] = None, cloud: typing.Union[str, None] = None):
84
- ...
76
+ def ephemeral(
77
+ cls: type[NetworkFileSystem],
78
+ client: typing.Optional[modal.client.Client] = None,
79
+ environment_name: typing.Optional[str] = None,
80
+ _heartbeat_sleep: float = 300,
81
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]: ...
85
82
 
86
83
  class __lookup_spec(typing_extensions.Protocol):
87
- 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) -> NetworkFileSystem:
88
- ...
89
-
90
- async def aio(self, *args, **kwargs) -> NetworkFileSystem:
91
- ...
84
+ def __call__(
85
+ self,
86
+ name: str,
87
+ namespace=1,
88
+ client: typing.Optional[modal.client.Client] = None,
89
+ environment_name: typing.Optional[str] = None,
90
+ create_if_missing: bool = False,
91
+ ) -> NetworkFileSystem: ...
92
+ async def aio(
93
+ self,
94
+ name: str,
95
+ namespace=1,
96
+ client: typing.Optional[modal.client.Client] = None,
97
+ environment_name: typing.Optional[str] = None,
98
+ create_if_missing: bool = False,
99
+ ) -> NetworkFileSystem: ...
92
100
 
93
101
  lookup: __lookup_spec
94
102
 
95
103
  class __create_deployed_spec(typing_extensions.Protocol):
96
- def __call__(self, deployment_name: str, namespace=1, client: typing.Union[modal.client.Client, None] = None, environment_name: typing.Union[str, None] = None) -> str:
97
- ...
98
-
99
- async def aio(self, *args, **kwargs) -> str:
100
- ...
104
+ def __call__(
105
+ self,
106
+ deployment_name: str,
107
+ namespace=1,
108
+ client: typing.Optional[modal.client.Client] = None,
109
+ environment_name: typing.Optional[str] = None,
110
+ ) -> str: ...
111
+ async def aio(
112
+ self,
113
+ deployment_name: str,
114
+ namespace=1,
115
+ client: typing.Optional[modal.client.Client] = None,
116
+ environment_name: typing.Optional[str] = None,
117
+ ) -> str: ...
101
118
 
102
119
  create_deployed: __create_deployed_spec
103
120
 
104
- class __write_file_spec(typing_extensions.Protocol):
105
- def __call__(self, remote_path: str, fp: typing.BinaryIO) -> int:
106
- ...
121
+ class __delete_spec(typing_extensions.Protocol):
122
+ def __call__(
123
+ self,
124
+ name: str,
125
+ client: typing.Optional[modal.client.Client] = None,
126
+ environment_name: typing.Optional[str] = None,
127
+ ): ...
128
+ async def aio(
129
+ self,
130
+ name: str,
131
+ client: typing.Optional[modal.client.Client] = None,
132
+ environment_name: typing.Optional[str] = None,
133
+ ): ...
134
+
135
+ delete: __delete_spec
107
136
 
108
- async def aio(self, *args, **kwargs) -> int:
109
- ...
137
+ class __write_file_spec(typing_extensions.Protocol):
138
+ def __call__(
139
+ self,
140
+ remote_path: str,
141
+ fp: typing.BinaryIO,
142
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
143
+ ) -> int: ...
144
+ async def aio(
145
+ self,
146
+ remote_path: str,
147
+ fp: typing.BinaryIO,
148
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
149
+ ) -> int: ...
110
150
 
111
151
  write_file: __write_file_spec
112
152
 
113
153
  class __read_file_spec(typing_extensions.Protocol):
114
- def __call__(self, path: str) -> typing.Iterator[bytes]:
115
- ...
116
-
117
- def aio(self, path: str) -> typing.AsyncIterator[bytes]:
118
- ...
154
+ def __call__(self, path: str) -> typing.Iterator[bytes]: ...
155
+ def aio(self, path: str) -> collections.abc.AsyncIterator[bytes]: ...
119
156
 
120
157
  read_file: __read_file_spec
121
158
 
122
159
  class __iterdir_spec(typing_extensions.Protocol):
123
- def __call__(self, path: str) -> typing.Iterator[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
124
- ...
125
-
126
- def aio(self, path: str) -> typing.AsyncIterator[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
127
- ...
160
+ def __call__(self, path: str) -> typing.Iterator[modal.volume.FileEntry]: ...
161
+ def aio(self, path: str) -> collections.abc.AsyncIterator[modal.volume.FileEntry]: ...
128
162
 
129
163
  iterdir: __iterdir_spec
130
164
 
131
165
  class __add_local_file_spec(typing_extensions.Protocol):
132
- def __call__(self, local_path: typing.Union[pathlib.Path, str], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None):
133
- ...
134
-
135
- async def aio(self, *args, **kwargs):
136
- ...
166
+ def __call__(
167
+ self,
168
+ local_path: typing.Union[pathlib.Path, str],
169
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
170
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
171
+ ): ...
172
+ async def aio(
173
+ self,
174
+ local_path: typing.Union[pathlib.Path, str],
175
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
176
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
177
+ ): ...
137
178
 
138
179
  add_local_file: __add_local_file_spec
139
180
 
140
181
  class __add_local_dir_spec(typing_extensions.Protocol):
141
- def __call__(self, local_path: typing.Union[pathlib.Path, str], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None):
142
- ...
143
-
144
- async def aio(self, *args, **kwargs):
145
- ...
182
+ def __call__(
183
+ self,
184
+ local_path: typing.Union[pathlib.Path, str],
185
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
186
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
187
+ ): ...
188
+ async def aio(
189
+ self,
190
+ local_path: typing.Union[pathlib.Path, str],
191
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
192
+ progress_cb: typing.Optional[typing.Callable[..., typing.Any]] = None,
193
+ ): ...
146
194
 
147
195
  add_local_dir: __add_local_dir_spec
148
196
 
149
197
  class __listdir_spec(typing_extensions.Protocol):
150
- def __call__(self, path: str) -> typing.List[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
151
- ...
152
-
153
- async def aio(self, *args, **kwargs) -> typing.List[modal_proto.api_pb2.SharedVolumeListFilesEntry]:
154
- ...
198
+ def __call__(self, path: str) -> list[modal.volume.FileEntry]: ...
199
+ async def aio(self, path: str) -> list[modal.volume.FileEntry]: ...
155
200
 
156
201
  listdir: __listdir_spec
157
202
 
158
203
  class __remove_file_spec(typing_extensions.Protocol):
159
- def __call__(self, path: str, recursive=False):
160
- ...
161
-
162
- async def aio(self, *args, **kwargs):
163
- ...
204
+ def __call__(self, path: str, recursive=False): ...
205
+ async def aio(self, path: str, recursive=False): ...
164
206
 
165
207
  remove_file: __remove_file_spec
modal/object.py CHANGED
@@ -1,24 +1,27 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import uuid
3
+ from collections.abc import Awaitable, Hashable, Sequence
3
4
  from functools import wraps
4
- from typing import Awaitable, Callable, ClassVar, Dict, Hashable, List, Optional, Type, TypeVar
5
+ from typing import Callable, ClassVar, Optional, TypeVar
5
6
 
6
7
  from google.protobuf.message import Message
7
8
 
9
+ from modal._utils.async_utils import aclosing
10
+
8
11
  from ._resolver import Resolver
9
12
  from ._utils.async_utils import synchronize_api
10
13
  from .client import _Client
11
- from .config import config
14
+ from .config import config, logger
12
15
  from .exception import ExecutionError, InvalidError
13
16
 
14
17
  O = TypeVar("O", bound="_Object")
15
18
 
16
19
  _BLOCKING_O = synchronize_api(O)
17
20
 
18
- EPHEMERAL_OBJECT_HEARTBEAT_SLEEP = 300
21
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
19
22
 
20
23
 
21
- def _get_environment_name(environment_name: Optional[str], resolver: Optional[Resolver] = None) -> Optional[str]:
24
+ def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
22
25
  if environment_name:
23
26
  return environment_name
24
27
  elif resolver and resolver.environment_name:
@@ -29,7 +32,7 @@ def _get_environment_name(environment_name: Optional[str], resolver: Optional[Re
29
32
 
30
33
  class _Object:
31
34
  _type_prefix: ClassVar[Optional[str]] = None
32
- _prefix_to_type: ClassVar[Dict[str, type]] = {}
35
+ _prefix_to_type: ClassVar[dict[str, type]] = {}
33
36
 
34
37
  # For constructors
35
38
  _load: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]]
@@ -37,13 +40,14 @@ class _Object:
37
40
  _rep: str
38
41
  _is_another_app: bool
39
42
  _hydrate_lazily: bool
40
- _deps: Optional[Callable[..., List["_Object"]]]
43
+ _deps: Optional[Callable[..., list["_Object"]]]
41
44
  _deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
42
45
 
43
46
  # For hydrated objects
44
47
  _object_id: str
45
48
  _client: _Client
46
49
  _is_hydrated: bool
50
+ _is_rehydrated: bool
47
51
 
48
52
  @classmethod
49
53
  def __init_subclass__(cls, type_prefix: Optional[str] = None):
@@ -62,7 +66,7 @@ class _Object:
62
66
  is_another_app: bool = False,
63
67
  preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
64
68
  hydrate_lazily: bool = False,
65
- deps: Optional[Callable[..., List["_Object"]]] = None,
69
+ deps: Optional[Callable[..., list["_Object"]]] = None,
66
70
  deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
67
71
  ):
68
72
  self._local_uuid = str(uuid.uuid4())
@@ -77,6 +81,7 @@ class _Object:
77
81
  self._object_id = None
78
82
  self._client = None
79
83
  self._is_hydrated = False
84
+ self._is_rehydrated = False
80
85
 
81
86
  self._initialize_from_empty()
82
87
 
@@ -91,7 +96,9 @@ class _Object:
91
96
 
92
97
  def _initialize_from_other(self, other):
93
98
  # default implementation, can be overriden in subclasses
94
- pass
99
+ self._object_id = other._object_id
100
+ self._is_hydrated = other._is_hydrated
101
+ self._client = other._client
95
102
 
96
103
  def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
97
104
  assert isinstance(object_id, str)
@@ -116,17 +123,25 @@ class _Object:
116
123
  # the object_id is already provided by other means
117
124
  return
118
125
 
119
- def _init_from_other(self, other: O):
120
- # Transient use case, see Dict, Queue, and SharedVolume
121
- self._init(other._rep, other._load, other._is_another_app, other._preload)
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
+ )
122
138
 
123
139
  def clone(self: O) -> O:
124
140
  """mdmd:hidden Clone a given hydrated object."""
125
141
 
126
142
  # Object to clone must already be hydrated, otherwise from_loader is more suitable.
127
- assert self._is_hydrated
128
-
129
- obj = _Object.__new__(type(self))
143
+ self._validate_is_hydrated()
144
+ obj = type(self).__new__(type(self))
130
145
  obj._initialize_from_other(self)
131
146
  return obj
132
147
 
@@ -138,7 +153,7 @@ class _Object:
138
153
  is_another_app: bool = False,
139
154
  preload: Optional[Callable[[O, Resolver, Optional[str]], Awaitable[None]]] = None,
140
155
  hydrate_lazily: bool = False,
141
- deps: Optional[Callable[..., List["_Object"]]] = None,
156
+ deps: Optional[Callable[..., Sequence["_Object"]]] = None,
142
157
  deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
143
158
  ):
144
159
  # TODO(erikbern): flip the order of the two first arguments
@@ -146,26 +161,34 @@ class _Object:
146
161
  obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
147
162
  return obj
148
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
+
149
178
  @classmethod
150
179
  def _new_hydrated(
151
- cls: Type[O], object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
180
+ cls: type[O], object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
152
181
  ) -> O:
153
182
  if cls._type_prefix is not None:
154
183
  # This is called directly on a subclass, e.g. Secret.from_id
155
184
  if not object_id.startswith(cls._type_prefix + "-"):
156
185
  raise InvalidError(f"Object {object_id} does not start with {cls._type_prefix}")
157
- prefix = cls._type_prefix
186
+ obj_cls = cls
158
187
  else:
159
188
  # This is called on the base class, e.g. Handle.from_id
160
- parts = object_id.split("-")
161
- if len(parts) != 2:
162
- raise InvalidError(f"Object id {object_id} has no dash in it")
163
- prefix = parts[0]
164
- if prefix not in cls._prefix_to_type:
165
- raise InvalidError(f"Object prefix {prefix} does not correspond to a type")
189
+ obj_cls = cls._get_type_from_id(object_id)
166
190
 
167
191
  # Instantiate provider
168
- obj_cls = cls._prefix_to_type[prefix]
169
192
  obj = _Object.__new__(obj_cls)
170
193
  rep = f"Object({object_id})" # TODO(erikbern): dumb
171
194
  obj._init(rep, is_another_app=is_another_app)
@@ -185,7 +208,7 @@ class _Object:
185
208
  return self._local_uuid
186
209
 
187
210
  @property
188
- def object_id(self):
211
+ def object_id(self) -> str:
189
212
  """mdmd:hidden"""
190
213
  return self._object_id
191
214
 
@@ -195,23 +218,30 @@ class _Object:
195
218
  return self._is_hydrated
196
219
 
197
220
  @property
198
- def deps(self) -> Callable[..., List["_Object"]]:
221
+ def deps(self) -> Callable[..., list["_Object"]]:
199
222
  """mdmd:hidden"""
200
223
  return self._deps if self._deps is not None else lambda: []
201
224
 
202
- async def resolve(self):
225
+ async def resolve(self, client: Optional[_Client] = None):
203
226
  """mdmd:hidden"""
204
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)}")
205
238
  return
206
239
  elif not self._hydrate_lazily:
207
- raise ExecutionError(
208
- "Object has not been hydrated and doesn't support lazy hydration."
209
- " This might happen if an object is defined on a different stub,"
210
- " or if it's on the same stub but it didn't get created because it"
211
- " wasn't defined in global scope."
212
- )
240
+ self._validate_is_hydrated()
213
241
  else:
214
- resolver = Resolver() # TODO: this resolver has no attached Client!
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)
215
245
  await resolver.load(self)
216
246
 
217
247
 
@@ -231,7 +261,8 @@ def live_method_gen(method):
231
261
  @wraps(method)
232
262
  async def wrapped(self, *args, **kwargs):
233
263
  await self.resolve()
234
- async for item in method(self, *args, **kwargs):
235
- yield item
264
+ async with aclosing(method(self, *args, **kwargs)) as stream:
265
+ async for item in stream:
266
+ yield item
236
267
 
237
268
  return wrapped