modal 1.0.4.dev10__py3-none-any.whl → 1.0.5__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.
- modal/_clustered_functions.pyi +13 -3
- modal/_functions.py +84 -46
- modal/_partial_function.py +1 -1
- modal/_runtime/container_io_manager.pyi +222 -40
- modal/_runtime/execution_context.pyi +60 -6
- modal/_serialization.py +25 -2
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +1 -1
- modal/_utils/blob_utils.py +56 -19
- modal/_utils/function_utils.py +33 -7
- modal/_utils/grpc_utils.py +11 -4
- modal/app.py +5 -5
- modal/app.pyi +658 -48
- modal/cli/run.py +2 -1
- modal/client.pyi +224 -36
- modal/cloud_bucket_mount.pyi +192 -4
- modal/cls.py +57 -16
- modal/cls.pyi +442 -34
- modal/container_process.pyi +103 -14
- modal/dict.py +4 -4
- modal/dict.pyi +453 -51
- modal/environments.pyi +41 -9
- modal/exception.py +6 -2
- modal/experimental/__init__.py +90 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.pyi +236 -45
- modal/functions.pyi +573 -65
- modal/gpu.py +1 -1
- modal/image.py +1 -1
- modal/image.pyi +1256 -74
- modal/io_streams.py +8 -4
- modal/io_streams.pyi +348 -38
- modal/mount.pyi +261 -31
- modal/network_file_system.py +3 -3
- modal/network_file_system.pyi +307 -26
- modal/object.pyi +48 -9
- modal/parallel_map.py +93 -19
- modal/parallel_map.pyi +160 -15
- modal/partial_function.pyi +255 -14
- modal/proxy.py +1 -1
- modal/proxy.pyi +28 -3
- modal/queue.py +4 -4
- modal/queue.pyi +447 -30
- modal/runner.pyi +160 -22
- modal/sandbox.py +8 -7
- modal/sandbox.pyi +310 -50
- modal/schedule.py +1 -1
- modal/secret.py +2 -2
- modal/secret.pyi +164 -15
- modal/snapshot.pyi +25 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +41 -4
- modal/volume.pyi +693 -59
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/METADATA +3 -3
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/RECORD +67 -67
- modal_proto/api.proto +57 -0
- modal_proto/api_grpc.py +48 -0
- modal_proto/api_pb2.py +874 -780
- modal_proto/api_pb2.pyi +198 -9
- modal_proto/api_pb2_grpc.py +100 -0
- modal_proto/api_pb2_grpc.pyi +32 -0
- modal_proto/modal_api_grpc.py +3 -0
- modal_version/__init__.py +1 -1
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/WHEEL +0 -0
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/entry_points.txt +0 -0
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/top_level.txt +0 -0
modal/cli/run.py
CHANGED
@@ -579,7 +579,8 @@ def shell(
|
|
579
579
|
modal shell hello_world.py::my_function
|
580
580
|
```
|
581
581
|
|
582
|
-
Or, if you're using a [modal.Cls](/docs/reference/modal.Cls)
|
582
|
+
Or, if you're using a [modal.Cls](https://modal.com/docs/reference/modal.Cls)
|
583
|
+
you can refer to a `@modal.method` directly:
|
583
584
|
|
584
585
|
```
|
585
586
|
modal shell hello_world.py::MyClass.my_method
|
modal/client.pyi
CHANGED
@@ -27,32 +27,94 @@ class _Client:
|
|
27
27
|
_snapshotted: bool
|
28
28
|
|
29
29
|
def __init__(
|
30
|
-
self,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.0.5"
|
31
|
+
):
|
32
|
+
"""mdmd:hidden
|
33
|
+
The Modal client object is not intended to be instantiated directly by users.
|
34
|
+
"""
|
35
|
+
...
|
36
|
+
|
36
37
|
def is_closed(self) -> bool: ...
|
37
38
|
@property
|
38
|
-
def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
|
39
|
-
|
39
|
+
def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
|
40
|
+
"""mdmd:hidden
|
41
|
+
The default stub. Stubs can safely be used across forks / client snapshots.
|
42
|
+
|
43
|
+
This is useful if you want to make requests to the default Modal server in us-east, for example
|
44
|
+
control plane requests.
|
45
|
+
|
46
|
+
This is equivalent to client.get_stub(default_server_url), but it's cached, so it's a bit faster.
|
47
|
+
"""
|
48
|
+
...
|
49
|
+
|
50
|
+
async def get_stub(self, server_url: str) -> modal_proto.modal_api_grpc.ModalClientModal:
|
51
|
+
"""mdmd:hidden
|
52
|
+
Get a stub for a specific server URL. Stubs can safely be used across forks / client snapshots.
|
53
|
+
|
54
|
+
This is useful if you want to make requests to a regional Modal server, for example low-latency
|
55
|
+
function calls in us-west.
|
56
|
+
|
57
|
+
This function is O(n) where n is the number of RPCs in ModalClient.
|
58
|
+
"""
|
59
|
+
...
|
60
|
+
|
40
61
|
async def _open(self): ...
|
41
62
|
async def _close(self, prep_for_restore: bool = False): ...
|
42
|
-
async def hello(self):
|
63
|
+
async def hello(self):
|
64
|
+
"""Connect to server and retrieve version information; raise appropriate error for various failures."""
|
65
|
+
...
|
66
|
+
|
43
67
|
async def __aenter__(self): ...
|
44
68
|
async def __aexit__(self, exc_type, exc, tb): ...
|
45
69
|
@classmethod
|
46
|
-
def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]:
|
70
|
+
def anonymous(cls, server_url: str) -> typing.AsyncContextManager[_Client]:
|
71
|
+
"""mdmd:hidden
|
72
|
+
Create a connection with no credentials; to be used for token creation.
|
73
|
+
"""
|
74
|
+
...
|
75
|
+
|
47
76
|
@classmethod
|
48
|
-
async def from_env(cls, _override_config=None) -> _Client:
|
77
|
+
async def from_env(cls, _override_config=None) -> _Client:
|
78
|
+
"""mdmd:hidden
|
79
|
+
Singleton that is instantiated from the Modal config and reused on subsequent calls.
|
80
|
+
"""
|
81
|
+
...
|
82
|
+
|
49
83
|
@classmethod
|
50
|
-
async def from_credentials(cls, token_id: str, token_secret: str) -> _Client:
|
84
|
+
async def from_credentials(cls, token_id: str, token_secret: str) -> _Client:
|
85
|
+
"""Constructor based on token credentials; useful for managing Modal on behalf of third-party users.
|
86
|
+
|
87
|
+
**Usage:**
|
88
|
+
|
89
|
+
```python notest
|
90
|
+
client = modal.Client.from_credentials("my_token_id", "my_token_secret")
|
91
|
+
|
92
|
+
modal.Sandbox.create("echo", "hi", client=client, app=app)
|
93
|
+
```
|
94
|
+
"""
|
95
|
+
...
|
96
|
+
|
51
97
|
@classmethod
|
52
|
-
async def verify(cls, server_url: str, credentials: tuple[str, str]) -> None:
|
98
|
+
async def verify(cls, server_url: str, credentials: tuple[str, str]) -> None:
|
99
|
+
"""mdmd:hidden
|
100
|
+
Check whether can the client can connect to this server with these credentials; raise if not.
|
101
|
+
"""
|
102
|
+
...
|
103
|
+
|
53
104
|
@classmethod
|
54
|
-
def set_env_client(cls, client: typing.Optional[_Client]):
|
55
|
-
|
105
|
+
def set_env_client(cls, client: typing.Optional[_Client]):
|
106
|
+
"""mdmd:hidden"""
|
107
|
+
...
|
108
|
+
|
109
|
+
async def _call_safely(self, coro, readable_method: str):
|
110
|
+
"""Runs coroutine wrapped in a task that's part of the client's task context
|
111
|
+
|
112
|
+
* Raises ClientClosed in case the client is closed while the coroutine is executed
|
113
|
+
* Logs warning if call is made outside of the event loop that the client is running in,
|
114
|
+
and execute without the cancellation context in that case
|
115
|
+
"""
|
116
|
+
...
|
117
|
+
|
56
118
|
async def _reset_on_pid_change(self): ...
|
57
119
|
async def _get_channel(self, server_url: str) -> grpclib.client.Channel: ...
|
58
120
|
async def _call_unary(
|
@@ -90,19 +152,48 @@ class Client:
|
|
90
152
|
_snapshotted: bool
|
91
153
|
|
92
154
|
def __init__(
|
93
|
-
self,
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
155
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.0.5"
|
156
|
+
):
|
157
|
+
"""mdmd:hidden
|
158
|
+
The Modal client object is not intended to be instantiated directly by users.
|
159
|
+
"""
|
160
|
+
...
|
161
|
+
|
99
162
|
def is_closed(self) -> bool: ...
|
100
163
|
@property
|
101
|
-
def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
|
164
|
+
def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal:
|
165
|
+
"""mdmd:hidden
|
166
|
+
The default stub. Stubs can safely be used across forks / client snapshots.
|
167
|
+
|
168
|
+
This is useful if you want to make requests to the default Modal server in us-east, for example
|
169
|
+
control plane requests.
|
170
|
+
|
171
|
+
This is equivalent to client.get_stub(default_server_url), but it's cached, so it's a bit faster.
|
172
|
+
"""
|
173
|
+
...
|
102
174
|
|
103
175
|
class __get_stub_spec(typing_extensions.Protocol[SUPERSELF]):
|
104
|
-
def __call__(self, /, server_url: str) -> modal_proto.modal_api_grpc.ModalClientModal:
|
105
|
-
|
176
|
+
def __call__(self, /, server_url: str) -> modal_proto.modal_api_grpc.ModalClientModal:
|
177
|
+
"""mdmd:hidden
|
178
|
+
Get a stub for a specific server URL. Stubs can safely be used across forks / client snapshots.
|
179
|
+
|
180
|
+
This is useful if you want to make requests to a regional Modal server, for example low-latency
|
181
|
+
function calls in us-west.
|
182
|
+
|
183
|
+
This function is O(n) where n is the number of RPCs in ModalClient.
|
184
|
+
"""
|
185
|
+
...
|
186
|
+
|
187
|
+
async def aio(self, /, server_url: str) -> modal_proto.modal_api_grpc.ModalClientModal:
|
188
|
+
"""mdmd:hidden
|
189
|
+
Get a stub for a specific server URL. Stubs can safely be used across forks / client snapshots.
|
190
|
+
|
191
|
+
This is useful if you want to make requests to a regional Modal server, for example low-latency
|
192
|
+
function calls in us-west.
|
193
|
+
|
194
|
+
This function is O(n) where n is the number of RPCs in ModalClient.
|
195
|
+
"""
|
196
|
+
...
|
106
197
|
|
107
198
|
get_stub: __get_stub_spec[typing_extensions.Self]
|
108
199
|
|
@@ -119,8 +210,13 @@ class Client:
|
|
119
210
|
_close: ___close_spec[typing_extensions.Self]
|
120
211
|
|
121
212
|
class __hello_spec(typing_extensions.Protocol[SUPERSELF]):
|
122
|
-
def __call__(self, /):
|
123
|
-
|
213
|
+
def __call__(self, /):
|
214
|
+
"""Connect to server and retrieve version information; raise appropriate error for various failures."""
|
215
|
+
...
|
216
|
+
|
217
|
+
async def aio(self, /):
|
218
|
+
"""Connect to server and retrieve version information; raise appropriate error for various failures."""
|
219
|
+
...
|
124
220
|
|
125
221
|
hello: __hello_spec[typing_extensions.Self]
|
126
222
|
|
@@ -129,19 +225,63 @@ class Client:
|
|
129
225
|
def __exit__(self, exc_type, exc, tb): ...
|
130
226
|
async def __aexit__(self, exc_type, exc, tb): ...
|
131
227
|
@classmethod
|
132
|
-
def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]:
|
228
|
+
def anonymous(cls, server_url: str) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Client]:
|
229
|
+
"""mdmd:hidden
|
230
|
+
Create a connection with no credentials; to be used for token creation.
|
231
|
+
"""
|
232
|
+
...
|
233
|
+
|
133
234
|
@classmethod
|
134
|
-
def from_env(cls, _override_config=None) -> Client:
|
235
|
+
def from_env(cls, _override_config=None) -> Client:
|
236
|
+
"""mdmd:hidden
|
237
|
+
Singleton that is instantiated from the Modal config and reused on subsequent calls.
|
238
|
+
"""
|
239
|
+
...
|
240
|
+
|
135
241
|
@classmethod
|
136
|
-
def from_credentials(cls, token_id: str, token_secret: str) -> Client:
|
242
|
+
def from_credentials(cls, token_id: str, token_secret: str) -> Client:
|
243
|
+
"""Constructor based on token credentials; useful for managing Modal on behalf of third-party users.
|
244
|
+
|
245
|
+
**Usage:**
|
246
|
+
|
247
|
+
```python notest
|
248
|
+
client = modal.Client.from_credentials("my_token_id", "my_token_secret")
|
249
|
+
|
250
|
+
modal.Sandbox.create("echo", "hi", client=client, app=app)
|
251
|
+
```
|
252
|
+
"""
|
253
|
+
...
|
254
|
+
|
137
255
|
@classmethod
|
138
|
-
def verify(cls, server_url: str, credentials: tuple[str, str]) -> None:
|
256
|
+
def verify(cls, server_url: str, credentials: tuple[str, str]) -> None:
|
257
|
+
"""mdmd:hidden
|
258
|
+
Check whether can the client can connect to this server with these credentials; raise if not.
|
259
|
+
"""
|
260
|
+
...
|
261
|
+
|
139
262
|
@classmethod
|
140
|
-
def set_env_client(cls, client: typing.Optional[Client]):
|
263
|
+
def set_env_client(cls, client: typing.Optional[Client]):
|
264
|
+
"""mdmd:hidden"""
|
265
|
+
...
|
141
266
|
|
142
267
|
class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
|
143
|
-
def __call__(self, /, coro, readable_method: str):
|
144
|
-
|
268
|
+
def __call__(self, /, coro, readable_method: str):
|
269
|
+
"""Runs coroutine wrapped in a task that's part of the client's task context
|
270
|
+
|
271
|
+
* Raises ClientClosed in case the client is closed while the coroutine is executed
|
272
|
+
* Logs warning if call is made outside of the event loop that the client is running in,
|
273
|
+
and execute without the cancellation context in that case
|
274
|
+
"""
|
275
|
+
...
|
276
|
+
|
277
|
+
async def aio(self, /, coro, readable_method: str):
|
278
|
+
"""Runs coroutine wrapped in a task that's part of the client's task context
|
279
|
+
|
280
|
+
* Raises ClientClosed in case the client is closed while the coroutine is executed
|
281
|
+
* Logs warning if call is made outside of the event loop that the client is running in,
|
282
|
+
and execute without the cancellation context in that case
|
283
|
+
"""
|
284
|
+
...
|
145
285
|
|
146
286
|
_call_safely: ___call_safely_spec[typing_extensions.Self]
|
147
287
|
|
@@ -182,6 +322,26 @@ class Client:
|
|
182
322
|
) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
|
183
323
|
|
184
324
|
class UnaryUnaryWrapper(typing.Generic[RequestType, ResponseType]):
|
325
|
+
"""Abstract base class for generic types.
|
326
|
+
|
327
|
+
A generic type is typically declared by inheriting from
|
328
|
+
this class parameterized with one or more type variables.
|
329
|
+
For example, a generic mapping type might be defined as::
|
330
|
+
|
331
|
+
class Mapping(Generic[KT, VT]):
|
332
|
+
def __getitem__(self, key: KT) -> VT:
|
333
|
+
...
|
334
|
+
# Etc.
|
335
|
+
|
336
|
+
This class can then be used as follows::
|
337
|
+
|
338
|
+
def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
|
339
|
+
try:
|
340
|
+
return mapping[key]
|
341
|
+
except KeyError:
|
342
|
+
return default
|
343
|
+
"""
|
344
|
+
|
185
345
|
wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]
|
186
346
|
client: _Client
|
187
347
|
|
@@ -190,7 +350,10 @@ class UnaryUnaryWrapper(typing.Generic[RequestType, ResponseType]):
|
|
190
350
|
wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType],
|
191
351
|
client: _Client,
|
192
352
|
server_url: str,
|
193
|
-
):
|
353
|
+
):
|
354
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
355
|
+
...
|
356
|
+
|
194
357
|
@property
|
195
358
|
def name(self) -> str: ...
|
196
359
|
async def __call__(
|
@@ -203,9 +366,31 @@ class UnaryUnaryWrapper(typing.Generic[RequestType, ResponseType]):
|
|
203
366
|
collections.abc.Collection[tuple[str, typing.Union[str, bytes]]],
|
204
367
|
None,
|
205
368
|
] = None,
|
206
|
-
) -> ResponseType:
|
369
|
+
) -> ResponseType:
|
370
|
+
"""Call self as a function."""
|
371
|
+
...
|
207
372
|
|
208
373
|
class UnaryStreamWrapper(typing.Generic[RequestType, ResponseType]):
|
374
|
+
"""Abstract base class for generic types.
|
375
|
+
|
376
|
+
A generic type is typically declared by inheriting from
|
377
|
+
this class parameterized with one or more type variables.
|
378
|
+
For example, a generic mapping type might be defined as::
|
379
|
+
|
380
|
+
class Mapping(Generic[KT, VT]):
|
381
|
+
def __getitem__(self, key: KT) -> VT:
|
382
|
+
...
|
383
|
+
# Etc.
|
384
|
+
|
385
|
+
This class can then be used as follows::
|
386
|
+
|
387
|
+
def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
|
388
|
+
try:
|
389
|
+
return mapping[key]
|
390
|
+
except KeyError:
|
391
|
+
return default
|
392
|
+
"""
|
393
|
+
|
209
394
|
wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType]
|
210
395
|
|
211
396
|
def __init__(
|
@@ -213,7 +398,10 @@ class UnaryStreamWrapper(typing.Generic[RequestType, ResponseType]):
|
|
213
398
|
wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType],
|
214
399
|
client: _Client,
|
215
400
|
server_url: str,
|
216
|
-
):
|
401
|
+
):
|
402
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
403
|
+
...
|
404
|
+
|
217
405
|
@property
|
218
406
|
def name(self) -> str: ...
|
219
407
|
def unary_stream(self, request, metadata: typing.Optional[typing.Any] = None): ...
|
modal/cloud_bucket_mount.pyi
CHANGED
@@ -3,6 +3,95 @@ import modal_proto.api_pb2
|
|
3
3
|
import typing
|
4
4
|
|
5
5
|
class _CloudBucketMount:
|
6
|
+
"""Mounts a cloud bucket to your container. Currently supports AWS S3 buckets.
|
7
|
+
|
8
|
+
S3 buckets are mounted using [AWS S3 Mountpoint](https://github.com/awslabs/mountpoint-s3).
|
9
|
+
S3 mounts are optimized for reading large files sequentially. It does not support every file operation; consult
|
10
|
+
[the AWS S3 Mountpoint documentation](https://github.com/awslabs/mountpoint-s3/blob/main/doc/SEMANTICS.md)
|
11
|
+
for more information.
|
12
|
+
|
13
|
+
**AWS S3 Usage**
|
14
|
+
|
15
|
+
```python
|
16
|
+
import subprocess
|
17
|
+
|
18
|
+
app = modal.App()
|
19
|
+
secret = modal.Secret.from_name(
|
20
|
+
"aws-secret",
|
21
|
+
required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
22
|
+
# Note: providing AWS_REGION can help when automatic detection of the bucket region fails.
|
23
|
+
)
|
24
|
+
|
25
|
+
@app.function(
|
26
|
+
volumes={
|
27
|
+
"/my-mount": modal.CloudBucketMount(
|
28
|
+
bucket_name="s3-bucket-name",
|
29
|
+
secret=secret,
|
30
|
+
read_only=True
|
31
|
+
)
|
32
|
+
}
|
33
|
+
)
|
34
|
+
def f():
|
35
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
36
|
+
```
|
37
|
+
|
38
|
+
**Cloudflare R2 Usage**
|
39
|
+
|
40
|
+
Cloudflare R2 is [S3-compatible](https://developers.cloudflare.com/r2/api/s3/api/) so its setup looks
|
41
|
+
very similar to S3. But additionally the `bucket_endpoint_url` argument must be passed.
|
42
|
+
|
43
|
+
```python
|
44
|
+
import subprocess
|
45
|
+
|
46
|
+
app = modal.App()
|
47
|
+
secret = modal.Secret.from_name(
|
48
|
+
"r2-secret",
|
49
|
+
required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
50
|
+
)
|
51
|
+
|
52
|
+
@app.function(
|
53
|
+
volumes={
|
54
|
+
"/my-mount": modal.CloudBucketMount(
|
55
|
+
bucket_name="my-r2-bucket",
|
56
|
+
bucket_endpoint_url="https://<ACCOUNT ID>.r2.cloudflarestorage.com",
|
57
|
+
secret=secret,
|
58
|
+
read_only=True
|
59
|
+
)
|
60
|
+
}
|
61
|
+
)
|
62
|
+
def f():
|
63
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
64
|
+
```
|
65
|
+
|
66
|
+
**Google GCS Usage**
|
67
|
+
|
68
|
+
Google Cloud Storage (GCS) is [S3-compatible](https://cloud.google.com/storage/docs/interoperability).
|
69
|
+
GCS Buckets also require a secret with Google-specific key names (see below) populated with
|
70
|
+
a [HMAC key](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create).
|
71
|
+
|
72
|
+
```python
|
73
|
+
import subprocess
|
74
|
+
|
75
|
+
app = modal.App()
|
76
|
+
gcp_hmac_secret = modal.Secret.from_name(
|
77
|
+
"gcp-secret",
|
78
|
+
required_keys=["GOOGLE_ACCESS_KEY_ID", "GOOGLE_ACCESS_KEY_SECRET"]
|
79
|
+
)
|
80
|
+
|
81
|
+
@app.function(
|
82
|
+
volumes={
|
83
|
+
"/my-mount": modal.CloudBucketMount(
|
84
|
+
bucket_name="my-gcs-bucket",
|
85
|
+
bucket_endpoint_url="https://storage.googleapis.com",
|
86
|
+
secret=gcp_hmac_secret,
|
87
|
+
)
|
88
|
+
}
|
89
|
+
)
|
90
|
+
def f():
|
91
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
92
|
+
```
|
93
|
+
"""
|
94
|
+
|
6
95
|
bucket_name: str
|
7
96
|
bucket_endpoint_url: typing.Optional[str]
|
8
97
|
key_prefix: typing.Optional[str]
|
@@ -20,15 +109,114 @@ class _CloudBucketMount:
|
|
20
109
|
oidc_auth_role_arn: typing.Optional[str] = None,
|
21
110
|
read_only: bool = False,
|
22
111
|
requester_pays: bool = False,
|
23
|
-
) -> None:
|
24
|
-
|
25
|
-
|
112
|
+
) -> None:
|
113
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
114
|
+
...
|
115
|
+
|
116
|
+
def __repr__(self):
|
117
|
+
"""Return repr(self)."""
|
118
|
+
...
|
119
|
+
|
120
|
+
def __eq__(self, other):
|
121
|
+
"""Return self==value."""
|
122
|
+
...
|
26
123
|
|
27
124
|
def cloud_bucket_mounts_to_proto(
|
28
125
|
mounts: list[tuple[str, _CloudBucketMount]],
|
29
|
-
) -> list[modal_proto.api_pb2.CloudBucketMount]:
|
126
|
+
) -> list[modal_proto.api_pb2.CloudBucketMount]:
|
127
|
+
"""Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server."""
|
128
|
+
...
|
30
129
|
|
31
130
|
class CloudBucketMount:
|
131
|
+
"""Mounts a cloud bucket to your container. Currently supports AWS S3 buckets.
|
132
|
+
|
133
|
+
S3 buckets are mounted using [AWS S3 Mountpoint](https://github.com/awslabs/mountpoint-s3).
|
134
|
+
S3 mounts are optimized for reading large files sequentially. It does not support every file operation; consult
|
135
|
+
[the AWS S3 Mountpoint documentation](https://github.com/awslabs/mountpoint-s3/blob/main/doc/SEMANTICS.md)
|
136
|
+
for more information.
|
137
|
+
|
138
|
+
**AWS S3 Usage**
|
139
|
+
|
140
|
+
```python
|
141
|
+
import subprocess
|
142
|
+
|
143
|
+
app = modal.App()
|
144
|
+
secret = modal.Secret.from_name(
|
145
|
+
"aws-secret",
|
146
|
+
required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
147
|
+
# Note: providing AWS_REGION can help when automatic detection of the bucket region fails.
|
148
|
+
)
|
149
|
+
|
150
|
+
@app.function(
|
151
|
+
volumes={
|
152
|
+
"/my-mount": modal.CloudBucketMount(
|
153
|
+
bucket_name="s3-bucket-name",
|
154
|
+
secret=secret,
|
155
|
+
read_only=True
|
156
|
+
)
|
157
|
+
}
|
158
|
+
)
|
159
|
+
def f():
|
160
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
161
|
+
```
|
162
|
+
|
163
|
+
**Cloudflare R2 Usage**
|
164
|
+
|
165
|
+
Cloudflare R2 is [S3-compatible](https://developers.cloudflare.com/r2/api/s3/api/) so its setup looks
|
166
|
+
very similar to S3. But additionally the `bucket_endpoint_url` argument must be passed.
|
167
|
+
|
168
|
+
```python
|
169
|
+
import subprocess
|
170
|
+
|
171
|
+
app = modal.App()
|
172
|
+
secret = modal.Secret.from_name(
|
173
|
+
"r2-secret",
|
174
|
+
required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
175
|
+
)
|
176
|
+
|
177
|
+
@app.function(
|
178
|
+
volumes={
|
179
|
+
"/my-mount": modal.CloudBucketMount(
|
180
|
+
bucket_name="my-r2-bucket",
|
181
|
+
bucket_endpoint_url="https://<ACCOUNT ID>.r2.cloudflarestorage.com",
|
182
|
+
secret=secret,
|
183
|
+
read_only=True
|
184
|
+
)
|
185
|
+
}
|
186
|
+
)
|
187
|
+
def f():
|
188
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
189
|
+
```
|
190
|
+
|
191
|
+
**Google GCS Usage**
|
192
|
+
|
193
|
+
Google Cloud Storage (GCS) is [S3-compatible](https://cloud.google.com/storage/docs/interoperability).
|
194
|
+
GCS Buckets also require a secret with Google-specific key names (see below) populated with
|
195
|
+
a [HMAC key](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create).
|
196
|
+
|
197
|
+
```python
|
198
|
+
import subprocess
|
199
|
+
|
200
|
+
app = modal.App()
|
201
|
+
gcp_hmac_secret = modal.Secret.from_name(
|
202
|
+
"gcp-secret",
|
203
|
+
required_keys=["GOOGLE_ACCESS_KEY_ID", "GOOGLE_ACCESS_KEY_SECRET"]
|
204
|
+
)
|
205
|
+
|
206
|
+
@app.function(
|
207
|
+
volumes={
|
208
|
+
"/my-mount": modal.CloudBucketMount(
|
209
|
+
bucket_name="my-gcs-bucket",
|
210
|
+
bucket_endpoint_url="https://storage.googleapis.com",
|
211
|
+
secret=gcp_hmac_secret,
|
212
|
+
)
|
213
|
+
}
|
214
|
+
)
|
215
|
+
def f():
|
216
|
+
subprocess.run(["ls", "/my-mount"], check=True)
|
217
|
+
```
|
218
|
+
"""
|
219
|
+
|
32
220
|
bucket_name: str
|
33
221
|
bucket_endpoint_url: typing.Optional[str]
|
34
222
|
key_prefix: typing.Optional[str]
|