modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- 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 +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- 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 +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.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 +1 -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 +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- 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_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- 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 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- 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 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- 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 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- 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 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
"""Pattern matching library ported from https://github.com/moby/patternmatcher.
|
3
|
+
|
4
|
+
This is the same pattern-matching logic used by Docker, except it is written in
|
5
|
+
Python rather than Go. Also, the original Go library has a couple deprecated
|
6
|
+
functions that we don't implement in this port.
|
7
|
+
|
8
|
+
The main way to use this library is by constructing a `FilePatternMatcher` object,
|
9
|
+
then asking it whether file paths match any of its patterns.
|
10
|
+
"""
|
11
|
+
|
12
|
+
import enum
|
13
|
+
import os
|
14
|
+
import re
|
15
|
+
from typing import Optional, TextIO
|
16
|
+
|
17
|
+
escape_chars = frozenset(".+()|{}$")
|
18
|
+
|
19
|
+
|
20
|
+
class MatchType(enum.IntEnum):
|
21
|
+
UNKNOWN = 0
|
22
|
+
EXACT = 1
|
23
|
+
PREFIX = 2
|
24
|
+
SUFFIX = 3
|
25
|
+
REGEXP = 4
|
26
|
+
|
27
|
+
|
28
|
+
class Pattern:
|
29
|
+
"""Defines a single regex pattern used to filter file paths."""
|
30
|
+
|
31
|
+
def __init__(self) -> None:
|
32
|
+
"""Initialize a new Pattern instance."""
|
33
|
+
self.match_type = MatchType.UNKNOWN
|
34
|
+
self.cleaned_pattern = ""
|
35
|
+
self.dirs: list[str] = []
|
36
|
+
self.regexp: Optional[re.Pattern] = None
|
37
|
+
self.exclusion = False
|
38
|
+
|
39
|
+
def __str__(self) -> str:
|
40
|
+
"""Return the cleaned pattern as the string representation."""
|
41
|
+
return self.cleaned_pattern
|
42
|
+
|
43
|
+
def compile(self, separator: str) -> None:
|
44
|
+
"""Compile the pattern into a regular expression.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
separator (str): The path separator (e.g., '/' or '\\').
|
48
|
+
|
49
|
+
Raises:
|
50
|
+
ValueError: If the pattern is invalid.
|
51
|
+
"""
|
52
|
+
reg_str = "^"
|
53
|
+
pattern = self.cleaned_pattern
|
54
|
+
|
55
|
+
esc_separator = separator
|
56
|
+
if separator == "\\":
|
57
|
+
esc_separator = "\\\\"
|
58
|
+
|
59
|
+
self.match_type = MatchType.EXACT
|
60
|
+
i = 0
|
61
|
+
pattern_length = len(pattern)
|
62
|
+
while i < pattern_length:
|
63
|
+
ch = pattern[i]
|
64
|
+
if ch == "*":
|
65
|
+
if (i + 1) < pattern_length and pattern[i + 1] == "*":
|
66
|
+
# Handle '**'
|
67
|
+
i += 1 # Skip the second '*'
|
68
|
+
# Treat '**/' as '**' so eat the '/'
|
69
|
+
if (i + 1) < pattern_length and pattern[i + 1] == separator:
|
70
|
+
i += 1 # Skip the '/'
|
71
|
+
if i + 1 == pattern_length:
|
72
|
+
# Pattern ends with '**'
|
73
|
+
if self.match_type == MatchType.EXACT:
|
74
|
+
self.match_type = MatchType.PREFIX
|
75
|
+
else:
|
76
|
+
reg_str += ".*"
|
77
|
+
self.match_type = MatchType.REGEXP
|
78
|
+
else:
|
79
|
+
# '**' in the middle
|
80
|
+
reg_str += f"(.*{esc_separator})?"
|
81
|
+
self.match_type = MatchType.REGEXP
|
82
|
+
|
83
|
+
if i == 1:
|
84
|
+
self.match_type = MatchType.SUFFIX
|
85
|
+
else:
|
86
|
+
# Single '*'
|
87
|
+
reg_str += f"[^{esc_separator}]*"
|
88
|
+
self.match_type = MatchType.REGEXP
|
89
|
+
elif ch == "?":
|
90
|
+
# Single '?'
|
91
|
+
reg_str += f"[^{esc_separator}]"
|
92
|
+
self.match_type = MatchType.REGEXP
|
93
|
+
elif ch in escape_chars:
|
94
|
+
reg_str += "\\" + ch
|
95
|
+
elif ch == "\\":
|
96
|
+
# Escape next character
|
97
|
+
if separator == "\\":
|
98
|
+
reg_str += esc_separator
|
99
|
+
i += 1
|
100
|
+
continue
|
101
|
+
if (i + 1) < pattern_length:
|
102
|
+
reg_str += "\\" + pattern[i + 1]
|
103
|
+
i += 1 # Skip the escaped character
|
104
|
+
self.match_type = MatchType.REGEXP
|
105
|
+
else:
|
106
|
+
reg_str += "\\"
|
107
|
+
elif ch == "[" or ch == "]":
|
108
|
+
reg_str += ch
|
109
|
+
self.match_type = MatchType.REGEXP
|
110
|
+
else:
|
111
|
+
reg_str += ch
|
112
|
+
i += 1
|
113
|
+
|
114
|
+
if self.match_type != MatchType.REGEXP:
|
115
|
+
return
|
116
|
+
|
117
|
+
reg_str += "$"
|
118
|
+
|
119
|
+
try:
|
120
|
+
self.regexp = re.compile(reg_str)
|
121
|
+
self.match_type = MatchType.REGEXP
|
122
|
+
except re.error as e:
|
123
|
+
raise ValueError(f"Bad pattern: {pattern}") from e
|
124
|
+
|
125
|
+
def match(self, path: str) -> bool:
|
126
|
+
"""Check if the path matches the pattern."""
|
127
|
+
if self.match_type == MatchType.UNKNOWN:
|
128
|
+
self.compile(os.path.sep)
|
129
|
+
|
130
|
+
if self.match_type == MatchType.EXACT:
|
131
|
+
return path == self.cleaned_pattern
|
132
|
+
elif self.match_type == MatchType.PREFIX:
|
133
|
+
# Strip trailing '**'
|
134
|
+
return path.startswith(self.cleaned_pattern[:-2])
|
135
|
+
elif self.match_type == MatchType.SUFFIX:
|
136
|
+
# Strip leading '**'
|
137
|
+
suffix = self.cleaned_pattern[2:]
|
138
|
+
if path.endswith(suffix):
|
139
|
+
return True
|
140
|
+
# '**/foo' matches 'foo'
|
141
|
+
if suffix[0] == os.path.sep and path == suffix[1:]:
|
142
|
+
return True
|
143
|
+
else:
|
144
|
+
return False
|
145
|
+
elif self.match_type == MatchType.REGEXP:
|
146
|
+
return self.regexp.match(path) is not None
|
147
|
+
else:
|
148
|
+
return False
|
149
|
+
|
150
|
+
|
151
|
+
def read_ignorefile(reader: TextIO) -> list[str]:
|
152
|
+
"""Read an ignore file from a reader and return the list of file patterns to
|
153
|
+
ignore, applying the following rules:
|
154
|
+
|
155
|
+
- An UTF8 BOM header (if present) is stripped. (Python does this already)
|
156
|
+
- Lines starting with "#" are considered comments and are skipped.
|
157
|
+
|
158
|
+
For remaining lines:
|
159
|
+
|
160
|
+
- Leading and trailing whitespace is removed from each ignore pattern.
|
161
|
+
- It uses `os.path.normpath` to get the shortest/cleanest path for ignore
|
162
|
+
patterns.
|
163
|
+
- Leading forward-slashes ("/") are removed from ignore patterns, so
|
164
|
+
"/some/path" and "some/path" are considered equivalent.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
reader (file-like object): The input stream to read from.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
list: A list of patterns to ignore.
|
171
|
+
"""
|
172
|
+
if reader is None:
|
173
|
+
return []
|
174
|
+
|
175
|
+
excludes: list[str] = []
|
176
|
+
|
177
|
+
for line in reader:
|
178
|
+
pattern = line.rstrip("\n\r")
|
179
|
+
|
180
|
+
# Lines starting with "#" are ignored
|
181
|
+
if pattern.startswith("#"):
|
182
|
+
continue
|
183
|
+
|
184
|
+
pattern = pattern.strip()
|
185
|
+
if pattern == "":
|
186
|
+
continue
|
187
|
+
|
188
|
+
# Normalize absolute paths to paths relative to the context
|
189
|
+
# (taking care of '!' prefix)
|
190
|
+
invert = pattern[0] == "!"
|
191
|
+
if invert:
|
192
|
+
pattern = pattern[1:].strip()
|
193
|
+
|
194
|
+
if len(pattern) > 0:
|
195
|
+
pattern = os.path.normpath(pattern)
|
196
|
+
pattern = pattern.replace(os.sep, "/")
|
197
|
+
if len(pattern) > 1 and pattern[0] == "/":
|
198
|
+
pattern = pattern[1:]
|
199
|
+
|
200
|
+
if invert:
|
201
|
+
pattern = "!" + pattern
|
202
|
+
|
203
|
+
excludes.append(pattern)
|
204
|
+
|
205
|
+
return excludes
|
modal/_utils/rand_pb_testing.py
CHANGED
@@ -7,13 +7,13 @@ Modal, with random seeds, and it supports oneofs, and Protobuf v4.
|
|
7
7
|
|
8
8
|
import string
|
9
9
|
from random import Random
|
10
|
-
from typing import Any, Callable,
|
10
|
+
from typing import Any, Callable, Optional, TypeVar
|
11
11
|
|
12
12
|
from google.protobuf.descriptor import Descriptor, FieldDescriptor
|
13
13
|
|
14
14
|
T = TypeVar("T")
|
15
15
|
|
16
|
-
_FIELD_RANDOM_GENERATOR:
|
16
|
+
_FIELD_RANDOM_GENERATOR: dict[int, Callable[[Random], Any]] = {
|
17
17
|
FieldDescriptor.TYPE_DOUBLE: lambda rand: rand.normalvariate(0, 1),
|
18
18
|
FieldDescriptor.TYPE_FLOAT: lambda rand: rand.normalvariate(0, 1),
|
19
19
|
FieldDescriptor.TYPE_INT32: lambda rand: int.from_bytes(rand.randbytes(4), "little", signed=True),
|
@@ -71,7 +71,7 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
71
71
|
setattr(msg, field.name, generator(rand))
|
72
72
|
|
73
73
|
|
74
|
-
def rand_pb(proto:
|
74
|
+
def rand_pb(proto: type[T], rand: Optional[Random] = None) -> T:
|
75
75
|
"""Generate a pseudorandom protobuf message.
|
76
76
|
|
77
77
|
```python notest
|
modal/_utils/shell_utils.py
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
|
+
|
2
3
|
import asyncio
|
3
4
|
import contextlib
|
4
5
|
import errno
|
5
6
|
import os
|
6
7
|
import select
|
7
8
|
import sys
|
8
|
-
from
|
9
|
-
|
10
|
-
import rich.status
|
9
|
+
from collections.abc import Coroutine
|
10
|
+
from typing import Callable, Optional
|
11
11
|
|
12
12
|
from modal._pty import raw_terminal, set_nonblocking
|
13
|
-
from modal.exception import ExecutionError, InteractiveTimeoutError
|
14
13
|
|
15
|
-
from .async_utils import
|
14
|
+
from .async_utils import asyncify
|
16
15
|
|
17
16
|
|
18
17
|
def write_to_fd(fd: int, data: bytes):
|
@@ -20,14 +19,20 @@ def write_to_fd(fd: int, data: bytes):
|
|
20
19
|
future = loop.create_future()
|
21
20
|
|
22
21
|
def try_write():
|
22
|
+
nonlocal data
|
23
23
|
try:
|
24
24
|
nbytes = os.write(fd, data)
|
25
|
-
|
26
|
-
|
25
|
+
data = data[nbytes:]
|
26
|
+
if not data:
|
27
|
+
loop.remove_writer(fd)
|
28
|
+
future.set_result(None)
|
27
29
|
except OSError as e:
|
28
|
-
if e.errno
|
29
|
-
|
30
|
-
|
30
|
+
if e.errno == errno.EAGAIN:
|
31
|
+
# Wait for the next write notification
|
32
|
+
return
|
33
|
+
# Fail if it's not EAGAIN
|
34
|
+
loop.remove_writer(fd)
|
35
|
+
future.set_exception(e)
|
31
36
|
|
32
37
|
loop.add_writer(fd, try_write)
|
33
38
|
return future
|
@@ -72,42 +77,3 @@ async def stream_from_stdin(handle_input: Callable[[bytes, int], Coroutine], use
|
|
72
77
|
yield
|
73
78
|
os.write(quit_pipe_write, b"\n")
|
74
79
|
write_task.cancel()
|
75
|
-
|
76
|
-
|
77
|
-
async def connect_to_terminal(
|
78
|
-
# Handles data read from stdin. Inputs are the stdin data and message index.
|
79
|
-
handle_stdin: Callable[[bytes, int], Coroutine],
|
80
|
-
# Creates a coroutine that streams data to stdout/stderr. Returns the exit status.
|
81
|
-
stream_to_stdio: Callable[[asyncio.Event], Coroutine[None, None, int]],
|
82
|
-
pty: bool = False,
|
83
|
-
connecting_status: Optional[rich.status.Status] = None,
|
84
|
-
) -> None:
|
85
|
-
"""
|
86
|
-
Connect to the current terminal by streaming data from terminal's stdin to the running process
|
87
|
-
and streaming output from running process into terminal's stdout.
|
88
|
-
|
89
|
-
If connecting_status is given, this function will stop the status spinner upon connection or error.
|
90
|
-
"""
|
91
|
-
|
92
|
-
def stop_connecting_status():
|
93
|
-
if connecting_status:
|
94
|
-
connecting_status.stop()
|
95
|
-
|
96
|
-
on_connect = asyncio.Event()
|
97
|
-
async with TaskContext() as tc:
|
98
|
-
exec_output_task = tc.create_task(stream_to_stdio(on_connect))
|
99
|
-
try:
|
100
|
-
# time out if we can't connect to the server fast enough
|
101
|
-
await asyncio.wait_for(on_connect.wait(), timeout=15)
|
102
|
-
stop_connecting_status()
|
103
|
-
|
104
|
-
async with stream_from_stdin(handle_stdin, use_raw_terminal=pty):
|
105
|
-
exit_status = await exec_output_task
|
106
|
-
|
107
|
-
if exit_status != 0:
|
108
|
-
raise ExecutionError(f"Process exited with status code {exit_status}")
|
109
|
-
|
110
|
-
except (asyncio.TimeoutError, TimeoutError):
|
111
|
-
stop_connecting_status()
|
112
|
-
exec_output_task.cancel()
|
113
|
-
raise InteractiveTimeoutError("Failed to establish connection to container.")
|
modal/_vendor/a2wsgi_wsgi.py
CHANGED
@@ -35,10 +35,8 @@ from concurrent.futures import ThreadPoolExecutor
|
|
35
35
|
from types import TracebackType
|
36
36
|
from typing import (
|
37
37
|
Any,
|
38
|
-
Awaitable,
|
39
38
|
Callable,
|
40
39
|
Dict,
|
41
|
-
Iterable,
|
42
40
|
List,
|
43
41
|
Literal,
|
44
42
|
Optional,
|
@@ -48,6 +46,7 @@ from typing import (
|
|
48
46
|
TypedDict,
|
49
47
|
Union,
|
50
48
|
)
|
49
|
+
from collections.abc import Awaitable, Iterable
|
51
50
|
|
52
51
|
|
53
52
|
## BEGIN a2wsgi/asgi_typing.py
|
@@ -73,11 +72,11 @@ class HTTPScope(TypedDict):
|
|
73
72
|
raw_path: NotRequired[bytes]
|
74
73
|
query_string: bytes
|
75
74
|
root_path: str
|
76
|
-
headers: Iterable[
|
77
|
-
client: NotRequired[
|
78
|
-
server: NotRequired[
|
79
|
-
state: NotRequired[
|
80
|
-
extensions: NotRequired[
|
75
|
+
headers: Iterable[tuple[bytes, bytes]]
|
76
|
+
client: NotRequired[tuple[str, int]]
|
77
|
+
server: NotRequired[tuple[str, Optional[int]]]
|
78
|
+
state: NotRequired[dict[str, Any]]
|
79
|
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
81
80
|
|
82
81
|
|
83
82
|
class WebSocketScope(TypedDict):
|
@@ -89,18 +88,18 @@ class WebSocketScope(TypedDict):
|
|
89
88
|
raw_path: bytes
|
90
89
|
query_string: bytes
|
91
90
|
root_path: str
|
92
|
-
headers: Iterable[
|
93
|
-
client: NotRequired[
|
94
|
-
server: NotRequired[
|
91
|
+
headers: Iterable[tuple[bytes, bytes]]
|
92
|
+
client: NotRequired[tuple[str, int]]
|
93
|
+
server: NotRequired[tuple[str, Optional[int]]]
|
95
94
|
subprotocols: Iterable[str]
|
96
|
-
state: NotRequired[
|
97
|
-
extensions: NotRequired[
|
95
|
+
state: NotRequired[dict[str, Any]]
|
96
|
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
98
97
|
|
99
98
|
|
100
99
|
class LifespanScope(TypedDict):
|
101
100
|
type: Literal["lifespan"]
|
102
101
|
asgi: ASGIVersions
|
103
|
-
state: NotRequired[
|
102
|
+
state: NotRequired[dict[str, Any]]
|
104
103
|
|
105
104
|
|
106
105
|
WWWScope = Union[HTTPScope, WebSocketScope]
|
@@ -116,7 +115,7 @@ class HTTPRequestEvent(TypedDict):
|
|
116
115
|
class HTTPResponseStartEvent(TypedDict):
|
117
116
|
type: Literal["http.response.start"]
|
118
117
|
status: int
|
119
|
-
headers: NotRequired[Iterable[
|
118
|
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
120
119
|
trailers: NotRequired[bool]
|
121
120
|
|
122
121
|
|
@@ -137,7 +136,7 @@ class WebSocketConnectEvent(TypedDict):
|
|
137
136
|
class WebSocketAcceptEvent(TypedDict):
|
138
137
|
type: Literal["websocket.accept"]
|
139
138
|
subprotocol: NotRequired[str]
|
140
|
-
headers: NotRequired[Iterable[
|
139
|
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
141
140
|
|
142
141
|
|
143
142
|
class WebSocketReceiveEvent(TypedDict):
|
@@ -223,56 +222,47 @@ ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]
|
|
223
222
|
|
224
223
|
## BEGIN a2wsgi/wsgi_typing.py
|
225
224
|
|
226
|
-
CGIRequiredDefined
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
# The contents of any Content-Type fields in the HTTP request. May be empty
|
268
|
-
# or absent.
|
269
|
-
"CONTENT_TYPE": str,
|
270
|
-
# The contents of any Content-Length fields in the HTTP request. May be empty
|
271
|
-
# or absent.
|
272
|
-
"CONTENT_LENGTH": str,
|
273
|
-
},
|
274
|
-
total=False,
|
275
|
-
)
|
225
|
+
class CGIRequiredDefined(TypedDict):
|
226
|
+
# The HTTP request method, such as GET or POST. This cannot ever be an
|
227
|
+
# empty string, and so is always required.
|
228
|
+
REQUEST_METHOD: str
|
229
|
+
# When HTTP_HOST is not set, these variables can be combined to determine
|
230
|
+
# a default.
|
231
|
+
# SERVER_NAME and SERVER_PORT are required strings and must never be empty.
|
232
|
+
SERVER_NAME: str
|
233
|
+
SERVER_PORT: str
|
234
|
+
# The version of the protocol the client used to send the request.
|
235
|
+
# Typically this will be something like "HTTP/1.0" or "HTTP/1.1" and
|
236
|
+
# may be used by the application to determine how to treat any HTTP
|
237
|
+
# request headers. (This variable should probably be called REQUEST_PROTOCOL,
|
238
|
+
# since it denotes the protocol used in the request, and is not necessarily
|
239
|
+
# the protocol that will be used in the server's response. However, for
|
240
|
+
# compatibility with CGI we have to keep the existing name.)
|
241
|
+
SERVER_PROTOCOL: str
|
242
|
+
|
243
|
+
class CGIOptionalDefined(TypedDict, total=False):
|
244
|
+
REQUEST_URI: str
|
245
|
+
REMOTE_ADDR: str
|
246
|
+
REMOTE_PORT: str
|
247
|
+
# The initial portion of the request URL’s “path” that corresponds to the
|
248
|
+
# application object, so that the application knows its virtual “location”.
|
249
|
+
# This may be an empty string, if the application corresponds to the “root”
|
250
|
+
# of the server.
|
251
|
+
SCRIPT_NAME: str
|
252
|
+
# The remainder of the request URL’s “path”, designating the virtual
|
253
|
+
# “location” of the request’s target within the application. This may be an
|
254
|
+
# empty string, if the request URL targets the application root and does
|
255
|
+
# not have a trailing slash.
|
256
|
+
PATH_INFO: str
|
257
|
+
# The portion of the request URL that follows the “?”, if any. May be empty
|
258
|
+
# or absent.
|
259
|
+
QUERY_STRING: str
|
260
|
+
# The contents of any Content-Type fields in the HTTP request. May be empty
|
261
|
+
# or absent.
|
262
|
+
CONTENT_TYPE: str
|
263
|
+
# The contents of any Content-Length fields in the HTTP request. May be empty
|
264
|
+
# or absent.
|
265
|
+
CONTENT_LENGTH: str
|
276
266
|
|
277
267
|
|
278
268
|
class InputStream(Protocol):
|
@@ -308,7 +298,7 @@ class InputStream(Protocol):
|
|
308
298
|
"""
|
309
299
|
raise NotImplementedError
|
310
300
|
|
311
|
-
def readlines(self, hint: int = -1, /) ->
|
301
|
+
def readlines(self, hint: int = -1, /) -> list[bytes]:
|
312
302
|
"""
|
313
303
|
Note that the hint argument to readlines() is optional for both caller and
|
314
304
|
implementer. The application is free not to supply it, and the server or gateway
|
@@ -349,14 +339,14 @@ class ErrorStream(Protocol):
|
|
349
339
|
def write(self, s: str, /) -> Any:
|
350
340
|
raise NotImplementedError
|
351
341
|
|
352
|
-
def writelines(self, seq:
|
342
|
+
def writelines(self, seq: list[str], /) -> Any:
|
353
343
|
raise NotImplementedError
|
354
344
|
|
355
345
|
|
356
346
|
WSGIDefined = TypedDict(
|
357
347
|
"WSGIDefined",
|
358
348
|
{
|
359
|
-
"wsgi.version":
|
349
|
+
"wsgi.version": tuple[int, int], # e.g. (1, 0)
|
360
350
|
"wsgi.url_scheme": str, # e.g. "http" or "https"
|
361
351
|
"wsgi.input": InputStream,
|
362
352
|
"wsgi.errors": ErrorStream,
|
@@ -381,7 +371,7 @@ class Environ(CGIRequiredDefined, CGIOptionalDefined, WSGIDefined):
|
|
381
371
|
"""
|
382
372
|
|
383
373
|
|
384
|
-
ExceptionInfo =
|
374
|
+
ExceptionInfo = tuple[type[BaseException], BaseException, Optional[TracebackType]]
|
385
375
|
|
386
376
|
# https://peps.python.org/pep-3333/#the-write-callable
|
387
377
|
WriteCallable = Callable[[bytes], None]
|
@@ -391,7 +381,7 @@ class StartResponse(Protocol):
|
|
391
381
|
def __call__(
|
392
382
|
self,
|
393
383
|
status: str,
|
394
|
-
response_headers:
|
384
|
+
response_headers: list[tuple[str, str]],
|
395
385
|
exc_info: Optional[ExceptionInfo] = None,
|
396
386
|
/,
|
397
387
|
) -> WriteCallable:
|
@@ -460,7 +450,7 @@ class Body:
|
|
460
450
|
self.buffer.clear()
|
461
451
|
return result
|
462
452
|
|
463
|
-
def readlines(self, hint: int = -1) ->
|
453
|
+
def readlines(self, hint: int = -1) -> list[bytes]:
|
464
454
|
if not self.has_more:
|
465
455
|
return []
|
466
456
|
if hint == -1:
|
@@ -626,7 +616,7 @@ class WSGIResponder:
|
|
626
616
|
def start_response(
|
627
617
|
self,
|
628
618
|
status: str,
|
629
|
-
response_headers:
|
619
|
+
response_headers: list[tuple[str, str]],
|
630
620
|
exc_info: typing.Optional[ExceptionInfo] = None,
|
631
621
|
) -> WriteCallable:
|
632
622
|
self.exc_info = exc_info
|
modal/_vendor/cloudpickle.py
CHANGED
@@ -256,7 +256,7 @@ def _should_pickle_by_reference(obj, name=None):
|
|
256
256
|
return False
|
257
257
|
return obj.__name__ in sys.modules
|
258
258
|
else:
|
259
|
-
raise TypeError("cannot check importability of {
|
259
|
+
raise TypeError(f"cannot check importability of {type(obj).__name__} instances")
|
260
260
|
|
261
261
|
|
262
262
|
def _lookup_module_and_qualname(obj, name=None):
|
modal/_watcher.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
from collections import defaultdict
|
3
|
+
from collections.abc import AsyncGenerator
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import
|
5
|
+
from typing import Optional
|
5
6
|
|
6
7
|
from rich.tree import Tree
|
7
8
|
from watchfiles import Change, DefaultFilter, awatch
|
8
9
|
|
9
10
|
from modal.mount import _Mount
|
10
11
|
|
11
|
-
from .
|
12
|
+
from .output import _get_output_manager
|
12
13
|
|
13
14
|
_TIMEOUT_SENTINEL = object()
|
14
15
|
|
@@ -21,7 +22,7 @@ class AppFilesFilter(DefaultFilter):
|
|
21
22
|
# Watching specific files is discouraged on Linux, so to watch a file we watch its
|
22
23
|
# containing directory and then filter that directory's changes for relevant files.
|
23
24
|
# https://github.com/notify-rs/notify/issues/394
|
24
|
-
dir_filters:
|
25
|
+
dir_filters: dict[Path, Optional[set[Path]]],
|
25
26
|
) -> None:
|
26
27
|
self.dir_filters = dir_filters
|
27
28
|
super().__init__()
|
@@ -54,7 +55,7 @@ class AppFilesFilter(DefaultFilter):
|
|
54
55
|
return super().__call__(change, path)
|
55
56
|
|
56
57
|
|
57
|
-
async def _watch_paths(paths:
|
58
|
+
async def _watch_paths(paths: set[Path], watch_filter: AppFilesFilter) -> AsyncGenerator[set[str], None]:
|
58
59
|
try:
|
59
60
|
async for changes in awatch(*paths, step=500, watch_filter=watch_filter):
|
60
61
|
changed_paths = {stringpath for _, stringpath in changes}
|
@@ -64,7 +65,7 @@ async def _watch_paths(paths: Set[Path], watch_filter: AppFilesFilter) -> AsyncG
|
|
64
65
|
pass
|
65
66
|
|
66
67
|
|
67
|
-
def _print_watched_paths(paths:
|
68
|
+
def _print_watched_paths(paths: set[Path]):
|
68
69
|
msg = "️️⚡️ Serving... hit Ctrl-C to stop!"
|
69
70
|
|
70
71
|
output_tree = Tree(msg, guide_style="gray50")
|
@@ -72,12 +73,13 @@ def _print_watched_paths(paths: Set[Path], output_mgr: OutputManager):
|
|
72
73
|
for path in paths:
|
73
74
|
output_tree.add(f"Watching {path}.")
|
74
75
|
|
75
|
-
output_mgr
|
76
|
+
if output_mgr := _get_output_manager():
|
77
|
+
output_mgr.print(output_tree)
|
76
78
|
|
77
79
|
|
78
|
-
def _watch_args_from_mounts(mounts:
|
80
|
+
def _watch_args_from_mounts(mounts: list[_Mount]) -> tuple[set[Path], AppFilesFilter]:
|
79
81
|
paths = set()
|
80
|
-
dir_filters:
|
82
|
+
dir_filters: dict[Path, Optional[set[Path]]] = defaultdict(set)
|
81
83
|
for mount in mounts:
|
82
84
|
# TODO(elias): Make this part of the mount class instead, since it uses so much internals
|
83
85
|
for entry in mount._entries:
|
@@ -93,10 +95,10 @@ def _watch_args_from_mounts(mounts: List[_Mount]) -> Tuple[Set[Path], AppFilesFi
|
|
93
95
|
return paths, watch_filter
|
94
96
|
|
95
97
|
|
96
|
-
async def watch(mounts:
|
98
|
+
async def watch(mounts: list[_Mount]) -> AsyncGenerator[set[str], None]:
|
97
99
|
paths, watch_filter = _watch_args_from_mounts(mounts)
|
98
100
|
|
99
|
-
_print_watched_paths(paths
|
101
|
+
_print_watched_paths(paths)
|
100
102
|
|
101
103
|
async for updated_paths in _watch_paths(paths, watch_filter):
|
102
104
|
yield updated_paths
|