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/_utils/mount_utils.py
CHANGED
@@ -1,26 +1,29 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
import posixpath
|
3
3
|
import typing
|
4
|
+
from collections.abc import Mapping, Sequence
|
4
5
|
from pathlib import PurePath, PurePosixPath
|
5
|
-
from typing import
|
6
|
+
from typing import Union
|
6
7
|
|
8
|
+
from ..cloud_bucket_mount import _CloudBucketMount
|
7
9
|
from ..exception import InvalidError
|
10
|
+
from ..network_file_system import _NetworkFileSystem
|
8
11
|
from ..volume import _Volume
|
9
12
|
|
10
|
-
|
11
|
-
from ..cloud_bucket_mount import _CloudBucketMount
|
12
|
-
from ..network_file_system import _NetworkFileSystem
|
13
|
-
|
14
|
-
|
15
|
-
T = typing.TypeVar("T", bound=Union["_Volume", "_NetworkFileSystem", "_CloudBucketMount"])
|
13
|
+
T = typing.TypeVar("T", bound=Union[_Volume, _NetworkFileSystem, _CloudBucketMount])
|
16
14
|
|
17
15
|
|
18
16
|
def validate_mount_points(
|
19
17
|
display_name: str,
|
20
18
|
volume_likes: Mapping[Union[str, PurePosixPath], T],
|
21
|
-
) ->
|
19
|
+
) -> list[tuple[str, T]]:
|
22
20
|
"""Mount point path validation for volumes and network file systems."""
|
23
21
|
|
22
|
+
if not isinstance(volume_likes, dict):
|
23
|
+
raise InvalidError(
|
24
|
+
f"`volume_likes` should be a dict[str | PurePosixPath, {display_name}], got {type(volume_likes)} instead"
|
25
|
+
)
|
26
|
+
|
24
27
|
validated = []
|
25
28
|
for path, vol in volume_likes.items():
|
26
29
|
path = PurePath(path).as_posix()
|
@@ -38,17 +41,32 @@ def validate_mount_points(
|
|
38
41
|
return validated
|
39
42
|
|
40
43
|
|
41
|
-
def
|
42
|
-
|
43
|
-
)
|
44
|
-
|
45
|
-
|
44
|
+
def validate_network_file_systems(
|
45
|
+
network_file_systems: Mapping[Union[str, PurePosixPath], _NetworkFileSystem],
|
46
|
+
):
|
47
|
+
validated_network_file_systems = validate_mount_points("NetworkFileSystem", network_file_systems)
|
48
|
+
|
49
|
+
for path, network_file_system in validated_network_file_systems:
|
50
|
+
if not isinstance(network_file_system, (_NetworkFileSystem)):
|
51
|
+
raise InvalidError(
|
52
|
+
f"Object of type {type(network_file_system)} mounted at '{path}' "
|
53
|
+
+ "is not useable as a network file system."
|
54
|
+
)
|
55
|
+
|
56
|
+
return validated_network_file_systems
|
46
57
|
|
58
|
+
|
59
|
+
def validate_volumes(
|
60
|
+
volumes: Mapping[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]],
|
61
|
+
) -> Sequence[tuple[str, Union[_Volume, _CloudBucketMount]]]:
|
47
62
|
validated_volumes = validate_mount_points("Volume", volumes)
|
48
|
-
# We don't support mounting a
|
49
|
-
|
63
|
+
# We don't support mounting a modal.Volume in more than one location,
|
64
|
+
# but the same CloudBucketMount object can be used in more than one location.
|
65
|
+
volume_to_paths: dict[_Volume, list[str]] = {}
|
50
66
|
for path, volume in validated_volumes:
|
51
|
-
if isinstance(volume, _Volume):
|
67
|
+
if not isinstance(volume, (_Volume, _CloudBucketMount)):
|
68
|
+
raise InvalidError(f"Object of type {type(volume)} mounted at '{path}' is not useable as a volume.")
|
69
|
+
elif isinstance(volume, (_Volume)):
|
52
70
|
volume_to_paths.setdefault(volume, []).append(path)
|
53
71
|
for paths in volume_to_paths.values():
|
54
72
|
if len(paths) > 1:
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright Modal Labs 2022
|
2
|
+
import re
|
3
|
+
|
4
|
+
from ..exception import InvalidError
|
5
|
+
|
6
|
+
# https://www.rfc-editor.org/rfc/rfc1035
|
7
|
+
subdomain_regex = re.compile("^(?![0-9]+$)(?!-)[a-z0-9-]{,63}(?<!-)$")
|
8
|
+
|
9
|
+
|
10
|
+
def is_valid_subdomain_label(label: str) -> bool:
|
11
|
+
return subdomain_regex.match(label) is not None
|
12
|
+
|
13
|
+
|
14
|
+
def replace_invalid_subdomain_chars(label: str) -> str:
|
15
|
+
return re.sub("[^a-z0-9-]", "-", label.lower())
|
16
|
+
|
17
|
+
|
18
|
+
def is_valid_object_name(name: str) -> bool:
|
19
|
+
return (
|
20
|
+
# Limit object name length
|
21
|
+
len(name) <= 64
|
22
|
+
# Limit character set
|
23
|
+
and re.match("^[a-zA-Z0-9-_.]+$", name) is not None
|
24
|
+
# Avoid collisions with App IDs
|
25
|
+
and re.match("^ap-[a-zA-Z0-9]{22}$", name) is None
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def is_valid_environment_name(name: str) -> bool:
|
30
|
+
# first char is alnum, the rest allows other chars
|
31
|
+
return len(name) <= 64 and re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-_.]+$", name) is not None
|
32
|
+
|
33
|
+
|
34
|
+
def is_valid_tag(tag: str) -> bool:
|
35
|
+
"""Tags are alphanumeric, dashes, periods, and underscores, and must be 50 characters or less"""
|
36
|
+
pattern = r"^[a-zA-Z0-9._-]{1,50}$"
|
37
|
+
return bool(re.match(pattern, tag))
|
38
|
+
|
39
|
+
|
40
|
+
def check_object_name(name: str, object_type: str) -> None:
|
41
|
+
message = (
|
42
|
+
f"Invalid {object_type} name: '{name}'."
|
43
|
+
"\n\nNames may contain only alphanumeric characters, dashes, periods, and underscores,"
|
44
|
+
" must be shorter than 64 characters, and cannot conflict with App ID strings."
|
45
|
+
)
|
46
|
+
if not is_valid_object_name(name):
|
47
|
+
raise InvalidError(message)
|
48
|
+
|
49
|
+
|
50
|
+
def check_environment_name(name: str) -> None:
|
51
|
+
message = (
|
52
|
+
f"Invalid environment name: '{name}'."
|
53
|
+
"\n\nEnvironment names can only start with alphanumeric characters,"
|
54
|
+
" may contain only alphanumeric characters, dashes, periods, and underscores,"
|
55
|
+
" and must be shorter than 64 characters."
|
56
|
+
)
|
57
|
+
if not is_valid_environment_name(name):
|
58
|
+
raise InvalidError(message)
|
modal/_utils/package_utils.py
CHANGED
@@ -23,7 +23,7 @@ def get_file_formats(module):
|
|
23
23
|
BINARY_FORMATS = ["so", "S", "s", "asm"] # TODO
|
24
24
|
|
25
25
|
|
26
|
-
def get_module_mount_info(module_name: str) -> typing.Sequence[
|
26
|
+
def get_module_mount_info(module_name: str) -> typing.Sequence[tuple[bool, Path]]:
|
27
27
|
"""Returns a list of tuples [(is_dir, path)] describing how to mount a given module."""
|
28
28
|
file_formats = get_file_formats(module_name)
|
29
29
|
if set(BINARY_FORMATS) & set(file_formats):
|
@@ -46,3 +46,16 @@ def get_module_mount_info(module_name: str) -> typing.Sequence[typing.Tuple[bool
|
|
46
46
|
if not entries:
|
47
47
|
raise ModuleNotMountable(f"{module_name} has no mountable paths")
|
48
48
|
return entries
|
49
|
+
|
50
|
+
|
51
|
+
def parse_major_minor_version(version_string: str) -> tuple[int, int]:
|
52
|
+
parts = version_string.split(".")
|
53
|
+
if len(parts) < 2:
|
54
|
+
raise ValueError("version_string must have at least an 'X.Y' format")
|
55
|
+
try:
|
56
|
+
major = int(parts[0])
|
57
|
+
minor = int(parts[1])
|
58
|
+
except ValueError:
|
59
|
+
raise ValueError("version_string must have at least an 'X.Y' format with integral major/minor values")
|
60
|
+
|
61
|
+
return major, minor
|
@@ -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),
|
@@ -58,12 +58,10 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
58
58
|
else:
|
59
59
|
if field.type == FieldDescriptor.TYPE_ENUM:
|
60
60
|
enum_values = [x.number for x in field.enum_type.values]
|
61
|
-
|
62
|
-
def generator(rand):
|
63
|
-
return rand.choice(enum_values)
|
61
|
+
generator = lambda rand: rand.choice(enum_values) # noqa: E731
|
64
62
|
|
65
63
|
else:
|
66
|
-
generator = _FIELD_RANDOM_GENERATOR
|
64
|
+
generator = _FIELD_RANDOM_GENERATOR[field.type]
|
67
65
|
if is_repeated:
|
68
66
|
num = rand.randint(0, 2)
|
69
67
|
msg_field = getattr(msg, field.name)
|
@@ -73,7 +71,7 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
73
71
|
setattr(msg, field.name, generator(rand))
|
74
72
|
|
75
73
|
|
76
|
-
def rand_pb(proto:
|
74
|
+
def rand_pb(proto: type[T], rand: Optional[Random] = None) -> T:
|
77
75
|
"""Generate a pseudorandom protobuf message.
|
78
76
|
|
79
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.")
|