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/queue_test.py DELETED
@@ -1,97 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import pytest
3
- import queue
4
- import time
5
-
6
- from modal import Queue
7
-
8
- from .supports.skip import skip_macos, skip_windows
9
-
10
-
11
- def test_queue(servicer, client):
12
- q = Queue.lookup("some-random-queue", create_if_missing=True, client=client)
13
- assert isinstance(q, Queue)
14
- assert q.len() == 0
15
- q.put(42)
16
- assert q.len() == 1
17
- assert q.get() == 42
18
- with pytest.raises(queue.Empty):
19
- q.get(timeout=0)
20
- assert q.len() == 0
21
-
22
-
23
- def test_queue_ephemeral(servicer, client):
24
- with Queue.ephemeral(client=client, _heartbeat_sleep=1) as q:
25
- q.put("hello")
26
- assert q.len() == 1
27
- assert q.get() == "hello"
28
- time.sleep(1.5) # enough to trigger two heartbeats
29
-
30
- assert servicer.n_queue_heartbeats == 2
31
-
32
-
33
- @skip_macos("TODO(erikbern): this consistently fails on OSX. Unclear why.")
34
- @skip_windows("TODO(Jonathon): figure out why timeouts don't occur on Windows.")
35
- @pytest.mark.parametrize(
36
- ["put_timeout_secs", "min_queue_full_exc_count", "max_queue_full_exc_count"],
37
- [
38
- (0.02, 1, 100), # a low timeout causes some exceptions
39
- (10.0, 0, 0), # a high timeout causes zero exceptions
40
- (0.00, 1, 100), # zero-len timeout causes some exceptions
41
- (None, 0, 0), # no timeout causes zero exceptions
42
- ],
43
- )
44
- def test_queue_blocking_put(put_timeout_secs, min_queue_full_exc_count, max_queue_full_exc_count, servicer, client):
45
- import queue
46
- import threading
47
-
48
- producer_delay = 0.001
49
- consumer_delay = producer_delay * 5
50
-
51
- queue_full_exceptions = 0
52
- with Queue.ephemeral(client=client) as q:
53
-
54
- def producer():
55
- nonlocal queue_full_exceptions
56
- for i in range(servicer.queue_max_len * 2):
57
- item = f"Item {i}"
58
- try:
59
- q.put(item, block=True, timeout=put_timeout_secs) # type: ignore
60
- except queue.Full:
61
- queue_full_exceptions += 1
62
- time.sleep(producer_delay)
63
-
64
- def consumer():
65
- while True:
66
- time.sleep(consumer_delay)
67
- item = q.get(block=True) # type: ignore
68
- if item is None:
69
- break # Exit if a None item is received
70
-
71
- producer_thread = threading.Thread(target=producer)
72
- consumer_thread = threading.Thread(target=consumer)
73
- producer_thread.start()
74
- consumer_thread.start()
75
- producer_thread.join()
76
- # Stop the consumer by sending a None item
77
- q.put(None) # type: ignore
78
- consumer_thread.join()
79
-
80
- assert queue_full_exceptions >= min_queue_full_exc_count
81
- assert queue_full_exceptions <= max_queue_full_exc_count
82
-
83
-
84
- def test_queue_nonblocking_put(servicer, client):
85
- with Queue.ephemeral(client=client) as q:
86
- # Non-blocking PUTs don't tolerate a full queue and will raise exception.
87
- with pytest.raises(queue.Full) as excinfo:
88
- for i in range(servicer.queue_max_len + 1):
89
- q.put(i, block=False) # type: ignore
90
-
91
- assert str(servicer.queue_max_len) in str(excinfo.value)
92
- assert i == servicer.queue_max_len
93
-
94
-
95
- def test_queue_deploy(servicer, client):
96
- d = Queue.lookup("xyz", create_if_missing=True, client=client)
97
- d.put(123)
test/resolver_test.py DELETED
@@ -1,58 +0,0 @@
1
- # Copyright Modal Labs 2023
2
- import asyncio
3
- import pytest
4
- import time
5
- from typing import Optional
6
-
7
- from modal._output import OutputManager
8
- from modal._resolver import Resolver
9
- from modal.object import _Object
10
-
11
-
12
- @pytest.mark.asyncio
13
- async def test_multi_resolve_sequential_loads_once():
14
- output_manager = OutputManager(None, show_progress=False)
15
- resolver = Resolver(None, output_mgr=output_manager, environment_name="", app_id=None)
16
-
17
- load_count = 0
18
-
19
- class _DumbObject(_Object, type_prefix="zz"):
20
- pass
21
-
22
- async def _load(self: _DumbObject, resolver: Resolver, existing_object_id: Optional[str]):
23
- nonlocal load_count
24
- load_count += 1
25
- self._hydrate("zz-123", resolver.client, None)
26
- await asyncio.sleep(0.1)
27
-
28
- obj = _DumbObject._from_loader(_load, "DumbObject()")
29
-
30
- t0 = time.monotonic()
31
- await resolver.load(obj)
32
- await resolver.load(obj)
33
- assert 0.08 < time.monotonic() - t0 < 0.15
34
-
35
- assert load_count == 1
36
-
37
-
38
- @pytest.mark.asyncio
39
- async def test_multi_resolve_concurrent_loads_once():
40
- output_manager = OutputManager(None, show_progress=False)
41
- resolver = Resolver(None, output_mgr=output_manager, environment_name="", app_id=None)
42
-
43
- load_count = 0
44
-
45
- class _DumbObject(_Object, type_prefix="zz"):
46
- pass
47
-
48
- async def _load(self: _DumbObject, resolver: Resolver, existing_object_id: Optional[str]):
49
- nonlocal load_count
50
- load_count += 1
51
- self._hydrate("zz-123", resolver.client, None)
52
- await asyncio.sleep(0.1)
53
-
54
- obj = _DumbObject._from_loader(_load, "DumbObject()")
55
- t0 = time.monotonic()
56
- await asyncio.gather(resolver.load(obj), resolver.load(obj))
57
- assert 0.08 < time.monotonic() - t0 < 0.17
58
- assert load_count == 1
test/retries_test.py DELETED
@@ -1,67 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import pytest
3
-
4
- import modal
5
- from modal.exception import InvalidError
6
-
7
-
8
- def default_retries_from_int():
9
- pass
10
-
11
-
12
- def fixed_delay_retries():
13
- pass
14
-
15
-
16
- def exponential_backoff():
17
- return 67
18
-
19
-
20
- def exponential_with_max_delay():
21
- return 67
22
-
23
-
24
- def dummy():
25
- pass
26
-
27
-
28
- def zero_retries():
29
- pass
30
-
31
-
32
- def test_retries(client):
33
- stub = modal.Stub()
34
-
35
- default_retries_from_int_modal = stub.function(retries=5)(default_retries_from_int)
36
- fixed_delay_retries_modal = stub.function(retries=modal.Retries(max_retries=5, backoff_coefficient=1.0))(
37
- fixed_delay_retries
38
- )
39
-
40
- exponential_backoff_modal = stub.function(
41
- retries=modal.Retries(max_retries=2, initial_delay=2.0, backoff_coefficient=2.0)
42
- )(exponential_backoff)
43
-
44
- exponential_with_max_delay_modal = stub.function(
45
- retries=modal.Retries(max_retries=2, backoff_coefficient=2.0, max_delay=30.0)
46
- )(exponential_with_max_delay)
47
-
48
- zero_retries_modal = stub.function(retries=0)(zero_retries)
49
-
50
- with pytest.raises(TypeError):
51
- # Reject no-args constructions, which is unreadable and harder to support long-term
52
- stub.function(retries=modal.Retries())(dummy) # type: ignore
53
-
54
- # Reject weird inputs:
55
- # Don't need server to detect and reject nonsensical input. Can do client-side.
56
- with pytest.raises(InvalidError):
57
- stub.function(retries=modal.Retries(max_retries=-2))(dummy)
58
-
59
- with pytest.raises(InvalidError):
60
- stub.function(retries=modal.Retries(max_retries=2, backoff_coefficient=0.0))(dummy)
61
-
62
- with stub.run(client=client):
63
- default_retries_from_int_modal.remote()
64
- fixed_delay_retries_modal.remote()
65
- exponential_backoff_modal.remote()
66
- exponential_with_max_delay_modal.remote()
67
- zero_retries_modal.remote()
test/runner_test.py DELETED
@@ -1,85 +0,0 @@
1
- # Copyright Modal Labs 2023
2
- import pytest
3
- import typing
4
-
5
- import modal
6
- from modal.client import Client
7
- from modal.exception import ExecutionError
8
- from modal.runner import run_stub
9
- from modal_proto import api_pb2
10
-
11
- T = typing.TypeVar("T")
12
-
13
-
14
- def test_run_stub(servicer, client):
15
- dummy_stub = modal.Stub()
16
- with servicer.intercept() as ctx:
17
- with run_stub(dummy_stub, client=client):
18
- pass
19
-
20
- ctx.pop_request("AppCreate")
21
- ctx.pop_request("AppSetObjects")
22
- ctx.pop_request("AppClientDisconnect")
23
-
24
-
25
- def test_run_stub_unauthenticated(servicer):
26
- dummy_stub = modal.Stub()
27
- with Client.anonymous(servicer.remote_addr) as client:
28
- with pytest.raises(ExecutionError, match=".+unauthenticated client"):
29
- with run_stub(dummy_stub, client=client):
30
- pass
31
-
32
-
33
- def dummy():
34
- ...
35
-
36
-
37
- def test_run_stub_profile_env_with_refs(servicer, client, monkeypatch):
38
- monkeypatch.setenv("MODAL_ENVIRONMENT", "profile_env")
39
- with servicer.intercept() as ctx:
40
- dummy_stub = modal.Stub()
41
- ref = modal.Secret.from_name("some_secret")
42
- dummy_stub.function(secrets=[ref])(dummy)
43
-
44
- assert ctx.calls == [] # all calls should be deferred
45
-
46
- with servicer.intercept() as ctx:
47
- ctx.add_response("SecretGetOrCreate", api_pb2.SecretGetOrCreateResponse(secret_id="st-123"))
48
- with run_stub(dummy_stub, client=client):
49
- pass
50
-
51
- with pytest.raises(Exception):
52
- ctx.pop_request("SecretCreate") # should not create a new secret...
53
-
54
- app_create = ctx.pop_request("AppCreate")
55
- assert app_create.environment_name == "profile_env"
56
-
57
- secret_get_or_create = ctx.pop_request("SecretGetOrCreate")
58
- assert secret_get_or_create.environment_name == "profile_env"
59
-
60
-
61
- def test_run_stub_custom_env_with_refs(servicer, client, monkeypatch):
62
- monkeypatch.setenv("MODAL_ENVIRONMENT", "profile_env")
63
- dummy_stub = modal.Stub()
64
- own_env_secret = modal.Secret.from_name("own_env_secret")
65
- other_env_secret = modal.Secret.from_name("other_env_secret", environment_name="third") # explicit lookup
66
-
67
- dummy_stub.function(secrets=[own_env_secret, other_env_secret])(dummy)
68
-
69
- with servicer.intercept() as ctx:
70
- ctx.add_response("SecretGetOrCreate", api_pb2.SecretGetOrCreateResponse(secret_id="st-123"))
71
- ctx.add_response("SecretGetOrCreate", api_pb2.SecretGetOrCreateResponse(secret_id="st-456"))
72
- with run_stub(dummy_stub, client=client, environment_name="custom"):
73
- pass
74
-
75
- with pytest.raises(Exception):
76
- ctx.pop_request("SecretCreate")
77
-
78
- app_create = ctx.pop_request("AppCreate")
79
- assert app_create.environment_name == "custom"
80
-
81
- secret_get_or_create = ctx.pop_request("SecretGetOrCreate")
82
- assert secret_get_or_create.environment_name == "custom"
83
-
84
- secret_get_or_create_2 = ctx.pop_request("SecretGetOrCreate")
85
- assert secret_get_or_create_2.environment_name == "third"
test/sandbox_test.py DELETED
@@ -1,191 +0,0 @@
1
- # Copyright Modal Labs 2022
2
-
3
- import hashlib
4
- import platform
5
- import pytest
6
- import time
7
- from pathlib import Path
8
-
9
- from modal import Image, Mount, NetworkFileSystem, Sandbox, Secret, Stub
10
- from modal.exception import InvalidError
11
-
12
- stub = Stub()
13
-
14
-
15
- skip_non_linux = pytest.mark.skipif(platform.system() != "Linux", reason="sandbox mock uses subprocess")
16
-
17
-
18
- @skip_non_linux
19
- def test_spawn_sandbox(client, servicer):
20
- with stub.run(client=client):
21
- sb = stub.spawn_sandbox("bash", "-c", "echo bye >&2 && sleep 1 && echo hi && exit 42", timeout=600)
22
-
23
- assert sb.poll() is None
24
-
25
- t0 = time.time()
26
- sb.wait()
27
- # Test that we actually waited for the sandbox to finish.
28
- assert time.time() - t0 > 0.3
29
-
30
- assert sb.stdout.read() == "hi\n"
31
- assert sb.stderr.read() == "bye\n"
32
- # read a second time
33
- assert sb.stdout.read() == ""
34
- assert sb.stderr.read() == ""
35
-
36
- assert sb.returncode == 42
37
- assert sb.poll() == 42
38
-
39
-
40
- @skip_non_linux
41
- def test_sandbox_mount(client, servicer, tmpdir):
42
- tmpdir.join("a.py").write(b"foo")
43
-
44
- with stub.run(client=client):
45
- sb = stub.spawn_sandbox(
46
- "echo",
47
- "hi",
48
- mounts=[Mount.from_local_dir(Path(tmpdir), remote_path="/m")],
49
- )
50
- sb.wait()
51
-
52
- sha = hashlib.sha256(b"foo").hexdigest()
53
- assert servicer.files_sha2data[sha]["data"] == b"foo"
54
-
55
-
56
- @skip_non_linux
57
- def test_sandbox_image(client, servicer, tmpdir):
58
- tmpdir.join("a.py").write(b"foo")
59
-
60
- with stub.run(client=client):
61
- sb = stub.spawn_sandbox("echo", "hi", image=Image.debian_slim().pip_install("foo", "bar", "potato"))
62
- sb.wait()
63
-
64
- idx = max(servicer.images.keys())
65
- last_image = servicer.images[idx]
66
-
67
- assert all(c in last_image.dockerfile_commands[-1] for c in ["foo", "bar", "potato"])
68
-
69
-
70
- @skip_non_linux
71
- def test_sandbox_secret(client, servicer, tmpdir):
72
- with stub.run(client=client):
73
- sb = stub.spawn_sandbox("echo", "$FOO", secrets=[Secret.from_dict({"FOO": "BAR"})])
74
- sb.wait()
75
-
76
- assert len(servicer.sandbox_defs[0].secret_ids) == 1
77
-
78
-
79
- @skip_non_linux
80
- def test_sandbox_nfs(client, servicer, tmpdir):
81
- with stub.run(client=client):
82
- with NetworkFileSystem.ephemeral(client=client) as nfs:
83
- with pytest.raises(InvalidError):
84
- stub.spawn_sandbox("echo", "foo > /cache/a.txt", network_file_systems={"/": nfs})
85
-
86
- stub.spawn_sandbox("echo", "foo > /cache/a.txt", network_file_systems={"/cache": nfs})
87
-
88
- assert len(servicer.sandbox_defs[0].nfs_mounts) == 1
89
-
90
-
91
- @skip_non_linux
92
- def test_sandbox_from_id(client, servicer):
93
- with stub.run(client=client):
94
- sb = stub.spawn_sandbox("bash", "-c", "echo foo && exit 42", timeout=600)
95
- sb.wait()
96
-
97
- sb2 = Sandbox.from_id(sb.object_id, client=client)
98
- assert sb2.stdout.read() == "foo\n"
99
- assert sb2.returncode == 42
100
-
101
-
102
- @skip_non_linux
103
- def test_sandbox_terminate(client, servicer):
104
- with stub.run(client=client):
105
- sb = stub.spawn_sandbox("bash", "-c", "sleep 10000")
106
- sb.terminate()
107
-
108
- assert sb.returncode != 0
109
-
110
-
111
- @skip_non_linux
112
- @pytest.mark.asyncio
113
- async def test_sandbox_stdin_async(client, servicer):
114
- async with stub.run.aio(client=client):
115
- sb = stub.spawn_sandbox("bash", "-c", "while read line; do echo $line; done && exit 13")
116
-
117
- sb.stdin.write(b"foo\n")
118
- sb.stdin.write(b"bar\n")
119
-
120
- sb.stdin.write_eof()
121
-
122
- await sb.stdin.drain.aio()
123
-
124
- sb.wait()
125
-
126
- assert sb.stdout.read() == "foo\nbar\n"
127
- assert sb.returncode == 13
128
-
129
-
130
- @skip_non_linux
131
- def test_sandbox_stdin(client, servicer):
132
- with stub.run(client=client):
133
- sb = stub.spawn_sandbox("bash", "-c", "while read line; do echo $line; done && exit 13")
134
-
135
- sb.stdin.write(b"foo\n")
136
- sb.stdin.write(b"bar\n")
137
-
138
- sb.stdin.write_eof()
139
-
140
- sb.stdin.drain()
141
-
142
- sb.wait()
143
-
144
- assert sb.stdout.read() == "foo\nbar\n"
145
- assert sb.returncode == 13
146
-
147
-
148
- @skip_non_linux
149
- def test_sandbox_stdin_invalid_write(client, servicer):
150
- with stub.run(client=client):
151
- sb = stub.spawn_sandbox("bash", "-c", "echo foo")
152
- with pytest.raises(TypeError):
153
- sb.stdin.write("foo\n") # type: ignore
154
-
155
-
156
- @skip_non_linux
157
- def test_sandbox_stdin_write_after_eof(client, servicer):
158
- with stub.run(client=client):
159
- sb = stub.spawn_sandbox("bash", "-c", "echo foo")
160
- sb.stdin.write_eof()
161
- with pytest.raises(EOFError):
162
- sb.stdin.write(b"foo")
163
-
164
-
165
- @skip_non_linux
166
- @pytest.mark.asyncio
167
- async def test_sandbox_async_for(client, servicer):
168
- async with stub.run.aio(client=client):
169
- sb = stub.spawn_sandbox("bash", "-c", "echo hello && echo world && echo bye >&2")
170
-
171
- out = ""
172
-
173
- async for message in sb.stdout:
174
- out += message
175
- assert out == "hello\nworld\n"
176
-
177
- # test streaming stdout a second time
178
- out2 = ""
179
- async for message in sb.stdout:
180
- out2 += message
181
- assert out2 == ""
182
-
183
- err = ""
184
- async for message in sb.stderr:
185
- err += message
186
-
187
- assert err == "bye\n"
188
-
189
- # test reading after receiving EOF
190
- assert sb.stdout.read() == ""
191
- assert sb.stderr.read() == ""
test/schedule_test.py DELETED
@@ -1,15 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- from modal import Period, Stub
3
- from modal_proto import api_pb2
4
-
5
- stub = Stub()
6
-
7
-
8
- @stub.function(schedule=Period(seconds=5))
9
- def f():
10
- pass
11
-
12
-
13
- def test_schedule(servicer, client):
14
- with stub.run(client=client):
15
- assert servicer.function2schedule == {"fu-1": api_pb2.Schedule(period=api_pb2.Schedule.Period(seconds=5.0))}
@@ -1,29 +0,0 @@
1
- # Copyright Modal Labs 2024
2
- from modal import SchedulerPlacement, Stub
3
- from modal_proto import api_pb2
4
-
5
- stub = Stub()
6
-
7
-
8
- @stub.function(
9
- _experimental_scheduler=True,
10
- _experimental_scheduler_placement=SchedulerPlacement(
11
- region="us-east-1",
12
- zone="us-east-1a",
13
- spot=False,
14
- ),
15
- )
16
- def f():
17
- pass
18
-
19
-
20
- def test_scheduler_placement(servicer, client):
21
- with stub.run(client=client):
22
- assert len(servicer.app_functions) == 1
23
- fn = servicer.app_functions["fu-1"]
24
- assert fn._experimental_scheduler
25
- assert fn._experimental_scheduler_placement == api_pb2.SchedulerPlacement(
26
- _region="us-east-1",
27
- _zone="us-east-1a",
28
- _lifecycle="on-demand",
29
- )
test/secret_test.py DELETED
@@ -1,78 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import os
3
- import pytest
4
- import tempfile
5
- from unittest import mock
6
-
7
- from modal import Secret, Stub
8
- from modal.exception import InvalidError
9
-
10
- from .supports.skip import skip_old_py
11
-
12
-
13
- def dummy():
14
- ...
15
-
16
-
17
- def test_secret_from_dict(servicer, client):
18
- stub = Stub()
19
- secret = Secret.from_dict({"FOO": "hello, world"})
20
- stub.function(secrets=[secret])(dummy)
21
- with stub.run(client=client):
22
- assert secret.object_id == "st-0"
23
- assert servicer.secrets["st-0"] == {"FOO": "hello, world"}
24
-
25
-
26
- @skip_old_py("python-dotenv requires python3.8 or higher", (3, 8))
27
- def test_secret_from_dotenv(servicer, client):
28
- with tempfile.TemporaryDirectory() as tmpdirname:
29
- with open(os.path.join(tmpdirname, ".env"), "w") as f:
30
- f.write("# My settings\nUSER=user\nPASSWORD=abc123\n")
31
- stub = Stub()
32
- secret = Secret.from_dotenv(tmpdirname)
33
- stub.function(secrets=[secret])(dummy)
34
- with stub.run(client=client):
35
- assert secret.object_id == "st-0"
36
- assert servicer.secrets["st-0"] == {"USER": "user", "PASSWORD": "abc123"}
37
-
38
- @mock.patch.dict(os.environ, {"FOO": "easy", "BAR": "1234"})
39
- def test_secret_from_local_environ(servicer, client):
40
- stub = Stub()
41
- secret = Secret.from_local_environ(["FOO", "BAR"])
42
- stub.function(secrets=[secret])(dummy)
43
- with stub.run(client=client):
44
- assert secret.object_id == "st-0"
45
- assert servicer.secrets["st-0"] == {"FOO": "easy", "BAR": "1234"}
46
-
47
- with pytest.raises(InvalidError, match="NOTFOUND"):
48
- Secret.from_local_environ(["FOO", "NOTFOUND"])
49
-
50
-
51
-
52
- def test_init_types():
53
- with pytest.raises(InvalidError):
54
- Secret.from_dict({"foo": 1.0}) # type: ignore
55
-
56
-
57
- def test_secret_from_dict_none(servicer, client):
58
- stub = Stub()
59
- secret = Secret.from_dict({"FOO": os.getenv("xyz"), "BAR": os.environ.get("abc"), "BAZ": "baz"})
60
- stub.function(secrets=[secret])(dummy)
61
- with stub.run(client=client):
62
- assert servicer.secrets["st-0"] == {"BAZ": "baz"}
63
-
64
-
65
- def test_secret_from_name(servicer, client):
66
- # Deploy secret
67
- secret_id = Secret.create_deployed("my-secret", {"FOO": "123"}, client=client)
68
-
69
- # Look up secret
70
- secret = Secret.lookup("my-secret", client=client)
71
- assert secret.object_id == secret_id
72
-
73
- # Look up secret through app
74
- stub = Stub()
75
- secret = Secret.from_name("my-secret")
76
- stub.function(secrets=[secret])(dummy)
77
- with stub.run(client=client):
78
- assert secret.object_id == secret_id
@@ -1,42 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import pytest
3
- import random
4
-
5
- from modal import Queue
6
- from modal._serialization import deserialize, deserialize_data_format, serialize, serialize_data_format
7
- from modal._utils.rand_pb_testing import rand_pb
8
- from modal_proto import api_pb2
9
-
10
- from .supports.skip import skip_old_py
11
-
12
-
13
- @pytest.mark.asyncio
14
- async def test_roundtrip(servicer, client):
15
- async with Queue.ephemeral(client=client) as q:
16
- data = serialize(q)
17
- # TODO: strip synchronizer reference from synchronicity entities!
18
- assert len(data) < 350 # Used to be 93...
19
- # Note: if this blows up significantly, it's most likely because
20
- # cloudpickle can't find a class in the global scope. When this
21
- # happens, it tries to serialize the entire class along with the
22
- # object. The reason it doesn't find the class in the global scope
23
- # is most likely because the name doesn't match. To fix this, make
24
- # sure that cls.__name__ (which is something synchronicity sets)
25
- # is the same as the symbol defined in the global scope.
26
- q_roundtrip = deserialize(data, client)
27
- assert isinstance(q_roundtrip, Queue)
28
- assert q.object_id == q_roundtrip.object_id
29
-
30
-
31
- @skip_old_py("random.randbytes() was introduced in python 3.9", (3, 9))
32
- @pytest.mark.asyncio
33
- async def test_asgi_roundtrip():
34
- rand = random.Random(42)
35
- for _ in range(10000):
36
- msg = rand_pb(api_pb2.Asgi, rand)
37
- buf = msg.SerializeToString()
38
- asgi_obj = deserialize_data_format(buf, api_pb2.DATA_FORMAT_ASGI, None)
39
- assert asgi_obj is None or (isinstance(asgi_obj, dict) and asgi_obj["type"])
40
- buf = serialize_data_format(asgi_obj, api_pb2.DATA_FORMAT_ASGI)
41
- asgi_obj_roundtrip = deserialize_data_format(buf, api_pb2.DATA_FORMAT_ASGI, None)
42
- assert asgi_obj == asgi_obj_roundtrip
@@ -1,10 +0,0 @@
1
- # Copyright Modal Labs 2024
2
- from test.helpers import deploy_stub_externally
3
-
4
-
5
- def test_stub_composition_includes_all_functions(servicer, supports_dir, monkeypatch, client):
6
- print(deploy_stub_externally(servicer, "main.py", cwd=supports_dir / "multifile_project"))
7
- assert servicer.n_functions == 3
8
- assert {"/root/main.py", "/root/a.py", "/root/b.py", "/root/c.py"} == set(servicer.files_name2sha.keys())
9
- assert len(servicer.secrets) == 1 # secret from B should be included
10
- assert servicer.n_mounts == 4 # mounts should not be duplicated