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
modal/client.pyi CHANGED
@@ -1,157 +1,196 @@
1
- import grpclib.exceptions
1
+ import asyncio.events
2
+ import asyncio.locks
3
+ import collections.abc
4
+ import google.protobuf.message
5
+ import grpclib.client
6
+ import modal._utils.async_utils
2
7
  import modal_proto.api_grpc
8
+ import modal_proto.modal_api_grpc
3
9
  import synchronicity.combined_types
4
10
  import typing
5
11
  import typing_extensions
6
12
 
7
- def _get_metadata(client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str) -> typing.Dict[str, str]:
8
- ...
13
+ def _get_metadata(client_type: int, credentials: typing.Optional[tuple[str, str]], version: str) -> dict[str, str]: ...
9
14
 
15
+ ReturnType = typing.TypeVar("ReturnType")
10
16
 
11
- async def _http_check(url: str, timeout: float) -> str:
12
- ...
13
-
14
-
15
- async def _grpc_exc_string(exc: grpclib.exceptions.GRPCError, method_name: str, server_url: str, timeout: float) -> str:
16
- ...
17
+ RequestType = typing.TypeVar("RequestType", bound="google.protobuf.message.Message")
17
18
 
19
+ ResponseType = typing.TypeVar("ResponseType", bound="google.protobuf.message.Message")
18
20
 
19
21
  class _Client:
20
- def __init__(self, server_url: str, client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str = '0.62.16'):
21
- ...
22
-
23
- @property
24
- def stub(self) -> typing.Union[modal_proto.api_grpc.ModalClientStub, None]:
25
- ...
26
-
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.11"
30
+ ): ...
31
+ def is_closed(self) -> bool: ...
27
32
  @property
28
- def authenticated(self) -> bool:
29
- ...
30
-
31
- async def _open(self):
32
- ...
33
-
34
- async def _close(self):
35
- ...
36
-
37
- def set_pre_stop(self, pre_stop: typing.Callable[[], typing.Awaitable[None]]):
38
- ...
39
-
40
- async def _init(self):
41
- ...
42
-
43
- async def __aenter__(self):
44
- ...
45
-
46
- async def __aexit__(self, exc_type, exc, tb):
47
- ...
48
-
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): ...
49
39
  @classmethod
50
- def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]:
51
- ...
52
-
40
+ def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]: ...
53
41
  @classmethod
54
- async def from_env(cls, _override_config=None) -> _Client:
55
- ...
56
-
42
+ async def from_env(cls, _override_config=None) -> _Client: ...
57
43
  @classmethod
58
- async def from_credentials(cls, token_id: str, token_secret: str) -> _Client:
59
- ...
60
-
44
+ async def from_credentials(cls, token_id: str, token_secret: str) -> _Client: ...
61
45
  @classmethod
62
- async def verify(cls, server_url: str, credentials: typing.Tuple[str, str]) -> None:
63
- ...
64
-
46
+ async def verify(cls, server_url: str, credentials: tuple[str, str]) -> None: ...
65
47
  @classmethod
66
- def set_env_client(cls, client: typing.Union[_Client, None]):
67
- ...
68
-
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]: ...
69
75
 
70
76
  class Client:
71
- def __init__(self, server_url: str, client_type: int, credentials: typing.Union[typing.Tuple[str, str], None], version: str = '0.62.16'):
72
- ...
73
-
74
- @property
75
- def stub(self) -> typing.Union[modal_proto.api_grpc.ModalClientStub, None]:
76
- ...
77
-
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.11"
85
+ ): ...
86
+ def is_closed(self) -> bool: ...
78
87
  @property
79
- def authenticated(self) -> bool:
80
- ...
88
+ def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal: ...
81
89
 
82
90
  class ___open_spec(typing_extensions.Protocol):
83
- def __call__(self):
84
- ...
85
-
86
- async def aio(self, *args, **kwargs):
87
- ...
91
+ def __call__(self): ...
92
+ async def aio(self): ...
88
93
 
89
94
  _open: ___open_spec
90
95
 
91
96
  class ___close_spec(typing_extensions.Protocol):
92
- def __call__(self):
93
- ...
94
-
95
- async def aio(self, *args, **kwargs):
96
- ...
97
+ def __call__(self, prep_for_restore: bool = False): ...
98
+ async def aio(self, prep_for_restore: bool = False): ...
97
99
 
98
100
  _close: ___close_spec
99
101
 
100
- class __set_pre_stop_spec(typing_extensions.Protocol):
101
- def __call__(self, pre_stop: typing.Callable[[], None]):
102
- ...
103
-
104
- def aio(self, pre_stop: typing.Callable[[], typing.Awaitable[None]]):
105
- ...
106
-
107
- set_pre_stop: __set_pre_stop_spec
108
-
109
- class ___init_spec(typing_extensions.Protocol):
110
- def __call__(self):
111
- ...
112
-
113
- async def aio(self, *args, **kwargs):
114
- ...
115
-
116
- _init: ___init_spec
117
-
118
- def __enter__(self):
119
- ...
120
-
121
- async def __aenter__(self, *args, **kwargs):
122
- ...
123
-
124
- def __exit__(self, exc_type, exc, tb):
125
- ...
102
+ class __hello_spec(typing_extensions.Protocol):
103
+ def __call__(self): ...
104
+ async def aio(self): ...
126
105
 
127
- async def __aexit__(self, *args, **kwargs):
128
- ...
106
+ hello: __hello_spec
129
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): ...
130
112
  @classmethod
131
- def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]:
132
- ...
133
-
113
+ def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]: ...
134
114
  @classmethod
135
- def from_env(cls, _override_config=None) -> Client:
136
- ...
137
-
115
+ def from_env(cls, _override_config=None) -> Client: ...
138
116
  @classmethod
139
- def from_credentials(cls, token_id: str, token_secret: str) -> Client:
140
- ...
141
-
117
+ def from_credentials(cls, token_id: str, token_secret: str) -> Client: ...
142
118
  @classmethod
143
- def verify(cls, server_url: str, credentials: typing.Tuple[str, str]) -> None:
144
- ...
145
-
119
+ def verify(cls, server_url: str, credentials: tuple[str, str]) -> None: ...
146
120
  @classmethod
147
- def set_env_client(cls, client: typing.Union[Client, None]):
148
- ...
149
-
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): ...
150
193
 
151
194
  HEARTBEAT_INTERVAL: float
152
195
 
153
196
  HEARTBEAT_TIMEOUT: float
154
-
155
- CLIENT_CREATE_ATTEMPT_TIMEOUT: float
156
-
157
- CLIENT_CREATE_TOTAL_TIMEOUT: float
@@ -1,86 +1,160 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from dataclasses import dataclass
3
- from enum import Enum
4
- from typing import List, Optional, Tuple, Union
3
+ from typing import Optional
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
 
12
- class BucketType(Enum):
13
- S3 = "s3"
14
-
15
- @property
16
- def proto(self):
17
- if self.value == "s3":
18
- return api_pb2.CloudBucketMount.BucketType.S3
19
-
20
-
21
13
  @dataclass
22
14
  class _CloudBucketMount:
23
15
  """Mounts a cloud bucket to your container. Currently supports AWS S3 buckets.
24
16
 
25
- S3 buckets are mounted using [AWS' S3 Mountpoint](https://github.com/awslabs/mountpoint-s3).
17
+ S3 buckets are mounted using [AWS S3 Mountpoint](https://github.com/awslabs/mountpoint-s3).
26
18
  S3 mounts are optimized for reading large files sequentially. It does not support every file operation; consult
27
- [the AWS S3 Mountpoin 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.
28
21
 
29
- **Usage**
22
+ **AWS S3 Usage**
30
23
 
31
24
  ```python
32
- import modal
33
25
  import subprocess
34
26
 
35
- stub = modal.Stub()
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
+
34
+ @app.function(
35
+ volumes={
36
+ "/my-mount": modal.CloudBucketMount(
37
+ bucket_name="s3-bucket-name",
38
+ secret=secret,
39
+ read_only=True
40
+ )
41
+ }
42
+ )
43
+ def f():
44
+ subprocess.run(["ls", "/my-mount"], check=True)
45
+ ```
46
+
47
+ **Cloudflare R2 Usage**
48
+
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.
51
+
52
+ ```python
53
+ import subprocess
54
+
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
+ )
36
60
 
37
- @stub.function(
61
+ @app.function(
38
62
  volumes={
39
- "/my-mount": modal.CloudBucketMount("s3-bucket-name", secret=modal.Secret.from_dict({
40
- "AWS_ACCESS_KEY_ID": "...",
41
- "AWS_SECRET_ACCESS_KEY": "...",
42
- }), read_only=True)
63
+ "/my-mount": modal.CloudBucketMount(
64
+ bucket_name="my-r2-bucket",
65
+ bucket_endpoint_url="https://<ACCOUNT ID>.r2.cloudflarestorage.com",
66
+ secret=secret,
67
+ read_only=True
68
+ )
43
69
  }
44
70
  )
45
71
  def f():
46
- subprocess.run(["ls", "/my-mount"])
72
+ subprocess.run(["ls", "/my-mount"], check=True)
73
+ ```
74
+
75
+ **Google GCS Usage**
76
+
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).
80
+
81
+ ```python
82
+ import subprocess
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
+
90
+ @app.function(
91
+ volumes={
92
+ "/my-mount": modal.CloudBucketMount(
93
+ bucket_name="my-gcs-bucket",
94
+ bucket_endpoint_url="https://storage.googleapis.com",
95
+ secret=gcp_hmac_secret,
96
+ )
97
+ }
98
+ )
99
+ def f():
100
+ subprocess.run(["ls", "/my-mount"], check=True)
47
101
  ```
48
102
  """
49
103
 
50
104
  bucket_name: str
105
+ # Endpoint URL is used to support Cloudflare R2 and Google Cloud Platform GCS.
106
+ bucket_endpoint_url: Optional[str] = None
107
+
108
+ key_prefix: Optional[str] = None
51
109
 
52
- # Credentials used to access a cloud bucket. When
53
- # The given secret can contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
54
- # AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY can be omitted if the bucket is publicly accessible.
110
+ # Credentials used to access a cloud bucket.
111
+ # If the bucket is private, the secret **must** contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
112
+ # If the bucket is publicly accessible, the secret is unnecessary and can be omitted.
55
113
  secret: Optional[_Secret] = None
56
114
 
57
115
  read_only: bool = False
58
116
  requester_pays: bool = False
59
- bucket_type: Union[
60
- BucketType, str
61
- ] = BucketType.S3.value # S3 is the default bucket type until other cloud buckets are supported
62
117
 
63
118
 
64
- 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]:
65
120
  """Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server."""
66
- cloud_bucket_mounts: List[api_pb2.CloudBucketMount] = []
121
+ cloud_bucket_mounts: list[api_pb2.CloudBucketMount] = []
67
122
 
68
123
  for path, mount in mounts:
69
- if isinstance(mount.bucket_type, str):
70
- bucket_type = BucketType(mount.bucket_type)
124
+ # crude mapping from mount arguments to type.
125
+ if mount.bucket_endpoint_url:
126
+ parse_result = urlparse(mount.bucket_endpoint_url)
127
+ if parse_result.hostname.endswith("r2.cloudflarestorage.com"):
128
+ bucket_type = api_pb2.CloudBucketMount.BucketType.R2
129
+ elif parse_result.hostname.endswith("storage.googleapis.com"):
130
+ bucket_type = api_pb2.CloudBucketMount.BucketType.GCP
131
+ else:
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
71
137
  else:
72
- bucket_type = mount.bucket_type
138
+ # just assume S3; this is backwards and forwards compatible.
139
+ bucket_type = api_pb2.CloudBucketMount.BucketType.S3
73
140
 
74
141
  if mount.requester_pays and not mount.secret:
75
142
  raise ValueError("Credentials required in order to use Requester Pays.")
76
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
+
77
149
  cloud_bucket_mount = api_pb2.CloudBucketMount(
78
150
  bucket_name=mount.bucket_name,
151
+ bucket_endpoint_url=mount.bucket_endpoint_url,
79
152
  mount_path=path,
80
153
  credentials_secret_id=mount.secret.object_id if mount.secret else "",
81
154
  read_only=mount.read_only,
82
- bucket_type=bucket_type.proto,
155
+ bucket_type=bucket_type,
83
156
  requester_pays=mount.requester_pays,
157
+ key_prefix=key_prefix,
84
158
  )
85
159
  cloud_bucket_mounts.append(cloud_bucket_mount)
86
160
 
@@ -1,53 +1,47 @@
1
- import enum
2
1
  import modal.secret
3
2
  import modal_proto.api_pb2
4
3
  import typing
5
4
 
6
- class BucketType(enum.Enum):
7
- def _generate_next_value_(name, start, count, last_values):
8
- ...
9
-
10
- @property
11
- def proto(self):
12
- ...
13
-
14
- def __new__(cls, value):
15
- ...
16
-
17
-
18
5
  class _CloudBucketMount:
19
6
  bucket_name: str
20
- 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]
21
10
  read_only: bool
22
11
  requester_pays: bool
23
- bucket_type: typing.Union[BucketType, str]
24
-
25
- def __init__(self, bucket_name: str, secret: typing.Union[modal.secret._Secret, None] = None, read_only: bool = False, requester_pays: bool = False, bucket_type: typing.Union[BucketType, str] = 's3') -> None:
26
- ...
27
-
28
- def __repr__(self):
29
- ...
30
-
31
- def __eq__(self, other):
32
- ...
33
-
34
-
35
- def cloud_bucket_mounts_to_proto(mounts: typing.List[typing.Tuple[str, _CloudBucketMount]]) -> typing.List[modal_proto.api_pb2.CloudBucketMount]:
36
- ...
37
12
 
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]: ...
38
28
 
39
29
  class CloudBucketMount:
40
30
  bucket_name: str
41
- 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]
42
34
  read_only: bool
43
35
  requester_pays: bool
44
- bucket_type: typing.Union[BucketType, str]
45
-
46
- def __init__(self, bucket_name: str, secret: typing.Union[modal.secret.Secret, None] = None, read_only: bool = False, requester_pays: bool = False, bucket_type: typing.Union[BucketType, str] = 's3') -> None:
47
- ...
48
-
49
- def __repr__(self):
50
- ...
51
36
 
52
- def __eq__(self, other):
53
- ...
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): ...