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.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {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
|
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.
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
29
|
-
|
30
|
-
|
31
|
-
async def
|
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:
|
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.
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
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
|
101
|
-
def __call__(self
|
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
|
-
|
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:
|
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.
|
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
|
modal/cloud_bucket_mount.py
CHANGED
@@ -1,86 +1,160 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from
|
4
|
-
from
|
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
|
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
|
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
|
-
|
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
|
-
@
|
61
|
+
@app.function(
|
38
62
|
volumes={
|
39
|
-
"/my-mount": modal.CloudBucketMount(
|
40
|
-
"
|
41
|
-
"
|
42
|
-
|
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.
|
53
|
-
#
|
54
|
-
#
|
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:
|
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:
|
121
|
+
cloud_bucket_mounts: list[api_pb2.CloudBucketMount] = []
|
67
122
|
|
68
123
|
for path, mount in mounts:
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
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
|
|
modal/cloud_bucket_mount.pyi
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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): ...
|