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/client.pyi CHANGED
@@ -1,164 +1,196 @@
1
+ import asyncio.events
1
2
  import asyncio.locks
2
- import grpclib.exceptions
3
+ import collections.abc
4
+ import google.protobuf.message
5
+ import grpclib.client
6
+ import modal._utils.async_utils
3
7
  import modal_proto.api_grpc
8
+ import modal_proto.modal_api_grpc
4
9
  import synchronicity.combined_types
5
10
  import typing
6
11
  import typing_extensions
7
12
 
8
- def _get_metadata(client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str) -> typing.Dict[str, str]:
9
- ...
13
+ def _get_metadata(client_type: int, credentials: typing.Optional[tuple[str, str]], version: str) -> dict[str, str]: ...
10
14
 
15
+ ReturnType = typing.TypeVar("ReturnType")
11
16
 
12
- async def _http_check(url: str, timeout: float) -> str:
13
- ...
14
-
15
-
16
- async def _grpc_exc_string(exc: grpclib.exceptions.GRPCError, method_name: str, server_url: str, timeout: float) -> str:
17
- ...
17
+ RequestType = typing.TypeVar("RequestType", bound="google.protobuf.message.Message")
18
18
 
19
+ ResponseType = typing.TypeVar("ResponseType", bound="google.protobuf.message.Message")
19
20
 
20
21
  class _Client:
21
- _client_from_env: typing.ClassVar[typing.Union[_Client, None]]
22
- _client_from_env_lock: typing.ClassVar[typing.Union[asyncio.locks.Lock, None]]
23
-
24
- def __init__(self, server_url: str, client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str = '0.62.115'):
25
- ...
26
-
27
- @property
28
- def stub(self) -> typing.Union[modal_proto.api_grpc.ModalClientStub, None]:
29
- ...
30
-
22
+ _client_from_env: typing.ClassVar[typing.Optional[_Client]]
23
+ _client_from_env_lock: typing.ClassVar[typing.Optional[asyncio.locks.Lock]]
24
+ _cancellation_context: modal._utils.async_utils.TaskContext
25
+ _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
26
+ _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
+
28
+ def __init__(
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.13"
30
+ ): ...
31
+ def is_closed(self) -> bool: ...
31
32
  @property
32
- def authenticated(self) -> bool:
33
- ...
34
-
35
- async def _open(self):
36
- ...
37
-
38
- async def _close(self):
39
- ...
40
-
41
- def set_pre_stop(self, pre_stop: typing.Callable[[], typing.Awaitable[None]]):
42
- ...
43
-
44
- async def _init(self):
45
- ...
46
-
47
- async def __aenter__(self):
48
- ...
49
-
50
- async def __aexit__(self, exc_type, exc, tb):
51
- ...
52
-
33
+ def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal: ...
34
+ async def _open(self): ...
35
+ async def _close(self, prep_for_restore: bool = False): ...
36
+ async def hello(self): ...
37
+ async def __aenter__(self): ...
38
+ async def __aexit__(self, exc_type, exc, tb): ...
53
39
  @classmethod
54
- def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]:
55
- ...
56
-
40
+ def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]: ...
57
41
  @classmethod
58
- async def from_env(cls, _override_config=None) -> _Client:
59
- ...
60
-
42
+ async def from_env(cls, _override_config=None) -> _Client: ...
61
43
  @classmethod
62
- async def from_credentials(cls, token_id: str, token_secret: str) -> _Client:
63
- ...
64
-
44
+ async def from_credentials(cls, token_id: str, token_secret: str) -> _Client: ...
65
45
  @classmethod
66
- async def verify(cls, server_url: str, credentials: typing.Tuple[str, str]) -> None:
67
- ...
68
-
46
+ async def verify(cls, server_url: str, credentials: tuple[str, str]) -> None: ...
69
47
  @classmethod
70
- def set_env_client(cls, client: typing.Union[_Client, None]):
71
- ...
72
-
48
+ def set_env_client(cls, client: typing.Optional[_Client]): ...
49
+ async def _call_safely(self, coro, readable_method: str): ...
50
+ async def _reset_on_pid_change(self): ...
51
+ async def _get_grpclib_method(self, method_name: str) -> typing.Any: ...
52
+ async def _call_unary(
53
+ self,
54
+ method_name: str,
55
+ request: typing.Any,
56
+ *,
57
+ timeout: typing.Optional[float] = None,
58
+ metadata: typing.Union[
59
+ collections.abc.Mapping[str, typing.Union[str, bytes]],
60
+ collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
61
+ None,
62
+ ] = None,
63
+ ) -> typing.Any: ...
64
+ def _call_stream(
65
+ self,
66
+ method_name: str,
67
+ request: typing.Any,
68
+ *,
69
+ metadata: typing.Union[
70
+ collections.abc.Mapping[str, typing.Union[str, bytes]],
71
+ collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
72
+ None,
73
+ ],
74
+ ) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
73
75
 
74
76
  class Client:
75
- _client_from_env: typing.ClassVar[typing.Union[Client, None]]
76
- _client_from_env_lock: typing.ClassVar[typing.Union[asyncio.locks.Lock, None]]
77
-
78
- def __init__(self, server_url: str, client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str = '0.62.115'):
79
- ...
80
-
77
+ _client_from_env: typing.ClassVar[typing.Optional[Client]]
78
+ _client_from_env_lock: typing.ClassVar[typing.Optional[asyncio.locks.Lock]]
79
+ _cancellation_context: modal._utils.async_utils.TaskContext
80
+ _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
81
+ _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
+
83
+ def __init__(
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.13"
85
+ ): ...
86
+ def is_closed(self) -> bool: ...
81
87
  @property
82
- def stub(self) -> typing.Union[modal_proto.api_grpc.ModalClientStub, None]:
83
- ...
84
-
85
- @property
86
- def authenticated(self) -> bool:
87
- ...
88
+ def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal: ...
88
89
 
89
90
  class ___open_spec(typing_extensions.Protocol):
90
- def __call__(self):
91
- ...
92
-
93
- async def aio(self, *args, **kwargs):
94
- ...
91
+ def __call__(self): ...
92
+ async def aio(self): ...
95
93
 
96
94
  _open: ___open_spec
97
95
 
98
96
  class ___close_spec(typing_extensions.Protocol):
99
- def __call__(self):
100
- ...
101
-
102
- async def aio(self, *args, **kwargs):
103
- ...
97
+ def __call__(self, prep_for_restore: bool = False): ...
98
+ async def aio(self, prep_for_restore: bool = False): ...
104
99
 
105
100
  _close: ___close_spec
106
101
 
107
- class __set_pre_stop_spec(typing_extensions.Protocol):
108
- def __call__(self, pre_stop: typing.Callable[[], None]):
109
- ...
110
-
111
- def aio(self, pre_stop: typing.Callable[[], typing.Awaitable[None]]):
112
- ...
113
-
114
- set_pre_stop: __set_pre_stop_spec
115
-
116
- class ___init_spec(typing_extensions.Protocol):
117
- def __call__(self):
118
- ...
119
-
120
- async def aio(self, *args, **kwargs):
121
- ...
122
-
123
- _init: ___init_spec
124
-
125
- def __enter__(self):
126
- ...
127
-
128
- async def __aenter__(self, *args, **kwargs):
129
- ...
102
+ class __hello_spec(typing_extensions.Protocol):
103
+ def __call__(self): ...
104
+ async def aio(self): ...
130
105
 
131
- def __exit__(self, exc_type, exc, tb):
132
- ...
133
-
134
- async def __aexit__(self, *args, **kwargs):
135
- ...
106
+ hello: __hello_spec
136
107
 
108
+ def __enter__(self): ...
109
+ async def __aenter__(self): ...
110
+ def __exit__(self, exc_type, exc, tb): ...
111
+ async def __aexit__(self, exc_type, exc, tb): ...
137
112
  @classmethod
138
- def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]:
139
- ...
140
-
113
+ def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]: ...
141
114
  @classmethod
142
- def from_env(cls, _override_config=None) -> Client:
143
- ...
144
-
115
+ def from_env(cls, _override_config=None) -> Client: ...
145
116
  @classmethod
146
- def from_credentials(cls, token_id: str, token_secret: str) -> Client:
147
- ...
148
-
117
+ def from_credentials(cls, token_id: str, token_secret: str) -> Client: ...
149
118
  @classmethod
150
- def verify(cls, server_url: str, credentials: typing.Tuple[str, str]) -> None:
151
- ...
152
-
119
+ def verify(cls, server_url: str, credentials: tuple[str, str]) -> None: ...
153
120
  @classmethod
154
- def set_env_client(cls, client: typing.Union[Client, None]):
155
- ...
156
-
121
+ def set_env_client(cls, client: typing.Optional[Client]): ...
122
+
123
+ class ___call_safely_spec(typing_extensions.Protocol):
124
+ def __call__(self, coro, readable_method: str): ...
125
+ async def aio(self, coro, readable_method: str): ...
126
+
127
+ _call_safely: ___call_safely_spec
128
+
129
+ class ___reset_on_pid_change_spec(typing_extensions.Protocol):
130
+ def __call__(self): ...
131
+ async def aio(self): ...
132
+
133
+ _reset_on_pid_change: ___reset_on_pid_change_spec
134
+
135
+ class ___get_grpclib_method_spec(typing_extensions.Protocol):
136
+ def __call__(self, method_name: str) -> typing.Any: ...
137
+ async def aio(self, method_name: str) -> typing.Any: ...
138
+
139
+ _get_grpclib_method: ___get_grpclib_method_spec
140
+
141
+ async def _call_unary(
142
+ self,
143
+ method_name: str,
144
+ request: typing.Any,
145
+ *,
146
+ timeout: typing.Optional[float] = None,
147
+ metadata: typing.Union[
148
+ collections.abc.Mapping[str, typing.Union[str, bytes]],
149
+ collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
150
+ None,
151
+ ] = None,
152
+ ) -> typing.Any: ...
153
+ def _call_stream(
154
+ self,
155
+ method_name: str,
156
+ request: typing.Any,
157
+ *,
158
+ metadata: typing.Union[
159
+ collections.abc.Mapping[str, typing.Union[str, bytes]],
160
+ collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
161
+ None,
162
+ ],
163
+ ) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
164
+
165
+ class UnaryUnaryWrapper(typing.Generic[RequestType, ResponseType]):
166
+ wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]
167
+ client: _Client
168
+
169
+ def __init__(self, wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType], client: _Client): ...
170
+ @property
171
+ def name(self) -> str: ...
172
+ async def __call__(
173
+ self,
174
+ req: RequestType,
175
+ *,
176
+ timeout: typing.Optional[float] = None,
177
+ metadata: typing.Union[
178
+ collections.abc.Mapping[str, typing.Union[str, bytes]],
179
+ collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
180
+ None,
181
+ ] = None,
182
+ ) -> ResponseType: ...
183
+
184
+ class UnaryStreamWrapper(typing.Generic[RequestType, ResponseType]):
185
+ wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType]
186
+
187
+ def __init__(
188
+ self, wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType], client: _Client
189
+ ): ...
190
+ @property
191
+ def name(self) -> str: ...
192
+ def unary_stream(self, request, metadata: typing.Optional[typing.Any] = None): ...
157
193
 
158
194
  HEARTBEAT_INTERVAL: float
159
195
 
160
196
  HEARTBEAT_TIMEOUT: float
161
-
162
- CLIENT_CREATE_ATTEMPT_TIMEOUT: float
163
-
164
- CLIENT_CREATE_TOTAL_TIMEOUT: float
@@ -1,11 +1,12 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from dataclasses import dataclass
3
- from typing import List, Optional, Tuple
3
+ from typing import Optional
4
4
  from urllib.parse import urlparse
5
5
 
6
6
  from modal_proto import api_pb2
7
7
 
8
8
  from ._utils.async_utils import synchronize_api
9
+ from .config import logger
9
10
  from .secret import _Secret
10
11
 
11
12
 
@@ -15,18 +16,21 @@ class _CloudBucketMount:
15
16
 
16
17
  S3 buckets are mounted using [AWS S3 Mountpoint](https://github.com/awslabs/mountpoint-s3).
17
18
  S3 mounts are optimized for reading large files sequentially. It does not support every file operation; consult
18
- [the AWS S3 Mountpoint documentation](https://github.com/awslabs/mountpoint-s3/blob/main/doc/SEMANTICS.md) for more information.
19
+ [the AWS S3 Mountpoint documentation](https://github.com/awslabs/mountpoint-s3/blob/main/doc/SEMANTICS.md)
20
+ for more information.
19
21
 
20
22
  **AWS S3 Usage**
21
23
 
22
24
  ```python
23
25
  import subprocess
24
26
 
25
- app = modal.App() # Note: "app" was called "stub" up until April 2024
26
- secret = modal.Secret.from_dict({
27
- "AWS_ACCESS_KEY_ID": "...",
28
- "AWS_SECRET_ACCESS_KEY": "...",
29
- })
27
+ app = modal.App()
28
+ secret = modal.Secret.from_name(
29
+ "aws-secret",
30
+ required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
31
+ # Note: providing AWS_REGION can help when automatic detection of the bucket region fails.
32
+ )
33
+
30
34
  @app.function(
31
35
  volumes={
32
36
  "/my-mount": modal.CloudBucketMount(
@@ -42,17 +46,18 @@ class _CloudBucketMount:
42
46
 
43
47
  **Cloudflare R2 Usage**
44
48
 
45
- Cloudflare R2 is [S3-compatible](https://developers.cloudflare.com/r2/api/s3/api/) so its setup looks very similar to S3.
46
- But additionally the `bucket_endpoint_url` argument must be passed.
49
+ Cloudflare R2 is [S3-compatible](https://developers.cloudflare.com/r2/api/s3/api/) so its setup looks
50
+ very similar to S3. But additionally the `bucket_endpoint_url` argument must be passed.
47
51
 
48
52
  ```python
49
53
  import subprocess
50
54
 
51
- app = modal.App() # Note: "app" was called "stub" up until April 2024
52
- secret = modal.Secret.from_dict({
53
- "AWS_ACCESS_KEY_ID": "...",
54
- "AWS_SECRET_ACCESS_KEY": "...",
55
- })
55
+ app = modal.App()
56
+ secret = modal.Secret.from_name(
57
+ "r2-secret",
58
+ required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
59
+ )
60
+
56
61
  @app.function(
57
62
  volumes={
58
63
  "/my-mount": modal.CloudBucketMount(
@@ -69,25 +74,25 @@ class _CloudBucketMount:
69
74
 
70
75
  **Google GCS Usage**
71
76
 
72
- Google Cloud Storage (GCS) is partially [S3-compatible](https://cloud.google.com/storage/docs/interoperability).
73
- Currently **only `read_only=True`** is supported for GCS buckets. GCS Buckets also require a secret with Google-specific
74
- key names (see below) populated with a [HMAC key](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create).
77
+ Google Cloud Storage (GCS) is [S3-compatible](https://cloud.google.com/storage/docs/interoperability).
78
+ GCS Buckets also require a secret with Google-specific key names (see below) populated with
79
+ a [HMAC key](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create).
75
80
 
76
81
  ```python
77
82
  import subprocess
78
83
 
79
- app = modal.App() # Note: "app" was called "stub" up until April 2024
80
- gcp_hmac_secret = modal.Secret.from_dict({
81
- "GOOGLE_ACCESS_KEY_ID": "GOOG1ERM12345...",
82
- "GOOGLE_ACCESS_KEY_SECRET": "HTJ123abcdef...",
83
- })
84
+ app = modal.App()
85
+ gcp_hmac_secret = modal.Secret.from_name(
86
+ "gcp-secret",
87
+ required_keys=["GOOGLE_ACCESS_KEY_ID", "GOOGLE_ACCESS_KEY_SECRET"]
88
+ )
89
+
84
90
  @app.function(
85
91
  volumes={
86
92
  "/my-mount": modal.CloudBucketMount(
87
93
  bucket_name="my-gcs-bucket",
88
94
  bucket_endpoint_url="https://storage.googleapis.com",
89
95
  secret=gcp_hmac_secret,
90
- read_only=True, # writing to bucket currently unsupported
91
96
  )
92
97
  }
93
98
  )
@@ -100,6 +105,8 @@ class _CloudBucketMount:
100
105
  # Endpoint URL is used to support Cloudflare R2 and Google Cloud Platform GCS.
101
106
  bucket_endpoint_url: Optional[str] = None
102
107
 
108
+ key_prefix: Optional[str] = None
109
+
103
110
  # Credentials used to access a cloud bucket.
104
111
  # If the bucket is private, the secret **must** contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
105
112
  # If the bucket is publicly accessible, the secret is unnecessary and can be omitted.
@@ -109,9 +116,9 @@ class _CloudBucketMount:
109
116
  requester_pays: bool = False
110
117
 
111
118
 
112
- def cloud_bucket_mounts_to_proto(mounts: List[Tuple[str, _CloudBucketMount]]) -> List[api_pb2.CloudBucketMount]:
119
+ def cloud_bucket_mounts_to_proto(mounts: list[tuple[str, _CloudBucketMount]]) -> list[api_pb2.CloudBucketMount]:
113
120
  """Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server."""
114
- cloud_bucket_mounts: List[api_pb2.CloudBucketMount] = []
121
+ cloud_bucket_mounts: list[api_pb2.CloudBucketMount] = []
115
122
 
116
123
  for path, mount in mounts:
117
124
  # crude mapping from mount arguments to type.
@@ -121,12 +128,12 @@ def cloud_bucket_mounts_to_proto(mounts: List[Tuple[str, _CloudBucketMount]]) ->
121
128
  bucket_type = api_pb2.CloudBucketMount.BucketType.R2
122
129
  elif parse_result.hostname.endswith("storage.googleapis.com"):
123
130
  bucket_type = api_pb2.CloudBucketMount.BucketType.GCP
124
- if not mount.read_only:
125
- raise ValueError(
126
- f"CloudBucketMount of '{mount.bucket_name}' is invalid. Writing to GCP buckets with modal.CloudBucketMount in currently unsupported."
127
- )
128
131
  else:
129
- raise ValueError(f"Unsupported bucket endpoint hostname '{parse_result.hostname}'")
132
+ logger.warning(
133
+ "CloudBucketMount received unrecognized bucket endpoint URL. "
134
+ "Assuming AWS S3 configuration as fallback."
135
+ )
136
+ bucket_type = api_pb2.CloudBucketMount.BucketType.S3
130
137
  else:
131
138
  # just assume S3; this is backwards and forwards compatible.
132
139
  bucket_type = api_pb2.CloudBucketMount.BucketType.S3
@@ -134,6 +141,11 @@ def cloud_bucket_mounts_to_proto(mounts: List[Tuple[str, _CloudBucketMount]]) ->
134
141
  if mount.requester_pays and not mount.secret:
135
142
  raise ValueError("Credentials required in order to use Requester Pays.")
136
143
 
144
+ if mount.key_prefix and not mount.key_prefix.endswith("/"):
145
+ raise ValueError("key_prefix will be prefixed to all object paths, so it must end in a '/'")
146
+ else:
147
+ key_prefix = mount.key_prefix
148
+
137
149
  cloud_bucket_mount = api_pb2.CloudBucketMount(
138
150
  bucket_name=mount.bucket_name,
139
151
  bucket_endpoint_url=mount.bucket_endpoint_url,
@@ -142,6 +154,7 @@ def cloud_bucket_mounts_to_proto(mounts: List[Tuple[str, _CloudBucketMount]]) ->
142
154
  read_only=mount.read_only,
143
155
  bucket_type=bucket_type,
144
156
  requester_pays=mount.requester_pays,
157
+ key_prefix=key_prefix,
145
158
  )
146
159
  cloud_bucket_mounts.append(cloud_bucket_mount)
147
160
 
@@ -4,37 +4,44 @@ import typing
4
4
 
5
5
  class _CloudBucketMount:
6
6
  bucket_name: str
7
- bucket_endpoint_url: typing.Union[str, None]
8
- secret: typing.Union[modal.secret._Secret, None]
7
+ bucket_endpoint_url: typing.Optional[str]
8
+ key_prefix: typing.Optional[str]
9
+ secret: typing.Optional[modal.secret._Secret]
9
10
  read_only: bool
10
11
  requester_pays: bool
11
12
 
12
- def __init__(self, bucket_name: str, bucket_endpoint_url: typing.Union[str, None] = None, secret: typing.Union[modal.secret._Secret, None] = None, read_only: bool = False, requester_pays: bool = False) -> None:
13
- ...
14
-
15
- def __repr__(self):
16
- ...
17
-
18
- def __eq__(self, other):
19
- ...
20
-
21
-
22
- def cloud_bucket_mounts_to_proto(mounts: typing.List[typing.Tuple[str, _CloudBucketMount]]) -> typing.List[modal_proto.api_pb2.CloudBucketMount]:
23
- ...
24
-
13
+ def __init__(
14
+ self,
15
+ bucket_name: str,
16
+ bucket_endpoint_url: typing.Optional[str] = None,
17
+ key_prefix: typing.Optional[str] = None,
18
+ secret: typing.Optional[modal.secret._Secret] = None,
19
+ read_only: bool = False,
20
+ requester_pays: bool = False,
21
+ ) -> None: ...
22
+ def __repr__(self): ...
23
+ def __eq__(self, other): ...
24
+
25
+ def cloud_bucket_mounts_to_proto(
26
+ mounts: list[tuple[str, _CloudBucketMount]],
27
+ ) -> list[modal_proto.api_pb2.CloudBucketMount]: ...
25
28
 
26
29
  class CloudBucketMount:
27
30
  bucket_name: str
28
- bucket_endpoint_url: typing.Union[str, None]
29
- secret: typing.Union[modal.secret.Secret, None]
31
+ bucket_endpoint_url: typing.Optional[str]
32
+ key_prefix: typing.Optional[str]
33
+ secret: typing.Optional[modal.secret.Secret]
30
34
  read_only: bool
31
35
  requester_pays: bool
32
36
 
33
- def __init__(self, bucket_name: str, bucket_endpoint_url: typing.Union[str, None] = None, secret: typing.Union[modal.secret.Secret, None] = None, read_only: bool = False, requester_pays: bool = False) -> None:
34
- ...
35
-
36
- def __repr__(self):
37
- ...
38
-
39
- def __eq__(self, other):
40
- ...
37
+ def __init__(
38
+ self,
39
+ bucket_name: str,
40
+ bucket_endpoint_url: typing.Optional[str] = None,
41
+ key_prefix: typing.Optional[str] = None,
42
+ secret: typing.Optional[modal.secret.Secret] = None,
43
+ read_only: bool = False,
44
+ requester_pays: bool = False,
45
+ ) -> None: ...
46
+ def __repr__(self): ...
47
+ def __eq__(self, other): ...