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.
Files changed (220) hide show
  1. modal/__init__.py +13 -9
  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 +402 -398
  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 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  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 +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  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 +3 -3
  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 +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  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 +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.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 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