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
test/function_utils_test.py
DELETED
@@ -1,101 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2023
|
2
|
-
import pytest
|
3
|
-
from typing import List
|
4
|
-
|
5
|
-
from modal import Queue
|
6
|
-
from modal._utils.function_utils import FunctionInfo, get_referred_objects, method_has_params
|
7
|
-
from modal.exception import InvalidError
|
8
|
-
from modal.object import Object
|
9
|
-
|
10
|
-
q1 = Queue.from_name("q1", create_if_missing=True)
|
11
|
-
q2 = Queue.from_name("q2", create_if_missing=True)
|
12
|
-
|
13
|
-
|
14
|
-
def f1():
|
15
|
-
q1.get()
|
16
|
-
|
17
|
-
|
18
|
-
def f2():
|
19
|
-
f1()
|
20
|
-
q2.get()
|
21
|
-
|
22
|
-
|
23
|
-
def test_referred_objects():
|
24
|
-
objs: List[Object] = get_referred_objects(f1)
|
25
|
-
assert objs == [q1]
|
26
|
-
|
27
|
-
|
28
|
-
def test_referred_objects_recursive():
|
29
|
-
objs: List[Object] = get_referred_objects(f2)
|
30
|
-
assert set(objs) == set([q1, q2])
|
31
|
-
|
32
|
-
|
33
|
-
def recursive():
|
34
|
-
recursive()
|
35
|
-
|
36
|
-
|
37
|
-
def test_recursive():
|
38
|
-
get_referred_objects(recursive)
|
39
|
-
|
40
|
-
|
41
|
-
l = [q1, q2]
|
42
|
-
|
43
|
-
|
44
|
-
def refers_list():
|
45
|
-
return len(l)
|
46
|
-
|
47
|
-
|
48
|
-
def test_refers_list():
|
49
|
-
objs: List[Object] = get_referred_objects(refers_list)
|
50
|
-
assert objs == [] # This may return [q1, q2] in the future
|
51
|
-
|
52
|
-
|
53
|
-
def hasarg(a):
|
54
|
-
...
|
55
|
-
|
56
|
-
|
57
|
-
def noarg():
|
58
|
-
...
|
59
|
-
|
60
|
-
|
61
|
-
def defaultarg(a="hello"):
|
62
|
-
...
|
63
|
-
|
64
|
-
|
65
|
-
def wildcard_args(*wildcard_list, **wildcard_dict):
|
66
|
-
...
|
67
|
-
|
68
|
-
|
69
|
-
def test_is_nullary():
|
70
|
-
assert not FunctionInfo(hasarg).is_nullary()
|
71
|
-
assert FunctionInfo(noarg).is_nullary()
|
72
|
-
assert FunctionInfo(defaultarg).is_nullary()
|
73
|
-
assert FunctionInfo(wildcard_args).is_nullary()
|
74
|
-
|
75
|
-
|
76
|
-
class Cls:
|
77
|
-
def foo(self):
|
78
|
-
pass
|
79
|
-
|
80
|
-
def bar(self, x):
|
81
|
-
pass
|
82
|
-
|
83
|
-
def buz(self, *args):
|
84
|
-
pass
|
85
|
-
|
86
|
-
|
87
|
-
def test_method_has_params():
|
88
|
-
assert not method_has_params(Cls.foo)
|
89
|
-
assert not method_has_params(Cls().foo)
|
90
|
-
assert method_has_params(Cls.bar)
|
91
|
-
assert method_has_params(Cls().bar)
|
92
|
-
assert method_has_params(Cls.buz)
|
93
|
-
assert method_has_params(Cls().buz)
|
94
|
-
|
95
|
-
|
96
|
-
def test_nonglobal_function():
|
97
|
-
def f():
|
98
|
-
...
|
99
|
-
|
100
|
-
with pytest.raises(InvalidError, match=r"Cannot wrap `test_nonglobal_function.<locals>.f"):
|
101
|
-
FunctionInfo(f)
|
test/gpu_test.py
DELETED
@@ -1,159 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import pytest
|
3
|
-
|
4
|
-
from modal import App
|
5
|
-
from modal.exception import DeprecationError, InvalidError
|
6
|
-
from modal_proto import api_pb2
|
7
|
-
|
8
|
-
|
9
|
-
def dummy():
|
10
|
-
pass # not actually used in test (servicer returns sum of square of all args)
|
11
|
-
|
12
|
-
|
13
|
-
def test_gpu_true_function(client, servicer):
|
14
|
-
app = App()
|
15
|
-
|
16
|
-
with pytest.raises(DeprecationError):
|
17
|
-
app.function(gpu=True)(dummy)
|
18
|
-
|
19
|
-
|
20
|
-
def test_gpu_any_function(client, servicer):
|
21
|
-
app = App()
|
22
|
-
|
23
|
-
app.function(gpu="any")(dummy)
|
24
|
-
with app.run(client=client):
|
25
|
-
pass
|
26
|
-
|
27
|
-
assert len(servicer.app_functions) == 1
|
28
|
-
func_def = next(iter(servicer.app_functions.values()))
|
29
|
-
assert func_def.resources.gpu_config.count == 1
|
30
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_ANY
|
31
|
-
|
32
|
-
|
33
|
-
def test_gpu_string_config(client, servicer):
|
34
|
-
app = App()
|
35
|
-
|
36
|
-
# Invalid enum value.
|
37
|
-
with pytest.raises(InvalidError):
|
38
|
-
app.function(gpu="foo")(dummy)
|
39
|
-
|
40
|
-
app.function(gpu="A100")(dummy)
|
41
|
-
with app.run(client=client):
|
42
|
-
pass
|
43
|
-
|
44
|
-
assert len(servicer.app_functions) == 1
|
45
|
-
func_def = next(iter(servicer.app_functions.values()))
|
46
|
-
assert func_def.resources.gpu_config.count == 1
|
47
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_A100
|
48
|
-
|
49
|
-
|
50
|
-
def test_gpu_string_count_config(client, servicer):
|
51
|
-
app = App()
|
52
|
-
|
53
|
-
# Invalid count values.
|
54
|
-
with pytest.raises(InvalidError):
|
55
|
-
app.function(gpu="A10G:hello")(dummy)
|
56
|
-
with pytest.raises(InvalidError):
|
57
|
-
app.function(gpu="Nonexistent:2")(dummy)
|
58
|
-
|
59
|
-
app.function(gpu="A10G:4")(dummy)
|
60
|
-
with app.run(client=client):
|
61
|
-
pass
|
62
|
-
|
63
|
-
assert len(servicer.app_functions) == 1
|
64
|
-
func_def = next(iter(servicer.app_functions.values()))
|
65
|
-
assert func_def.resources.gpu_config.count == 4
|
66
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_A10G
|
67
|
-
|
68
|
-
|
69
|
-
def test_gpu_config_function(client, servicer):
|
70
|
-
import modal
|
71
|
-
|
72
|
-
app = App()
|
73
|
-
|
74
|
-
app.function(gpu=modal.gpu.A100())(dummy)
|
75
|
-
with app.run(client=client):
|
76
|
-
pass
|
77
|
-
|
78
|
-
assert len(servicer.app_functions) == 1
|
79
|
-
func_def = next(iter(servicer.app_functions.values()))
|
80
|
-
assert func_def.resources.gpu_config.count == 1
|
81
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_A100
|
82
|
-
|
83
|
-
|
84
|
-
def test_cloud_provider_selection(client, servicer):
|
85
|
-
import modal
|
86
|
-
|
87
|
-
app = App()
|
88
|
-
|
89
|
-
app.function(gpu=modal.gpu.A100(), cloud="gcp")(dummy)
|
90
|
-
with app.run(client=client):
|
91
|
-
pass
|
92
|
-
|
93
|
-
assert len(servicer.app_functions) == 1
|
94
|
-
func_def = next(iter(servicer.app_functions.values()))
|
95
|
-
assert func_def.cloud_provider == api_pb2.CLOUD_PROVIDER_GCP
|
96
|
-
|
97
|
-
assert func_def.resources.gpu_config.count == 1
|
98
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_A100
|
99
|
-
|
100
|
-
# Invalid enum value.
|
101
|
-
with pytest.raises(InvalidError):
|
102
|
-
app.function(cloud="foo")(dummy)
|
103
|
-
|
104
|
-
|
105
|
-
@pytest.mark.parametrize(
|
106
|
-
"memory_arg,gpu_type,memory_gb",
|
107
|
-
[
|
108
|
-
(0, api_pb2.GPU_TYPE_A100, 40),
|
109
|
-
(40, api_pb2.GPU_TYPE_A100, 40),
|
110
|
-
(80, api_pb2.GPU_TYPE_A100_80GB, 80),
|
111
|
-
("40GB", api_pb2.GPU_TYPE_A100, 40),
|
112
|
-
("80GB", api_pb2.GPU_TYPE_A100_80GB, 80),
|
113
|
-
],
|
114
|
-
)
|
115
|
-
def test_memory_selection_gpu_variant(client, servicer, memory_arg, gpu_type, memory_gb):
|
116
|
-
import modal
|
117
|
-
|
118
|
-
app = App()
|
119
|
-
if isinstance(memory_arg, int):
|
120
|
-
app.function(gpu=modal.gpu.A100(memory=memory_arg))(dummy)
|
121
|
-
elif isinstance(memory_arg, str):
|
122
|
-
app.function(gpu=modal.gpu.A100(size=memory_arg))(dummy)
|
123
|
-
else:
|
124
|
-
raise RuntimeError(f"Unexpected test parameterization arg type {type(memory_arg)}")
|
125
|
-
|
126
|
-
with app.run(client=client):
|
127
|
-
pass
|
128
|
-
|
129
|
-
func_def = next(iter(servicer.app_functions.values()))
|
130
|
-
|
131
|
-
assert func_def.resources.gpu_config.count == 1
|
132
|
-
assert func_def.resources.gpu_config.type == gpu_type
|
133
|
-
assert func_def.resources.gpu_config.memory == memory_gb
|
134
|
-
|
135
|
-
|
136
|
-
def test_a100_20gb_gpu_unsupported():
|
137
|
-
import modal
|
138
|
-
|
139
|
-
app = App()
|
140
|
-
|
141
|
-
with pytest.raises(ValueError, match="A100 20GB is unsupported, consider"):
|
142
|
-
app.function(gpu=modal.gpu.A100(memory=20))(dummy)
|
143
|
-
|
144
|
-
|
145
|
-
@pytest.mark.parametrize("count", [1, 2, 3, 4])
|
146
|
-
def test_gpu_type_selection_from_count(client, servicer, count):
|
147
|
-
import modal
|
148
|
-
|
149
|
-
app = App()
|
150
|
-
|
151
|
-
# Task type does not change when user asks more than 1 GPU on an A100.
|
152
|
-
app.function(gpu=modal.gpu.A100(count=count))(dummy)
|
153
|
-
with app.run(client=client):
|
154
|
-
pass
|
155
|
-
|
156
|
-
func_def = next(iter(servicer.app_functions.values()))
|
157
|
-
|
158
|
-
assert func_def.resources.gpu_config.count == count
|
159
|
-
assert func_def.resources.gpu_config.type == api_pb2.GPU_TYPE_A100
|
test/grpc_utils_test.py
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import pytest
|
3
|
-
import time
|
4
|
-
|
5
|
-
from grpclib import GRPCError, Status
|
6
|
-
|
7
|
-
from modal._utils.grpc_utils import create_channel, retry_transient_errors
|
8
|
-
from modal_proto import api_grpc, api_pb2
|
9
|
-
|
10
|
-
from .supports.skip import skip_windows_unix_socket
|
11
|
-
|
12
|
-
|
13
|
-
@pytest.mark.asyncio
|
14
|
-
async def test_http_channel(servicer):
|
15
|
-
assert servicer.remote_addr.startswith("http://")
|
16
|
-
channel = create_channel(servicer.remote_addr)
|
17
|
-
client_stub = api_grpc.ModalClientStub(channel)
|
18
|
-
|
19
|
-
req = api_pb2.BlobCreateRequest()
|
20
|
-
resp = await client_stub.BlobCreate(req)
|
21
|
-
assert resp.blob_id
|
22
|
-
|
23
|
-
channel.close()
|
24
|
-
|
25
|
-
|
26
|
-
@skip_windows_unix_socket
|
27
|
-
@pytest.mark.asyncio
|
28
|
-
async def test_unix_channel(unix_servicer):
|
29
|
-
assert unix_servicer.remote_addr.startswith("unix://")
|
30
|
-
channel = create_channel(unix_servicer.remote_addr)
|
31
|
-
client_stub = api_grpc.ModalClientStub(channel)
|
32
|
-
|
33
|
-
req = api_pb2.BlobCreateRequest()
|
34
|
-
resp = await client_stub.BlobCreate(req)
|
35
|
-
assert resp.blob_id
|
36
|
-
|
37
|
-
channel.close()
|
38
|
-
|
39
|
-
|
40
|
-
@pytest.mark.asyncio
|
41
|
-
async def test_retry_transient_errors(servicer):
|
42
|
-
channel = create_channel(servicer.remote_addr)
|
43
|
-
client_stub = api_grpc.ModalClientStub(channel)
|
44
|
-
|
45
|
-
# Use the BlobCreate request for retries
|
46
|
-
req = api_pb2.BlobCreateRequest()
|
47
|
-
|
48
|
-
# Fail 3 times -> should still succeed
|
49
|
-
servicer.fail_blob_create = [Status.UNAVAILABLE] * 3
|
50
|
-
assert await retry_transient_errors(client_stub.BlobCreate, req)
|
51
|
-
assert servicer.blob_create_metadata.get("x-idempotency-key")
|
52
|
-
assert servicer.blob_create_metadata.get("x-retry-attempt") == "3"
|
53
|
-
|
54
|
-
# Fail 4 times -> should fail
|
55
|
-
servicer.fail_blob_create = [Status.UNAVAILABLE] * 4
|
56
|
-
with pytest.raises(GRPCError):
|
57
|
-
await retry_transient_errors(client_stub.BlobCreate, req)
|
58
|
-
assert servicer.blob_create_metadata.get("x-idempotency-key")
|
59
|
-
assert servicer.blob_create_metadata.get("x-retry-attempt") == "3"
|
60
|
-
|
61
|
-
# Fail 5 times, but set max_retries to infinity
|
62
|
-
servicer.fail_blob_create = [Status.UNAVAILABLE] * 5
|
63
|
-
assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, base_delay=0)
|
64
|
-
assert servicer.blob_create_metadata.get("x-idempotency-key")
|
65
|
-
assert servicer.blob_create_metadata.get("x-retry-attempt") == "5"
|
66
|
-
|
67
|
-
# Not a transient error.
|
68
|
-
servicer.fail_blob_create = [Status.PERMISSION_DENIED]
|
69
|
-
with pytest.raises(GRPCError):
|
70
|
-
assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, base_delay=0)
|
71
|
-
assert servicer.blob_create_metadata.get("x-idempotency-key")
|
72
|
-
assert servicer.blob_create_metadata.get("x-retry-attempt") == "0"
|
73
|
-
|
74
|
-
# Make sure to respect total_timeout
|
75
|
-
t0 = time.time()
|
76
|
-
servicer.fail_blob_create = [Status.UNAVAILABLE] * 99
|
77
|
-
with pytest.raises(GRPCError):
|
78
|
-
assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, total_timeout=3)
|
79
|
-
total_time = time.time() - t0
|
80
|
-
assert total_time <= 3.1
|
81
|
-
|
82
|
-
channel.close()
|
test/helpers.py
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2023
|
2
|
-
import os
|
3
|
-
import pathlib
|
4
|
-
import subprocess
|
5
|
-
import sys
|
6
|
-
from typing import Optional
|
7
|
-
|
8
|
-
|
9
|
-
def deploy_app_externally(
|
10
|
-
servicer,
|
11
|
-
file_or_module: str,
|
12
|
-
app_variable: Optional[str] = None,
|
13
|
-
deployment_name="Deployment",
|
14
|
-
cwd=None,
|
15
|
-
env={},
|
16
|
-
capture_output=True,
|
17
|
-
) -> Optional[str]:
|
18
|
-
# deploys an app from another interpreter to prevent leaking state from client into a container process
|
19
|
-
# (apart from what goes through the servicer) also has the advantage that no modules imported by the
|
20
|
-
# test files themselves will be added to sys.modules and included in mounts etc.
|
21
|
-
windows_support: dict[str, str] = {}
|
22
|
-
|
23
|
-
if sys.platform == "win32":
|
24
|
-
windows_support = {
|
25
|
-
**os.environ.copy(),
|
26
|
-
**{"PYTHONUTF8": "1"},
|
27
|
-
} # windows apparently needs a bunch of env vars to start python...
|
28
|
-
|
29
|
-
env = {**windows_support, "MODAL_SERVER_URL": servicer.remote_addr, **env}
|
30
|
-
if cwd is None:
|
31
|
-
cwd = pathlib.Path(__file__).parent.parent
|
32
|
-
|
33
|
-
app_ref = file_or_module if app_variable is None else f"{file_or_module}::{app_variable}"
|
34
|
-
|
35
|
-
p = subprocess.Popen(
|
36
|
-
[sys.executable, "-m", "modal.cli.entry_point", "deploy", app_ref, "--name", deployment_name],
|
37
|
-
cwd=cwd,
|
38
|
-
env=env,
|
39
|
-
stderr=subprocess.STDOUT,
|
40
|
-
stdout=subprocess.PIPE if capture_output else None,
|
41
|
-
)
|
42
|
-
stdout_b, stderr_b = p.communicate()
|
43
|
-
stdout_s, stderr_s = (b.decode() if b is not None else None for b in (stdout_b, stderr_b))
|
44
|
-
if p.returncode != 0:
|
45
|
-
print(f"Deploying app failed!\n### stdout ###\n{stdout_s}\n### stderr ###\n{stderr_s}")
|
46
|
-
raise Exception("Test helper failed to deploy app")
|
47
|
-
return stdout_s
|