modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/file_io.pyi
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import _typeshed
|
|
2
2
|
import enum
|
|
3
3
|
import modal.client
|
|
4
|
-
import modal_proto.api_pb2
|
|
5
4
|
import typing
|
|
6
5
|
import typing_extensions
|
|
7
6
|
|
|
8
7
|
T = typing.TypeVar("T")
|
|
9
8
|
|
|
10
|
-
async def _delete_bytes(
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
async def _delete_bytes(file: _FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
|
|
10
|
+
"""Delete a range of bytes from the file.
|
|
11
|
+
|
|
12
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
13
|
+
If either is None, the start or end of the file is used, respectively.
|
|
14
|
+
"""
|
|
15
|
+
...
|
|
16
|
+
|
|
13
17
|
async def _replace_bytes(
|
|
14
18
|
file: _FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
|
|
15
|
-
) -> None:
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Replace a range of bytes in the file with new data. The length of the data does not
|
|
21
|
+
have to be the same as the length of the range being replaced.
|
|
22
|
+
|
|
23
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
24
|
+
If either is None, the start or end of the file is used, respectively.
|
|
25
|
+
"""
|
|
26
|
+
...
|
|
16
27
|
|
|
17
28
|
class FileWatchEventType(enum.Enum):
|
|
18
29
|
Unknown = "Unknown"
|
|
@@ -22,23 +33,58 @@ class FileWatchEventType(enum.Enum):
|
|
|
22
33
|
Remove = "Remove"
|
|
23
34
|
|
|
24
35
|
class FileWatchEvent:
|
|
36
|
+
"""FileWatchEvent(paths: list[str], type: modal.file_io.FileWatchEventType)"""
|
|
37
|
+
|
|
25
38
|
paths: list[str]
|
|
26
39
|
type: FileWatchEventType
|
|
27
40
|
|
|
28
|
-
def __init__(self, paths: list[str], type: FileWatchEventType) -> None:
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
def __init__(self, paths: list[str], type: FileWatchEventType) -> None:
|
|
42
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
"""Return repr(self)."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other):
|
|
50
|
+
"""Return self==value."""
|
|
51
|
+
...
|
|
31
52
|
|
|
32
53
|
class _FileIO(typing.Generic[T]):
|
|
54
|
+
"""[Alpha] FileIO handle, used in the Sandbox filesystem API.
|
|
55
|
+
|
|
56
|
+
The API is designed to mimic Python's io.FileIO.
|
|
57
|
+
|
|
58
|
+
Currently this API is in Alpha and is subject to change. File I/O operations
|
|
59
|
+
may be limited in size to 100 MiB, and the throughput of requests is
|
|
60
|
+
restricted in the current implementation. For our recommendations on large file transfers
|
|
61
|
+
see the Sandbox [filesystem access guide](https://modal.com/docs/guide/sandbox-files).
|
|
62
|
+
|
|
63
|
+
**Usage**
|
|
64
|
+
|
|
65
|
+
```python notest
|
|
66
|
+
import modal
|
|
67
|
+
|
|
68
|
+
app = modal.App.lookup("my-app", create_if_missing=True)
|
|
69
|
+
|
|
70
|
+
sb = modal.Sandbox.create(app=app)
|
|
71
|
+
f = sb.open("/tmp/foo.txt", "w")
|
|
72
|
+
f.write("hello")
|
|
73
|
+
f.close()
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
33
77
|
_task_id: str
|
|
34
78
|
_file_descriptor: str
|
|
35
79
|
_client: modal.client._Client
|
|
36
|
-
_watch_output_buffer: list[typing.
|
|
80
|
+
_watch_output_buffer: list[typing.Union[bytes, None, Exception]]
|
|
81
|
+
|
|
82
|
+
def __init__(self, client: modal.client._Client, task_id: str) -> None:
|
|
83
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
84
|
+
...
|
|
37
85
|
|
|
38
|
-
def __init__(self, client: modal.client._Client, task_id: str) -> None: ...
|
|
39
86
|
def _validate_mode(self, mode: str) -> None: ...
|
|
40
|
-
def
|
|
41
|
-
def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
|
|
87
|
+
def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Union[bytes, None, Exception]]: ...
|
|
42
88
|
async def _consume_watch_output(self, exec_id: str) -> None: ...
|
|
43
89
|
async def _parse_watch_output(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
|
|
44
90
|
async def _wait(self, exec_id: str) -> bytes: ...
|
|
@@ -51,21 +97,60 @@ class _FileIO(typing.Generic[T]):
|
|
|
51
97
|
mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
|
|
52
98
|
client: modal.client._Client,
|
|
53
99
|
task_id: str,
|
|
54
|
-
) -> _FileIO:
|
|
100
|
+
) -> _FileIO:
|
|
101
|
+
"""Create a new FileIO handle."""
|
|
102
|
+
...
|
|
103
|
+
|
|
55
104
|
async def _make_read_request(self, n: typing.Optional[int]) -> bytes: ...
|
|
56
|
-
async def read(self, n: typing.Optional[int] = None) -> T:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
async def
|
|
105
|
+
async def read(self, n: typing.Optional[int] = None) -> T:
|
|
106
|
+
"""Read n bytes from the current position, or the entire remaining file if n is None."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
async def readline(self) -> T:
|
|
110
|
+
"""Read a single line from the current position."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
async def readlines(self) -> typing.Sequence[T]:
|
|
114
|
+
"""Read all lines from the current position."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
async def write(self, data: typing.Union[bytes, str]) -> None:
|
|
118
|
+
"""Write data to the current position.
|
|
119
|
+
|
|
120
|
+
Writes may not appear until the entire buffer is flushed, which
|
|
121
|
+
can be done manually with `flush()` or automatically when the file is
|
|
122
|
+
closed.
|
|
123
|
+
"""
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
async def flush(self) -> None:
|
|
127
|
+
"""Flush the buffer to disk."""
|
|
128
|
+
...
|
|
129
|
+
|
|
61
130
|
def _get_whence(self, whence: int): ...
|
|
62
|
-
async def seek(self, offset: int, whence: int = 0) -> None:
|
|
131
|
+
async def seek(self, offset: int, whence: int = 0) -> None:
|
|
132
|
+
"""Move to a new position in the file.
|
|
133
|
+
|
|
134
|
+
`whence` defaults to 0 (absolute file positioning); other values are 1
|
|
135
|
+
(relative to the current position) and 2 (relative to the file's end).
|
|
136
|
+
"""
|
|
137
|
+
...
|
|
138
|
+
|
|
63
139
|
@classmethod
|
|
64
|
-
async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]:
|
|
140
|
+
async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]:
|
|
141
|
+
"""List the contents of the provided directory."""
|
|
142
|
+
...
|
|
143
|
+
|
|
65
144
|
@classmethod
|
|
66
|
-
async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None:
|
|
145
|
+
async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None:
|
|
146
|
+
"""Create a new directory."""
|
|
147
|
+
...
|
|
148
|
+
|
|
67
149
|
@classmethod
|
|
68
|
-
async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None:
|
|
150
|
+
async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None:
|
|
151
|
+
"""Remove a file or directory in the Sandbox."""
|
|
152
|
+
...
|
|
153
|
+
|
|
69
154
|
@classmethod
|
|
70
155
|
def watch(
|
|
71
156
|
cls,
|
|
@@ -77,7 +162,10 @@ class _FileIO(typing.Generic[T]):
|
|
|
77
162
|
timeout: typing.Optional[int] = None,
|
|
78
163
|
) -> typing.AsyncIterator[FileWatchEvent]: ...
|
|
79
164
|
async def _close(self) -> None: ...
|
|
80
|
-
async def close(self) -> None:
|
|
165
|
+
async def close(self) -> None:
|
|
166
|
+
"""Flush the buffer and close the file."""
|
|
167
|
+
...
|
|
168
|
+
|
|
81
169
|
def _check_writable(self) -> None: ...
|
|
82
170
|
def _check_readable(self) -> None: ...
|
|
83
171
|
def _check_closed(self) -> None: ...
|
|
@@ -85,22 +173,46 @@ class _FileIO(typing.Generic[T]):
|
|
|
85
173
|
async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
|
|
86
174
|
|
|
87
175
|
class __delete_bytes_spec(typing_extensions.Protocol):
|
|
88
|
-
def __call__(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
176
|
+
def __call__(self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
|
|
177
|
+
"""Delete a range of bytes from the file.
|
|
178
|
+
|
|
179
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
180
|
+
If either is None, the start or end of the file is used, respectively.
|
|
181
|
+
"""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
async def aio(self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
|
|
185
|
+
"""Delete a range of bytes from the file.
|
|
186
|
+
|
|
187
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
188
|
+
If either is None, the start or end of the file is used, respectively.
|
|
189
|
+
"""
|
|
190
|
+
...
|
|
94
191
|
|
|
95
192
|
delete_bytes: __delete_bytes_spec
|
|
96
193
|
|
|
97
194
|
class __replace_bytes_spec(typing_extensions.Protocol):
|
|
98
195
|
def __call__(
|
|
99
196
|
self, /, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
|
|
100
|
-
) -> None:
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Replace a range of bytes in the file with new data. The length of the data does not
|
|
199
|
+
have to be the same as the length of the range being replaced.
|
|
200
|
+
|
|
201
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
202
|
+
If either is None, the start or end of the file is used, respectively.
|
|
203
|
+
"""
|
|
204
|
+
...
|
|
205
|
+
|
|
101
206
|
async def aio(
|
|
102
207
|
self, /, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
|
|
103
|
-
) -> None:
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Replace a range of bytes in the file with new data. The length of the data does not
|
|
210
|
+
have to be the same as the length of the range being replaced.
|
|
211
|
+
|
|
212
|
+
`start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
|
|
213
|
+
If either is None, the start or end of the file is used, respectively.
|
|
214
|
+
"""
|
|
215
|
+
...
|
|
104
216
|
|
|
105
217
|
replace_bytes: __replace_bytes_spec
|
|
106
218
|
|
|
@@ -109,18 +221,40 @@ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
|
109
221
|
T_INNER = typing.TypeVar("T_INNER", covariant=True)
|
|
110
222
|
|
|
111
223
|
class FileIO(typing.Generic[T]):
|
|
224
|
+
"""[Alpha] FileIO handle, used in the Sandbox filesystem API.
|
|
225
|
+
|
|
226
|
+
The API is designed to mimic Python's io.FileIO.
|
|
227
|
+
|
|
228
|
+
Currently this API is in Alpha and is subject to change. File I/O operations
|
|
229
|
+
may be limited in size to 100 MiB, and the throughput of requests is
|
|
230
|
+
restricted in the current implementation. For our recommendations on large file transfers
|
|
231
|
+
see the Sandbox [filesystem access guide](https://modal.com/docs/guide/sandbox-files).
|
|
232
|
+
|
|
233
|
+
**Usage**
|
|
234
|
+
|
|
235
|
+
```python notest
|
|
236
|
+
import modal
|
|
237
|
+
|
|
238
|
+
app = modal.App.lookup("my-app", create_if_missing=True)
|
|
239
|
+
|
|
240
|
+
sb = modal.Sandbox.create(app=app)
|
|
241
|
+
f = sb.open("/tmp/foo.txt", "w")
|
|
242
|
+
f.write("hello")
|
|
243
|
+
f.close()
|
|
244
|
+
```
|
|
245
|
+
"""
|
|
246
|
+
|
|
112
247
|
_task_id: str
|
|
113
248
|
_file_descriptor: str
|
|
114
249
|
_client: modal.client.Client
|
|
115
|
-
_watch_output_buffer: list[typing.
|
|
250
|
+
_watch_output_buffer: list[typing.Union[bytes, None, Exception]]
|
|
116
251
|
|
|
117
252
|
def __init__(self, client: modal.client.Client, task_id: str) -> None: ...
|
|
118
253
|
def _validate_mode(self, mode: str) -> None: ...
|
|
119
|
-
def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
|
|
120
254
|
|
|
121
255
|
class ___consume_output_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
122
|
-
def __call__(self, /, exec_id: str) -> typing.Iterator[typing.
|
|
123
|
-
def aio(self, /, exec_id: str) -> typing.AsyncIterator[typing.
|
|
256
|
+
def __call__(self, /, exec_id: str) -> typing.Iterator[typing.Union[bytes, None, Exception]]: ...
|
|
257
|
+
def aio(self, /, exec_id: str) -> typing.AsyncIterator[typing.Union[bytes, None, Exception]]: ...
|
|
124
258
|
|
|
125
259
|
_consume_output: ___consume_output_spec[typing_extensions.Self]
|
|
126
260
|
|
|
@@ -157,7 +291,9 @@ class FileIO(typing.Generic[T]):
|
|
|
157
291
|
mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
|
|
158
292
|
client: modal.client.Client,
|
|
159
293
|
task_id: str,
|
|
160
|
-
) -> FileIO:
|
|
294
|
+
) -> FileIO:
|
|
295
|
+
"""Create a new FileIO handle."""
|
|
296
|
+
...
|
|
161
297
|
|
|
162
298
|
class ___make_read_request_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
163
299
|
def __call__(self, /, n: typing.Optional[int]) -> bytes: ...
|
|
@@ -166,49 +302,106 @@ class FileIO(typing.Generic[T]):
|
|
|
166
302
|
_make_read_request: ___make_read_request_spec[typing_extensions.Self]
|
|
167
303
|
|
|
168
304
|
class __read_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
|
|
169
|
-
def __call__(self, /, n: typing.Optional[int] = None) -> T_INNER:
|
|
170
|
-
|
|
305
|
+
def __call__(self, /, n: typing.Optional[int] = None) -> T_INNER:
|
|
306
|
+
"""Read n bytes from the current position, or the entire remaining file if n is None."""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
async def aio(self, /, n: typing.Optional[int] = None) -> T_INNER:
|
|
310
|
+
"""Read n bytes from the current position, or the entire remaining file if n is None."""
|
|
311
|
+
...
|
|
171
312
|
|
|
172
313
|
read: __read_spec[T, typing_extensions.Self]
|
|
173
314
|
|
|
174
315
|
class __readline_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
|
|
175
|
-
def __call__(self, /) -> T_INNER:
|
|
176
|
-
|
|
316
|
+
def __call__(self, /) -> T_INNER:
|
|
317
|
+
"""Read a single line from the current position."""
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
async def aio(self, /) -> T_INNER:
|
|
321
|
+
"""Read a single line from the current position."""
|
|
322
|
+
...
|
|
177
323
|
|
|
178
324
|
readline: __readline_spec[T, typing_extensions.Self]
|
|
179
325
|
|
|
180
326
|
class __readlines_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
|
|
181
|
-
def __call__(self, /) -> typing.Sequence[T_INNER]:
|
|
182
|
-
|
|
327
|
+
def __call__(self, /) -> typing.Sequence[T_INNER]:
|
|
328
|
+
"""Read all lines from the current position."""
|
|
329
|
+
...
|
|
330
|
+
|
|
331
|
+
async def aio(self, /) -> typing.Sequence[T_INNER]:
|
|
332
|
+
"""Read all lines from the current position."""
|
|
333
|
+
...
|
|
183
334
|
|
|
184
335
|
readlines: __readlines_spec[T, typing_extensions.Self]
|
|
185
336
|
|
|
186
337
|
class __write_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
187
|
-
def __call__(self, /, data: typing.Union[bytes, str]) -> None:
|
|
188
|
-
|
|
338
|
+
def __call__(self, /, data: typing.Union[bytes, str]) -> None:
|
|
339
|
+
"""Write data to the current position.
|
|
340
|
+
|
|
341
|
+
Writes may not appear until the entire buffer is flushed, which
|
|
342
|
+
can be done manually with `flush()` or automatically when the file is
|
|
343
|
+
closed.
|
|
344
|
+
"""
|
|
345
|
+
...
|
|
346
|
+
|
|
347
|
+
async def aio(self, /, data: typing.Union[bytes, str]) -> None:
|
|
348
|
+
"""Write data to the current position.
|
|
349
|
+
|
|
350
|
+
Writes may not appear until the entire buffer is flushed, which
|
|
351
|
+
can be done manually with `flush()` or automatically when the file is
|
|
352
|
+
closed.
|
|
353
|
+
"""
|
|
354
|
+
...
|
|
189
355
|
|
|
190
356
|
write: __write_spec[typing_extensions.Self]
|
|
191
357
|
|
|
192
358
|
class __flush_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
193
|
-
def __call__(self, /) -> None:
|
|
194
|
-
|
|
359
|
+
def __call__(self, /) -> None:
|
|
360
|
+
"""Flush the buffer to disk."""
|
|
361
|
+
...
|
|
362
|
+
|
|
363
|
+
async def aio(self, /) -> None:
|
|
364
|
+
"""Flush the buffer to disk."""
|
|
365
|
+
...
|
|
195
366
|
|
|
196
367
|
flush: __flush_spec[typing_extensions.Self]
|
|
197
368
|
|
|
198
369
|
def _get_whence(self, whence: int): ...
|
|
199
370
|
|
|
200
371
|
class __seek_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
201
|
-
def __call__(self, /, offset: int, whence: int = 0) -> None:
|
|
202
|
-
|
|
372
|
+
def __call__(self, /, offset: int, whence: int = 0) -> None:
|
|
373
|
+
"""Move to a new position in the file.
|
|
374
|
+
|
|
375
|
+
`whence` defaults to 0 (absolute file positioning); other values are 1
|
|
376
|
+
(relative to the current position) and 2 (relative to the file's end).
|
|
377
|
+
"""
|
|
378
|
+
...
|
|
379
|
+
|
|
380
|
+
async def aio(self, /, offset: int, whence: int = 0) -> None:
|
|
381
|
+
"""Move to a new position in the file.
|
|
382
|
+
|
|
383
|
+
`whence` defaults to 0 (absolute file positioning); other values are 1
|
|
384
|
+
(relative to the current position) and 2 (relative to the file's end).
|
|
385
|
+
"""
|
|
386
|
+
...
|
|
203
387
|
|
|
204
388
|
seek: __seek_spec[typing_extensions.Self]
|
|
205
389
|
|
|
206
390
|
@classmethod
|
|
207
|
-
def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]:
|
|
391
|
+
def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]:
|
|
392
|
+
"""List the contents of the provided directory."""
|
|
393
|
+
...
|
|
394
|
+
|
|
208
395
|
@classmethod
|
|
209
|
-
def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None:
|
|
396
|
+
def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None:
|
|
397
|
+
"""Create a new directory."""
|
|
398
|
+
...
|
|
399
|
+
|
|
210
400
|
@classmethod
|
|
211
|
-
def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None:
|
|
401
|
+
def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None:
|
|
402
|
+
"""Remove a file or directory in the Sandbox."""
|
|
403
|
+
...
|
|
404
|
+
|
|
212
405
|
@classmethod
|
|
213
406
|
def watch(
|
|
214
407
|
cls,
|
|
@@ -227,8 +420,13 @@ class FileIO(typing.Generic[T]):
|
|
|
227
420
|
_close: ___close_spec[typing_extensions.Self]
|
|
228
421
|
|
|
229
422
|
class __close_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
230
|
-
def __call__(self, /) -> None:
|
|
231
|
-
|
|
423
|
+
def __call__(self, /) -> None:
|
|
424
|
+
"""Flush the buffer and close the file."""
|
|
425
|
+
...
|
|
426
|
+
|
|
427
|
+
async def aio(self, /) -> None:
|
|
428
|
+
"""Flush the buffer and close the file."""
|
|
429
|
+
...
|
|
232
430
|
|
|
233
431
|
close: __close_spec[typing_extensions.Self]
|
|
234
432
|
|
modal/file_pattern_matcher.py
CHANGED
|
@@ -11,6 +11,7 @@ then asking it whether file paths match any of its patterns.
|
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
from abc import abstractmethod
|
|
14
|
+
from functools import cached_property
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Callable, Optional, Sequence, Union
|
|
16
17
|
|
|
@@ -46,6 +47,18 @@ class _AbstractPatternMatcher:
|
|
|
46
47
|
|
|
47
48
|
return super().__repr__()
|
|
48
49
|
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def can_prune_directories(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Returns True if this pattern matcher allows safe early directory pruning.
|
|
54
|
+
|
|
55
|
+
Directory pruning is safe when matching directories can be skipped entirely
|
|
56
|
+
without missing any files that should be included.
|
|
57
|
+
|
|
58
|
+
An example where pruning is not safe is for inverted patterns, like "!**/*.py".
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
49
62
|
@abstractmethod
|
|
50
63
|
def __call__(self, path: Path) -> bool: ...
|
|
51
64
|
|
|
@@ -54,6 +67,15 @@ class _CustomPatternMatcher(_AbstractPatternMatcher):
|
|
|
54
67
|
def __init__(self, predicate: Callable[[Path], bool]):
|
|
55
68
|
self._predicate = predicate
|
|
56
69
|
|
|
70
|
+
def can_prune_directories(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Custom pattern matchers (like negated matchers) cannot safely prune directories.
|
|
73
|
+
|
|
74
|
+
Since these are arbitrary predicates, we cannot determine if a directory
|
|
75
|
+
can be safely skipped without evaluating all files within it.
|
|
76
|
+
"""
|
|
77
|
+
return False
|
|
78
|
+
|
|
57
79
|
def __call__(self, path: Path) -> bool:
|
|
58
80
|
return self._predicate(path)
|
|
59
81
|
|
|
@@ -78,11 +100,11 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
78
100
|
```
|
|
79
101
|
"""
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
_file_path: Optional[Union[str, Path]]
|
|
104
|
+
_pattern_strings: Optional[Sequence[str]]
|
|
83
105
|
|
|
84
|
-
def
|
|
85
|
-
|
|
106
|
+
def _parse_patterns(self, patterns: Sequence[str]) -> list[Pattern]:
|
|
107
|
+
parsed_patterns = []
|
|
86
108
|
for pattern in list(patterns):
|
|
87
109
|
pattern = pattern.strip().strip(os.path.sep)
|
|
88
110
|
if not pattern:
|
|
@@ -97,7 +119,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
97
119
|
# In Python, we can proceed without explicit syntax checking
|
|
98
120
|
new_pattern.cleaned_pattern = pattern
|
|
99
121
|
new_pattern.dirs = pattern.split(os.path.sep)
|
|
100
|
-
|
|
122
|
+
parsed_patterns.append(new_pattern)
|
|
123
|
+
return parsed_patterns
|
|
101
124
|
|
|
102
125
|
def __init__(self, *pattern: str) -> None:
|
|
103
126
|
"""Initialize a new FilePatternMatcher instance.
|
|
@@ -108,7 +131,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
108
131
|
Raises:
|
|
109
132
|
ValueError: If an illegal exclusion pattern is provided.
|
|
110
133
|
"""
|
|
111
|
-
self.
|
|
134
|
+
self._pattern_strings = pattern
|
|
135
|
+
self._file_path = None
|
|
112
136
|
|
|
113
137
|
@classmethod
|
|
114
138
|
def from_file(cls, file_path: Union[str, Path]) -> "FilePatternMatcher":
|
|
@@ -127,14 +151,10 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
127
151
|
```
|
|
128
152
|
|
|
129
153
|
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
uninitialized._delayed_init = None
|
|
135
|
-
|
|
136
|
-
uninitialized._delayed_init = _delayed_init
|
|
137
|
-
return uninitialized
|
|
154
|
+
instance = cls.__new__(cls)
|
|
155
|
+
instance._file_path = file_path
|
|
156
|
+
instance._pattern_strings = None
|
|
157
|
+
return instance
|
|
138
158
|
|
|
139
159
|
def _matches(self, file_path: str) -> bool:
|
|
140
160
|
"""Check if the file path or any of its parent directories match the patterns.
|
|
@@ -173,9 +193,29 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
|
173
193
|
|
|
174
194
|
return matched
|
|
175
195
|
|
|
196
|
+
@cached_property
|
|
197
|
+
def patterns(self) -> list[Pattern]:
|
|
198
|
+
"""Get the patterns, loading from file if necessary."""
|
|
199
|
+
if self._file_path is not None:
|
|
200
|
+
# Lazy load from file
|
|
201
|
+
pattern_strings = Path(self._file_path).read_text("utf8").splitlines()
|
|
202
|
+
else:
|
|
203
|
+
# Use patterns provided in __init__
|
|
204
|
+
pattern_strings = list(self._pattern_strings)
|
|
205
|
+
|
|
206
|
+
return self._parse_patterns(pattern_strings)
|
|
207
|
+
|
|
208
|
+
def can_prune_directories(self) -> bool:
|
|
209
|
+
"""
|
|
210
|
+
Returns True if this pattern matcher allows safe early directory pruning.
|
|
211
|
+
|
|
212
|
+
Directory pruning is safe when matching directories can be skipped entirely
|
|
213
|
+
without missing any files that should be included. This is for example not
|
|
214
|
+
safe when we have inverted/negated ignore patterns (e.g. "!**/*.py").
|
|
215
|
+
"""
|
|
216
|
+
return not any(pattern.exclusion for pattern in self.patterns)
|
|
217
|
+
|
|
176
218
|
def __call__(self, file_path: Path) -> bool:
|
|
177
|
-
if self._delayed_init:
|
|
178
|
-
self._delayed_init()
|
|
179
219
|
return self._matches(str(file_path))
|
|
180
220
|
|
|
181
221
|
|