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
test/queue_test.py DELETED
@@ -1,115 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import pytest
3
- import queue
4
- import time
5
-
6
- from modal import Queue
7
- from modal.exception import NotFoundError
8
-
9
- from .supports.skip import skip_macos, skip_windows
10
-
11
-
12
- def test_queue(servicer, client):
13
- q = Queue.lookup("some-random-queue", create_if_missing=True, client=client)
14
- assert isinstance(q, Queue)
15
- assert q.len() == 0
16
- q.put(42)
17
- assert q.len() == 1
18
- assert q.get() == 42
19
- with pytest.raises(queue.Empty):
20
- q.get(timeout=0)
21
- assert q.len() == 0
22
-
23
- # test iter
24
- q.put_many([1, 2, 3])
25
- t0 = time.time()
26
- assert [v for v in q.iterate(item_poll_timeout=1.0)] == [1, 2, 3]
27
- assert 1.0 < time.time() - t0 < 2.0
28
- assert [v for v in q.iterate(item_poll_timeout=0.0)] == [1, 2, 3]
29
-
30
- Queue.delete("some-random-queue", client=client)
31
- with pytest.raises(NotFoundError):
32
- Queue.lookup("some-random-queue", client=client)
33
-
34
-
35
- def test_queue_ephemeral(servicer, client):
36
- with Queue.ephemeral(client=client, _heartbeat_sleep=1) as q:
37
- q.put("hello")
38
- assert q.len() == 1
39
- assert q.get() == "hello"
40
- time.sleep(1.5) # enough to trigger two heartbeats
41
-
42
- assert servicer.n_queue_heartbeats == 2
43
-
44
-
45
- @skip_macos("TODO(erikbern): this consistently fails on OSX. Unclear why.")
46
- @skip_windows("TODO(Jonathon): figure out why timeouts don't occur on Windows.")
47
- @pytest.mark.parametrize(
48
- ["put_timeout_secs", "min_queue_full_exc_count", "max_queue_full_exc_count"],
49
- [
50
- (0.02, 1, 100), # a low timeout causes some exceptions
51
- (10.0, 0, 0), # a high timeout causes zero exceptions
52
- (0.00, 1, 100), # zero-len timeout causes some exceptions
53
- (None, 0, 0), # no timeout causes zero exceptions
54
- ],
55
- )
56
- def test_queue_blocking_put(put_timeout_secs, min_queue_full_exc_count, max_queue_full_exc_count, servicer, client):
57
- import queue
58
- import threading
59
-
60
- producer_delay = 0.001
61
- consumer_delay = producer_delay * 5
62
-
63
- queue_full_exceptions = 0
64
- with Queue.ephemeral(client=client) as q:
65
-
66
- def producer():
67
- nonlocal queue_full_exceptions
68
- for i in range(servicer.queue_max_len * 2):
69
- item = f"Item {i}"
70
- try:
71
- q.put(item, block=True, timeout=put_timeout_secs) # type: ignore
72
- except queue.Full:
73
- queue_full_exceptions += 1
74
- time.sleep(producer_delay)
75
-
76
- def consumer():
77
- while True:
78
- time.sleep(consumer_delay)
79
- item = q.get(block=True) # type: ignore
80
- if item is None:
81
- break # Exit if a None item is received
82
-
83
- producer_thread = threading.Thread(target=producer)
84
- consumer_thread = threading.Thread(target=consumer)
85
- producer_thread.start()
86
- consumer_thread.start()
87
- producer_thread.join()
88
- # Stop the consumer by sending a None item
89
- q.put(None) # type: ignore
90
- consumer_thread.join()
91
-
92
- assert queue_full_exceptions >= min_queue_full_exc_count
93
- assert queue_full_exceptions <= max_queue_full_exc_count
94
-
95
-
96
- def test_queue_nonblocking_put(servicer, client):
97
- with Queue.ephemeral(client=client) as q:
98
- # Non-blocking PUTs don't tolerate a full queue and will raise exception.
99
- with pytest.raises(queue.Full) as excinfo:
100
- for i in range(servicer.queue_max_len + 1):
101
- q.put(i, block=False) # type: ignore
102
-
103
- assert str(servicer.queue_max_len) in str(excinfo.value)
104
- assert i == servicer.queue_max_len
105
-
106
-
107
- def test_queue_deploy(servicer, client):
108
- d = Queue.lookup("xyz", create_if_missing=True, client=client)
109
- d.put(123)
110
-
111
-
112
- def test_queue_lazy_hydrate_from_name(set_env_client):
113
- q = Queue.from_name("foo", create_if_missing=True)
114
- q.put(123)
115
- assert q.get() == 123
test/resolver_test.py DELETED
@@ -1,59 +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.flaky(max_runs=2)
13
- @pytest.mark.asyncio
14
- async def test_multi_resolve_sequential_loads_once():
15
- output_manager = OutputManager(None, show_progress=False)
16
- resolver = Resolver(None, output_mgr=output_manager, environment_name="", app_id=None)
17
-
18
- load_count = 0
19
-
20
- class _DumbObject(_Object, type_prefix="zz"):
21
- pass
22
-
23
- async def _load(self: _DumbObject, resolver: Resolver, existing_object_id: Optional[str]):
24
- nonlocal load_count
25
- load_count += 1
26
- self._hydrate("zz-123", resolver.client, None)
27
- await asyncio.sleep(0.1)
28
-
29
- obj = _DumbObject._from_loader(_load, "DumbObject()")
30
-
31
- t0 = time.monotonic()
32
- await resolver.load(obj)
33
- await resolver.load(obj)
34
- assert 0.08 < time.monotonic() - t0 < 0.15
35
-
36
- assert load_count == 1
37
-
38
-
39
- @pytest.mark.asyncio
40
- async def test_multi_resolve_concurrent_loads_once():
41
- output_manager = OutputManager(None, show_progress=False)
42
- resolver = Resolver(None, output_mgr=output_manager, environment_name="", app_id=None)
43
-
44
- load_count = 0
45
-
46
- class _DumbObject(_Object, type_prefix="zz"):
47
- pass
48
-
49
- async def _load(self: _DumbObject, resolver: Resolver, existing_object_id: Optional[str]):
50
- nonlocal load_count
51
- load_count += 1
52
- self._hydrate("zz-123", resolver.client, None)
53
- await asyncio.sleep(0.1)
54
-
55
- obj = _DumbObject._from_loader(_load, "DumbObject()")
56
- t0 = time.monotonic()
57
- await asyncio.gather(resolver.load(obj), resolver.load(obj))
58
- assert 0.08 < time.monotonic() - t0 < 0.17
59
- 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
- app = modal.App()
34
-
35
- default_retries_from_int_modal = app.function(retries=5)(default_retries_from_int)
36
- fixed_delay_retries_modal = app.function(retries=modal.Retries(max_retries=5, backoff_coefficient=1.0))(
37
- fixed_delay_retries
38
- )
39
-
40
- exponential_backoff_modal = app.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 = app.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 = app.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
- app.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
- app.function(retries=modal.Retries(max_retries=-2))(dummy)
58
-
59
- with pytest.raises(InvalidError):
60
- app.function(retries=modal.Retries(max_retries=2, backoff_coefficient=0.0))(dummy)
61
-
62
- with app.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_app
9
- from modal_proto import api_pb2
10
-
11
- T = typing.TypeVar("T")
12
-
13
-
14
- def test_run_app(servicer, client):
15
- dummy_app = modal.App()
16
- with servicer.intercept() as ctx:
17
- with run_app(dummy_app, 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_app_unauthenticated(servicer):
26
- dummy_app = modal.App()
27
- with Client.anonymous(servicer.remote_addr) as client:
28
- with pytest.raises(ExecutionError, match=".+unauthenticated client"):
29
- with run_app(dummy_app, client=client):
30
- pass
31
-
32
-
33
- def dummy():
34
- ...
35
-
36
-
37
- def test_run_app_profile_env_with_refs(servicer, client, monkeypatch):
38
- monkeypatch.setenv("MODAL_ENVIRONMENT", "profile_env")
39
- with servicer.intercept() as ctx:
40
- dummy_app = modal.App()
41
- ref = modal.Secret.from_name("some_secret")
42
- dummy_app.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_app(dummy_app, 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_app_custom_env_with_refs(servicer, client, monkeypatch):
62
- monkeypatch.setenv("MODAL_ENVIRONMENT", "profile_env")
63
- dummy_app = modal.App()
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_app.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_app(dummy_app, 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 App, Image, Mount, NetworkFileSystem, Sandbox, Secret
10
- from modal.exception import InvalidError
11
-
12
- app = App()
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 app.run(client=client):
21
- sb = app.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 app.run(client=client):
45
- sb = app.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 app.run(client=client):
61
- sb = app.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 app.run(client=client):
73
- sb = app.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 app.run(client=client):
82
- with NetworkFileSystem.ephemeral(client=client) as nfs:
83
- with pytest.raises(InvalidError):
84
- app.spawn_sandbox("echo", "foo > /cache/a.txt", network_file_systems={"/": nfs})
85
-
86
- app.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 app.run(client=client):
94
- sb = app.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 app.run(client=client):
105
- sb = app.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 app.run.aio(client=client):
115
- sb = app.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 app.run(client=client):
133
- sb = app.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 app.run(client=client):
151
- sb = app.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 app.run(client=client):
159
- sb = app.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 app.run.aio(client=client):
169
- sb = app.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 App, Period
3
- from modal_proto import api_pb2
4
-
5
- app = App()
6
-
7
-
8
- @app.function(schedule=Period(seconds=5))
9
- def f():
10
- pass
11
-
12
-
13
- def test_schedule(servicer, client):
14
- with app.run(client=client):
15
- assert servicer.function2schedule == {"fu-1": api_pb2.Schedule(period=api_pb2.Schedule.Period(seconds=5.0))}
@@ -1,57 +0,0 @@
1
- # Copyright Modal Labs 2024
2
- from modal import App, SchedulerPlacement
3
- from modal_proto import api_pb2
4
-
5
- from .sandbox_test import skip_non_linux
6
-
7
- app = App()
8
-
9
-
10
- @app.function(
11
- _experimental_scheduler=True,
12
- _experimental_scheduler_placement=SchedulerPlacement(
13
- region="us-east-1",
14
- zone="us-east-1a",
15
- spot=False,
16
- ),
17
- )
18
- def f():
19
- pass
20
-
21
-
22
- def test_fn_scheduler_placement(servicer, client):
23
- with app.run(client=client):
24
- assert len(servicer.app_functions) == 1
25
- fn = servicer.app_functions["fu-1"]
26
- assert fn._experimental_scheduler
27
- assert fn._experimental_scheduler_placement == api_pb2.SchedulerPlacement(
28
- _region="us-east-1",
29
- _zone="us-east-1a",
30
- _lifecycle="on-demand",
31
- )
32
-
33
-
34
- @skip_non_linux
35
- def test_sandbox_scheduler_placement(client, servicer):
36
- with app.run(client=client):
37
- _ = app.spawn_sandbox(
38
- "bash",
39
- "-c",
40
- "echo bye >&2 && sleep 1 && echo hi && exit 42",
41
- timeout=600,
42
- _experimental_scheduler=True,
43
- _experimental_scheduler_placement=SchedulerPlacement(
44
- region="us-east-1",
45
- zone="us-east-1a",
46
- spot=False,
47
- ),
48
- )
49
-
50
- assert len(servicer.sandbox_defs) == 1
51
- sb_def = servicer.sandbox_defs[0]
52
- assert sb_def._experimental_scheduler
53
- assert sb_def._experimental_scheduler_placement == api_pb2.SchedulerPlacement(
54
- _region="us-east-1",
55
- _zone="us-east-1a",
56
- _lifecycle="on-demand",
57
- )
test/secret_test.py DELETED
@@ -1,89 +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 App, Secret
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
- app = App()
19
- secret = Secret.from_dict({"FOO": "hello, world"})
20
- app.function(secrets=[secret])(dummy)
21
- with app.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
-
32
- with open(os.path.join(tmpdirname, ".env-dev"), "w") as f:
33
- f.write("# My settings\nUSER=user2\nPASSWORD=abc456\n")
34
-
35
- app = App()
36
- secret = Secret.from_dotenv(tmpdirname)
37
- app.function(secrets=[secret])(dummy)
38
- with app.run(client=client):
39
- assert secret.object_id == "st-0"
40
- assert servicer.secrets["st-0"] == {"USER": "user", "PASSWORD": "abc123"}
41
-
42
- app = App()
43
- secret = Secret.from_dotenv(tmpdirname, filename=".env-dev")
44
- app.function(secrets=[secret])(dummy)
45
- with app.run(client=client):
46
- assert secret.object_id == "st-1"
47
- assert servicer.secrets["st-1"] == {"USER": "user2", "PASSWORD": "abc456"}
48
-
49
-
50
- @mock.patch.dict(os.environ, {"FOO": "easy", "BAR": "1234"})
51
- def test_secret_from_local_environ(servicer, client):
52
- app = App()
53
- secret = Secret.from_local_environ(["FOO", "BAR"])
54
- app.function(secrets=[secret])(dummy)
55
- with app.run(client=client):
56
- assert secret.object_id == "st-0"
57
- assert servicer.secrets["st-0"] == {"FOO": "easy", "BAR": "1234"}
58
-
59
- with pytest.raises(InvalidError, match="NOTFOUND"):
60
- Secret.from_local_environ(["FOO", "NOTFOUND"])
61
-
62
-
63
- def test_init_types():
64
- with pytest.raises(InvalidError):
65
- Secret.from_dict({"foo": 1.0}) # type: ignore
66
-
67
-
68
- def test_secret_from_dict_none(servicer, client):
69
- app = App()
70
- secret = Secret.from_dict({"FOO": os.getenv("xyz"), "BAR": os.environ.get("abc"), "BAZ": "baz"})
71
- app.function(secrets=[secret])(dummy)
72
- with app.run(client=client):
73
- assert servicer.secrets["st-0"] == {"BAZ": "baz"}
74
-
75
-
76
- def test_secret_from_name(servicer, client):
77
- # Deploy secret
78
- secret_id = Secret.create_deployed("my-secret", {"FOO": "123"}, client=client)
79
-
80
- # Look up secret
81
- secret = Secret.lookup("my-secret", client=client)
82
- assert secret.object_id == secret_id
83
-
84
- # Look up secret through app
85
- app = App()
86
- secret = Secret.from_name("my-secret")
87
- app.function(secrets=[secret])(dummy)
88
- with app.run(client=client):
89
- assert secret.object_id == secret_id