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
test/stub_test.py DELETED
@@ -1,360 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import asyncio
3
- import logging
4
- import pytest
5
-
6
- from google.protobuf.empty_pb2 import Empty
7
- from grpclib import GRPCError, Status
8
-
9
- import modal.app
10
- from modal import Dict, Image, Queue, Stub, web_endpoint
11
- from modal.config import config
12
- from modal.exception import DeprecationError, ExecutionError, InvalidError, NotFoundError
13
- from modal.partial_function import _parse_custom_domains
14
- from modal.runner import deploy_stub
15
- from modal_proto import api_pb2
16
-
17
- from .supports import module_1, module_2
18
-
19
-
20
- @pytest.mark.asyncio
21
- async def test_kwargs(servicer, client):
22
- with pytest.warns(DeprecationError):
23
- stub = Stub(
24
- d=Dict.new(),
25
- q=Queue.new(),
26
- )
27
- async with stub.run(client=client):
28
- with pytest.warns(DeprecationError):
29
- await stub["d"].put.aio("foo", "bar") # type: ignore
30
- await stub["q"].put.aio("baz") # type: ignore
31
- assert await stub["d"].get.aio("foo") == "bar" # type: ignore
32
- assert await stub["q"].get.aio() == "baz" # type: ignore
33
-
34
-
35
- @pytest.mark.asyncio
36
- async def test_attrs(servicer, client):
37
- stub = Stub()
38
- with pytest.warns(DeprecationError):
39
- stub.d = Dict.new()
40
- stub.q = Queue.new()
41
- async with stub.run(client=client):
42
- with pytest.warns(DeprecationError):
43
- await stub.d.put.aio("foo", "bar") # type: ignore
44
- await stub.q.put.aio("baz") # type: ignore
45
- assert await stub.d.get.aio("foo") == "bar" # type: ignore
46
- assert await stub.q.get.aio() == "baz" # type: ignore
47
-
48
-
49
- @pytest.mark.asyncio
50
- async def test_stub_type_validation(servicer, client):
51
- with pytest.raises(InvalidError):
52
- with pytest.warns(DeprecationError):
53
- stub = Stub(
54
- foo=4242, # type: ignore
55
- )
56
-
57
- stub = Stub()
58
-
59
- with pytest.raises(InvalidError) as excinfo:
60
- stub.bar = 4242 # type: ignore
61
-
62
- assert "4242" in str(excinfo.value)
63
-
64
-
65
- def square(x):
66
- return x**2
67
-
68
-
69
- @pytest.mark.asyncio
70
- async def test_redeploy(servicer, client):
71
- stub = Stub(image=Image.debian_slim().pip_install("pandas"))
72
- stub.function()(square)
73
-
74
- # Deploy app
75
- app = await deploy_stub.aio(stub, "my-app", client=client)
76
- assert app.app_id == "ap-1"
77
- assert servicer.app_objects["ap-1"]["square"] == "fu-1"
78
- assert servicer.app_state_history[app.app_id] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DEPLOYED]
79
-
80
- # Redeploy, make sure all ids are the same
81
- app = await deploy_stub.aio(stub, "my-app", client=client)
82
- assert app.app_id == "ap-1"
83
- assert servicer.app_objects["ap-1"]["square"] == "fu-1"
84
- assert servicer.app_state_history[app.app_id] == [
85
- api_pb2.APP_STATE_INITIALIZING,
86
- api_pb2.APP_STATE_DEPLOYED,
87
- api_pb2.APP_STATE_DEPLOYED,
88
- ]
89
-
90
- # Deploy to a different name, ids should change
91
- app = await deploy_stub.aio(stub, "my-app-xyz", client=client)
92
- assert app.app_id == "ap-2"
93
- assert servicer.app_objects["ap-2"]["square"] == "fu-2"
94
- assert servicer.app_state_history[app.app_id] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DEPLOYED]
95
-
96
-
97
- def dummy():
98
- pass
99
-
100
-
101
- # Should exit without waiting for the "logs_timeout" grace period.
102
- @pytest.mark.timeout(5)
103
- def test_create_object_exception(servicer, client):
104
- servicer.function_create_error = True
105
-
106
- stub = Stub()
107
- stub.function()(dummy)
108
-
109
- with pytest.raises(GRPCError) as excinfo:
110
- with stub.run(client=client):
111
- pass
112
-
113
- assert excinfo.value.status == Status.INTERNAL
114
-
115
-
116
- def test_deploy_falls_back_to_app_name(servicer, client):
117
- named_stub = Stub(name="foo_app")
118
- deploy_stub(named_stub, client=client)
119
- assert "foo_app" in servicer.deployed_apps
120
-
121
-
122
- def test_deploy_uses_deployment_name_if_specified(servicer, client):
123
- named_stub = Stub(name="foo_app")
124
- deploy_stub(named_stub, "bar_app", client=client)
125
- assert "bar_app" in servicer.deployed_apps
126
- assert "foo_app" not in servicer.deployed_apps
127
-
128
-
129
- def test_run_function_without_app_error():
130
- stub = Stub()
131
- dummy_modal = stub.function()(dummy)
132
-
133
- with pytest.raises(ExecutionError) as excinfo:
134
- dummy_modal.remote()
135
-
136
- assert "hydrated" in str(excinfo.value)
137
-
138
-
139
- def test_is_inside_basic():
140
- stub = Stub()
141
- with pytest.raises(DeprecationError, match="imports()"):
142
- stub.is_inside()
143
-
144
-
145
- def test_missing_attr():
146
- """Trying to call a non-existent function on the Stub should produce
147
- an understandable error message."""
148
-
149
- stub = Stub()
150
- with pytest.raises(AttributeError):
151
- stub.fun() # type: ignore
152
-
153
-
154
- def test_same_function_name(caplog):
155
- stub = Stub()
156
-
157
- # Add first function
158
- with caplog.at_level(logging.WARNING):
159
- stub.function()(module_1.square)
160
- assert len(caplog.records) == 0
161
-
162
- # Add second function: check warning
163
- with caplog.at_level(logging.WARNING):
164
- stub.function()(module_2.square)
165
- assert len(caplog.records) == 1
166
- assert "module_1" in caplog.text
167
- assert "module_2" in caplog.text
168
- assert "square" in caplog.text
169
-
170
-
171
- def test_run_state(client, servicer):
172
- stub = Stub()
173
- with stub.run(client=client):
174
- assert servicer.app_state_history[stub.app_id] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_EPHEMERAL]
175
-
176
-
177
- def test_deploy_state(client, servicer):
178
- stub = Stub()
179
- app = deploy_stub(stub, "foobar", client=client)
180
- assert servicer.app_state_history[app.app_id] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DEPLOYED]
181
-
182
-
183
- def test_detach_state(client, servicer):
184
- stub = Stub()
185
- with stub.run(client=client, detach=True):
186
- assert servicer.app_state_history[stub.app_id] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DETACHED]
187
-
188
-
189
- @pytest.mark.asyncio
190
- async def test_grpc_protocol(client, servicer):
191
- stub = Stub()
192
- async with stub.run(client=client):
193
- await asyncio.sleep(0.01) # wait for heartbeat
194
- assert len(servicer.requests) == 4
195
- assert isinstance(servicer.requests[0], Empty) # ClientHello
196
- assert isinstance(servicer.requests[1], api_pb2.AppCreateRequest)
197
- assert isinstance(servicer.requests[2], api_pb2.AppHeartbeatRequest)
198
- assert isinstance(servicer.requests[3], api_pb2.AppClientDisconnectRequest)
199
-
200
-
201
- async def web1(x):
202
- return {"square": x**2}
203
-
204
-
205
- async def web2(x):
206
- return {"cube": x**3}
207
-
208
-
209
- def test_registered_web_endpoints(client, servicer):
210
- stub = Stub()
211
- stub.function()(square)
212
- stub.function()(web_endpoint()(web1))
213
- stub.function()(web_endpoint()(web2))
214
-
215
- assert stub.registered_web_endpoints == ["web1", "web2"]
216
-
217
-
218
- def test_init_types():
219
- with pytest.raises(InvalidError):
220
- # singular secret to plural argument
221
- Stub(secrets=modal.Secret.from_dict()) # type: ignore
222
- with pytest.raises(InvalidError):
223
- # not a Secret Object
224
- Stub(secrets=[{"foo": "bar"}]) # type: ignore
225
- with pytest.raises(InvalidError):
226
- # blueprint needs to use _Providers
227
- with pytest.warns(DeprecationError):
228
- Stub(some_arg=5) # type: ignore
229
- with pytest.raises(InvalidError):
230
- # should be an Image
231
- Stub(image=modal.Secret.from_dict()) # type: ignore
232
-
233
- with pytest.warns(DeprecationError):
234
- Stub(
235
- image=modal.Image.debian_slim().pip_install("pandas"),
236
- secrets=[modal.Secret.from_dict()],
237
- mounts=[modal.Mount.from_local_file(__file__)],
238
- some_dict=modal.Dict.new(),
239
- some_queue=modal.Queue.new(),
240
- )
241
-
242
-
243
- def test_set_image_on_stub_as_attribute():
244
- # TODO: do we want to deprecate this syntax? It's kind of random for image to
245
- # have a reserved name in the blueprint, and being the only of the construction
246
- # arguments that can be set on the instance after construction
247
- custom_img = modal.Image.debian_slim().apt_install("emacs")
248
- stub = Stub(image=custom_img)
249
- assert stub._get_default_image() == custom_img
250
-
251
-
252
- def test_redeploy_delete_objects(servicer, client):
253
- # Deploy an app with objects d1 and d2
254
- stub = Stub()
255
- stub.function(name="d1")(dummy)
256
- stub.function(name="d2")(dummy)
257
- app = deploy_stub(stub, "xyz", client=client)
258
-
259
- # Check objects
260
- assert set(servicer.app_objects[app.app_id].keys()) == set(["d1", "d2"])
261
-
262
- # Deploy an app with objects d2 and d3
263
- stub = Stub()
264
- stub.function(name="d2")(dummy)
265
- stub.function(name="d3")(dummy)
266
- app = deploy_stub(stub, "xyz", client=client)
267
-
268
- # Make sure d1 is deleted
269
- assert set(servicer.app_objects[app.app_id].keys()) == set(["d2", "d3"])
270
-
271
-
272
- @pytest.mark.asyncio
273
- async def test_unhydrate(servicer, client):
274
- stub = Stub()
275
-
276
- f = stub.function()(dummy)
277
-
278
- assert not f.is_hydrated
279
- async with stub.run(client=client):
280
- assert f.is_hydrated
281
-
282
- # After app finishes, it should unhydrate
283
- assert not f.is_hydrated
284
-
285
-
286
- def test_keyboard_interrupt(servicer, client):
287
- stub = Stub()
288
- stub.function()(square)
289
- with stub.run(client=client):
290
- # The exit handler should catch this interrupt and exit gracefully
291
- raise KeyboardInterrupt()
292
-
293
-
294
- def test_function_image_positional():
295
- stub = Stub()
296
- image = Image.debian_slim()
297
-
298
- with pytest.raises(InvalidError) as excinfo:
299
-
300
- @stub.function(image) # type: ignore
301
- def f():
302
- pass
303
-
304
- assert "function(image=image)" in str(excinfo.value)
305
-
306
-
307
- @pytest.mark.asyncio
308
- async def test_deploy_disconnect(servicer, client):
309
- stub = Stub()
310
- stub.function(secrets=[modal.Secret.from_name("nonexistent-secret")])(square)
311
-
312
- with pytest.raises(NotFoundError):
313
- await deploy_stub.aio(stub, "my-app", client=client)
314
-
315
- assert servicer.app_state_history["ap-1"] == [
316
- api_pb2.APP_STATE_INITIALIZING,
317
- api_pb2.APP_STATE_STOPPED,
318
- ]
319
-
320
-
321
- def test_redeploy_from_name_change(servicer, client):
322
- # Deploy queue
323
- modal.Queue.lookup("foo-queue", create_if_missing=True, client=client)
324
-
325
- # Use it from stub
326
- stub = Stub()
327
- with pytest.warns(DeprecationError):
328
- stub.q = modal.Queue.from_name("foo-queue")
329
- deploy_stub(stub, "my-app", client=client)
330
-
331
- # Change the object id of foo-queue
332
- k = ("foo-queue", api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, config.get("environment"))
333
- assert servicer.deployed_queues[k]
334
- servicer.deployed_queues[k] = "qu-baz123"
335
-
336
- # Redeploy app
337
- # This should not fail because the object_id changed - it's a different app
338
- deploy_stub(stub, "my-app", client=client)
339
-
340
-
341
- def test_parse_custom_domains():
342
- assert len(_parse_custom_domains(None)) == 0
343
- assert len(_parse_custom_domains(["foo.com", "bar.com"])) == 2
344
- with pytest.raises(AssertionError):
345
- assert _parse_custom_domains("foo.com")
346
-
347
-
348
- def test_hydrated_other_app_object_gets_referenced(servicer, client):
349
- stub = Stub("my-stub")
350
- with servicer.intercept() as ctx:
351
- with modal.Volume.ephemeral(client=client) as vol:
352
- stub.function(volumes={"/vol": vol})(dummy) # implicitly load vol
353
- deploy_stub(stub, client=client)
354
- app_set_objects_req = ctx.pop_request("AppSetObjects")
355
- assert vol.object_id in app_set_objects_req.unindexed_object_ids
356
-
357
-
358
- def test_hasattr():
359
- stub = Stub()
360
- assert not hasattr(stub, "xyz")
test/test_asgi_wrapper.py DELETED
@@ -1,234 +0,0 @@
1
- # Copyright Modal Labs 2024
2
- import asyncio
3
- import pytest
4
-
5
- import fastapi
6
- from starlette.requests import ClientDisconnect
7
-
8
- from modal._asgi import asgi_app_wrapper
9
- from modal.functions import _set_current_context_ids
10
-
11
-
12
- class DummyException(Exception):
13
- pass
14
-
15
-
16
- app = fastapi.FastAPI()
17
-
18
-
19
- @app.get("/")
20
- def sync_index():
21
- return {"some_result": "foo"}
22
-
23
-
24
- @app.get("/error")
25
- def sync_error():
26
- raise DummyException()
27
-
28
-
29
- @app.post("/async_reading_body")
30
- async def async_index_reading_body(req: fastapi.Request):
31
- body = await req.body()
32
- return {"some_result": body}
33
-
34
-
35
- @app.get("/async_error")
36
- async def async_error():
37
- raise DummyException()
38
-
39
-
40
- @app.get("/streaming_response")
41
- async def streaming_response():
42
- from fastapi.responses import StreamingResponse
43
-
44
- async def stream_bytes():
45
- yield b"foo"
46
- yield b"bar"
47
-
48
- return StreamingResponse(stream_bytes())
49
-
50
-
51
- def _asgi_get_scope(path, method="GET"):
52
- return {
53
- "type": "http",
54
- "method": method,
55
- "path": path,
56
- "query_string": "",
57
- "headers": [],
58
- }
59
-
60
-
61
- class MockIOManager:
62
- class get_data_in:
63
- @staticmethod
64
- async def aio(_function_call_id):
65
- yield {"type": "http.request", "body": b"some_body"}
66
- await asyncio.sleep(10)
67
-
68
-
69
- @pytest.mark.asyncio
70
- @pytest.mark.timeout(1)
71
- async def test_success():
72
- mock_manager = MockIOManager()
73
- _set_current_context_ids("in-123", "fc-123")
74
- wrapped_app = asgi_app_wrapper(app, mock_manager)
75
- asgi_scope = _asgi_get_scope("/")
76
- outputs = [output async for output in wrapped_app(asgi_scope)]
77
- assert len(outputs) == 2
78
- before_body = outputs[0]
79
- assert before_body["status"] == 200
80
- assert before_body["type"] == "http.response.start"
81
- body = outputs[1]
82
- assert body["body"] == b'{"some_result":"foo"}'
83
- assert body["type"] == "http.response.body"
84
-
85
-
86
- @pytest.mark.asyncio
87
- @pytest.mark.parametrize("endpoint_url", ["/error", "/async_error"])
88
- @pytest.mark.timeout(1)
89
- async def test_endpoint_exception(endpoint_url):
90
- mock_manager = MockIOManager()
91
- _set_current_context_ids("in-123", "fc-123")
92
- wrapped_app = asgi_app_wrapper(app, mock_manager)
93
- asgi_scope = _asgi_get_scope(endpoint_url)
94
- outputs = []
95
-
96
- with pytest.raises(DummyException):
97
- async for output in wrapped_app(asgi_scope):
98
- outputs.append(output)
99
-
100
- assert len(outputs) == 2
101
- before_body = outputs[0]
102
- assert before_body["status"] == 500
103
- assert before_body["type"] == "http.response.start"
104
- body = outputs[1]
105
- assert body["body"] == b"Internal Server Error"
106
- assert body["type"] == "http.response.body"
107
-
108
-
109
- class BrokenIOManager:
110
- class get_data_in:
111
- @staticmethod
112
- async def aio(_function_call_id):
113
- raise DummyException("error while fetching data")
114
- yield # noqa (makes this a generator)
115
-
116
-
117
- @pytest.mark.asyncio
118
- @pytest.mark.timeout(1)
119
- async def test_broken_io_unused(caplog):
120
- # if IO channel breaks, but the endpoint doesn't actually use
121
- # any of the body data, it should be allowed to output its data
122
- # and not raise an exception - but print a warning since it's unexpected
123
- mock_manager = BrokenIOManager()
124
- _set_current_context_ids("in-123", "fc-123")
125
- wrapped_app = asgi_app_wrapper(app, mock_manager)
126
- asgi_scope = _asgi_get_scope("/")
127
- outputs = []
128
-
129
- async for output in wrapped_app(asgi_scope):
130
- outputs.append(output)
131
-
132
- assert len(outputs) == 2
133
- assert outputs[0]["status"] == 200
134
- assert outputs[1]["body"] == b'{"some_result":"foo"}'
135
- assert "Internal error" in caplog.text
136
- assert "DummyException: error while fetching data" in caplog.text
137
-
138
-
139
- @pytest.mark.asyncio
140
- @pytest.mark.timeout(10)
141
- async def test_broken_io_used():
142
- mock_manager = BrokenIOManager()
143
- _set_current_context_ids("in-123", "fc-123")
144
- wrapped_app = asgi_app_wrapper(app, mock_manager)
145
- asgi_scope = _asgi_get_scope("/async_reading_body", "POST")
146
- outputs = []
147
- with pytest.raises(ClientDisconnect):
148
- async for output in wrapped_app(asgi_scope):
149
- outputs.append(output)
150
-
151
- assert len(outputs) == 2
152
- assert outputs[0]["status"] == 500
153
-
154
-
155
- class SlowIOManager:
156
- class get_data_in:
157
- @staticmethod
158
- async def aio(_function_call_id):
159
- await asyncio.sleep(5)
160
- yield # makes this an async generator
161
-
162
-
163
- @pytest.mark.asyncio
164
- @pytest.mark.timeout(2)
165
- async def test_first_message_timeout(monkeypatch):
166
- monkeypatch.setattr("modal._asgi.FIRST_MESSAGE_TIMEOUT_SECONDS", 0.1) # simulate timeout
167
- _set_current_context_ids("in-123", "fc-123")
168
- wrapped_app = asgi_app_wrapper(app, SlowIOManager())
169
- asgi_scope = _asgi_get_scope("/async_reading_body", "POST")
170
- outputs = []
171
- with pytest.raises(ClientDisconnect):
172
- async for output in wrapped_app(asgi_scope):
173
- outputs.append(output)
174
-
175
- assert outputs[0]["status"] == 502
176
- assert b"Missing request" in outputs[1]["body"]
177
-
178
-
179
- @pytest.mark.asyncio
180
- async def test_cancellation_cleanup(caplog):
181
- # this test mostly exists to get some coverage on the cancellation/error paths and ensure nothing unexpected happens there
182
- _set_current_context_ids("in-123", "fc-123")
183
- wrapped_app = asgi_app_wrapper(app, SlowIOManager())
184
- asgi_scope = _asgi_get_scope("/async_reading_body", "POST")
185
- outputs = []
186
-
187
- async def app_runner():
188
- async for output in wrapped_app(asgi_scope):
189
- outputs.append(output)
190
-
191
- app_runner_task = asyncio.create_task(app_runner())
192
- await asyncio.sleep(0.1) # let it get started
193
- app_runner_task.cancel()
194
- await asyncio.sleep(0.1) # let it shut down
195
- assert len(outputs) == 0
196
- assert caplog.text == "" # make sure there are no junk traces about dangling tasks etc.
197
-
198
-
199
- @pytest.mark.asyncio
200
- async def test_streaming_response():
201
- _set_current_context_ids("in-123", "fc-123")
202
- wrapped_app = asgi_app_wrapper(app, SlowIOManager())
203
- asgi_scope = _asgi_get_scope("/streaming_response", "GET")
204
- outputs = []
205
- async for output in wrapped_app(asgi_scope):
206
- outputs.append(output)
207
- assert outputs == [
208
- {"headers": [], "status": 200, "type": "http.response.start"},
209
- {"body": b"foo", "more_body": True, "type": "http.response.body"},
210
- {"body": b"bar", "more_body": True, "type": "http.response.body"},
211
- {"body": b"", "more_body": False, "type": "http.response.body"},
212
- ]
213
-
214
-
215
- class StreamingIOManager:
216
- class get_data_in:
217
- @staticmethod
218
- async def aio(_function_call_id):
219
- yield {"type": "http.request", "body": b"foo", "more_body": True}
220
- yield {"type": "http.request", "body": b"bar", "more_body": True}
221
- yield {"type": "http.request", "body": b"baz", "more_body": False}
222
- yield {"type": "http.request", "body": b"this should not be read", "more_body": False}
223
-
224
-
225
- @pytest.mark.asyncio
226
- async def test_streaming_body():
227
- _set_current_context_ids("in-123", "fc-123")
228
-
229
- wrapped_app = asgi_app_wrapper(app, StreamingIOManager())
230
- asgi_scope = _asgi_get_scope("/async_reading_body", "POST")
231
- outputs = []
232
- async for output in wrapped_app(asgi_scope):
233
- outputs.append(output)
234
- assert outputs[1] == {"type": "http.response.body", "body": b'{"some_result":"foobarbaz"}'}
test/token_flow_test.py DELETED
@@ -1,18 +0,0 @@
1
- # Copyright Modal Labs 2023
2
- import pytest
3
-
4
- import aiohttp
5
-
6
- from modal.token_flow import TokenFlow
7
-
8
-
9
- @pytest.mark.asyncio
10
- async def test_token_flow_server(servicer, client):
11
- tf = TokenFlow(client)
12
- async with tf.start() as (token_flow_id, _, _):
13
- # Make a request against the local web server and make sure it validates
14
- localhost_url = f"http://localhost:{servicer.token_flow_localhost_port}"
15
- async with aiohttp.ClientSession() as session:
16
- async with session.get(localhost_url) as resp:
17
- text = await resp.text()
18
- assert text == token_flow_id