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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +420 -937
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +5 -7
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -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 Stub
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
- stub = Stub()
15
-
16
- with pytest.raises(DeprecationError):
17
- stub.function(gpu=True)(dummy)
18
-
19
-
20
- def test_gpu_any_function(client, servicer):
21
- stub = Stub()
22
-
23
- stub.function(gpu="any")(dummy)
24
- with stub.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
- stub = Stub()
35
-
36
- # Invalid enum value.
37
- with pytest.raises(InvalidError):
38
- stub.function(gpu="foo")(dummy)
39
-
40
- stub.function(gpu="A100")(dummy)
41
- with stub.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
- stub = Stub()
52
-
53
- # Invalid count values.
54
- with pytest.raises(InvalidError):
55
- stub.function(gpu="A10G:hello")(dummy)
56
- with pytest.raises(InvalidError):
57
- stub.function(gpu="Nonexistent:2")(dummy)
58
-
59
- stub.function(gpu="A10G:4")(dummy)
60
- with stub.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
- stub = Stub()
73
-
74
- stub.function(gpu=modal.gpu.A100())(dummy)
75
- with stub.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
- stub = Stub()
88
-
89
- stub.function(gpu=modal.gpu.A100(), cloud="gcp")(dummy)
90
- with stub.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
- stub.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
- stub = Stub()
119
- if isinstance(memory_arg, int):
120
- stub.function(gpu=modal.gpu.A100(memory=memory_arg))(dummy)
121
- elif isinstance(memory_arg, str):
122
- stub.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 stub.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
- stub = Stub()
140
-
141
- with pytest.raises(ValueError, match="A100 20GB is unsupported, consider"):
142
- stub.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
- stub = Stub()
150
-
151
- # Task type does not change when user asks more than 1 GPU on an A100.
152
- stub.function(gpu=modal.gpu.A100(count=count))(dummy)
153
- with stub.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,141 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import asyncio
3
- import math
4
- import pytest
5
- import time
6
-
7
- from grpclib import GRPCError, Status
8
-
9
- from modal._utils.grpc_utils import ChannelPool, create_channel, retry_transient_errors
10
- from modal_proto import api_grpc, api_pb2
11
-
12
- from .supports.skip import skip_windows_unix_socket
13
-
14
-
15
- @pytest.mark.asyncio
16
- async def test_http_channel(servicer):
17
- assert servicer.remote_addr.startswith("http://")
18
- channel = create_channel(servicer.remote_addr, use_pool=False)
19
- client_stub = api_grpc.ModalClientStub(channel)
20
-
21
- req = api_pb2.BlobCreateRequest()
22
- resp = await client_stub.BlobCreate(req)
23
- assert resp.blob_id
24
-
25
- channel.close()
26
-
27
-
28
- @skip_windows_unix_socket
29
- @pytest.mark.asyncio
30
- async def test_unix_channel(unix_servicer):
31
- assert unix_servicer.remote_addr.startswith("unix://")
32
- channel = create_channel(unix_servicer.remote_addr, use_pool=False)
33
- client_stub = api_grpc.ModalClientStub(channel)
34
-
35
- req = api_pb2.BlobCreateRequest()
36
- resp = await client_stub.BlobCreate(req)
37
- assert resp.blob_id
38
-
39
- channel.close()
40
-
41
-
42
- @pytest.mark.asyncio
43
- async def test_channel_pool(servicer, n=1000):
44
- channel_pool = create_channel(servicer.remote_addr, use_pool=True)
45
- assert isinstance(channel_pool, ChannelPool)
46
- client_stub = api_grpc.ModalClientStub(channel_pool)
47
-
48
- # Trigger a lot of requests
49
- for i in range(n):
50
- req = api_pb2.BlobCreateRequest()
51
- resp = await client_stub.BlobCreate(req)
52
- assert resp.blob_id
53
-
54
- # Make sure we created the right number of subchannels
55
- assert len(channel_pool._subchannels) == math.ceil(n / channel_pool._max_requests)
56
-
57
- channel_pool.close()
58
-
59
-
60
- @pytest.mark.asyncio
61
- async def test_channel_pool_closed_transport(servicer):
62
- channel_pool = create_channel(servicer.remote_addr, use_pool=True)
63
- assert isinstance(channel_pool, ChannelPool)
64
-
65
- connection = await channel_pool.__connect__()
66
- connection.connection_lost(None) # simulates a h2 connection being closed
67
-
68
- client_stub = api_grpc.ModalClientStub(channel_pool)
69
- req = api_pb2.BlobCreateRequest()
70
- resp = await client_stub.BlobCreate(req) # this should close the terminated connection and start a new one
71
- assert resp.blob_id
72
- channel_pool.close()
73
-
74
-
75
- @pytest.mark.asyncio
76
- async def test_channel_pool_max_active(servicer):
77
- channel_pool = create_channel(servicer.remote_addr, use_pool=True)
78
- assert isinstance(channel_pool, ChannelPool)
79
- channel_pool._max_active = 1.0
80
- client_stub = api_grpc.ModalClientStub(channel_pool)
81
-
82
- # Do a few requests and assert there's just one subchannel
83
- for i in range(3):
84
- req = api_pb2.BlobCreateRequest()
85
- resp = await client_stub.BlobCreate(req)
86
- assert resp.blob_id
87
- assert len(channel_pool._subchannels) == 1
88
-
89
- # Sleep a couple of seconds and do a new request: it should create a new subchannel
90
- await asyncio.sleep(2.0)
91
- req = api_pb2.BlobCreateRequest()
92
- resp = await client_stub.BlobCreate(req)
93
- assert resp.blob_id
94
- assert len(channel_pool._subchannels) == 2
95
-
96
- channel_pool.close()
97
-
98
-
99
- @pytest.mark.asyncio
100
- async def test_retry_transient_errors(servicer):
101
- channel = create_channel(servicer.remote_addr)
102
- client_stub = api_grpc.ModalClientStub(channel)
103
-
104
- # Use the BlobCreate request for retries
105
- req = api_pb2.BlobCreateRequest()
106
-
107
- # Fail 3 times -> should still succeed
108
- servicer.fail_blob_create = [Status.UNAVAILABLE] * 3
109
- assert await retry_transient_errors(client_stub.BlobCreate, req)
110
- assert servicer.blob_create_metadata.get("x-idempotency-key")
111
- assert servicer.blob_create_metadata.get("x-retry-attempt") == "3"
112
-
113
- # Fail 4 times -> should fail
114
- servicer.fail_blob_create = [Status.UNAVAILABLE] * 4
115
- with pytest.raises(GRPCError):
116
- await retry_transient_errors(client_stub.BlobCreate, req)
117
- assert servicer.blob_create_metadata.get("x-idempotency-key")
118
- assert servicer.blob_create_metadata.get("x-retry-attempt") == "3"
119
-
120
- # Fail 5 times, but set max_retries to infinity
121
- servicer.fail_blob_create = [Status.UNAVAILABLE] * 5
122
- assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, base_delay=0)
123
- assert servicer.blob_create_metadata.get("x-idempotency-key")
124
- assert servicer.blob_create_metadata.get("x-retry-attempt") == "5"
125
-
126
- # Not a transient error.
127
- servicer.fail_blob_create = [Status.PERMISSION_DENIED]
128
- with pytest.raises(GRPCError):
129
- assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, base_delay=0)
130
- assert servicer.blob_create_metadata.get("x-idempotency-key")
131
- assert servicer.blob_create_metadata.get("x-retry-attempt") == "0"
132
-
133
- # Make sure to respect total_timeout
134
- t0 = time.time()
135
- servicer.fail_blob_create = [Status.UNAVAILABLE] * 99
136
- with pytest.raises(GRPCError):
137
- assert await retry_transient_errors(client_stub.BlobCreate, req, max_retries=None, total_timeout=3)
138
- total_time = time.time() - t0
139
- assert total_time <= 3.1
140
-
141
- channel.close()
test/helpers.py DELETED
@@ -1,42 +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_stub_externally(
10
- servicer,
11
- file_or_module: str,
12
- stub_variable: Optional[str] = None,
13
- deployment_name="Deployment",
14
- cwd=None,
15
- env={},
16
- capture_output=True,
17
- ) -> Optional[str]:
18
- # deploys a stub from another interpreter to prevent leaking state from client into a container process (apart from what goes through the servicer)
19
- # also has the advantage that no modules imported by the test files themselves will be added to sys.modules and included in mounts etc.
20
- windows_support: dict[str, str] = {}
21
-
22
- if sys.platform == "win32":
23
- windows_support = {
24
- **os.environ.copy(),
25
- **{"PYTHONUTF8": "1"},
26
- } # windows apparently needs a bunch of env vars to start python...
27
-
28
- env = {**windows_support, "MODAL_SERVER_URL": servicer.remote_addr, **env}
29
- if cwd is None:
30
- cwd = pathlib.Path(__file__).parent.parent
31
-
32
- stub_ref = file_or_module if stub_variable is None else f"{file_or_module}::{stub_variable}"
33
-
34
- p = subprocess.Popen(
35
- [sys.executable, "-m", "modal.cli.entry_point", "deploy", stub_ref, "--name", deployment_name],
36
- cwd=cwd,
37
- env=env,
38
- stderr=subprocess.STDOUT,
39
- stdout=subprocess.PIPE if capture_output else None,
40
- )
41
- stdout, _ = p.communicate()
42
- return stdout.decode("utf8")