modal 0.62.115__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.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +407 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +946 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/METADATA +5 -5
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.11.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))}
|
test/scheduler_placement_test.py
DELETED
@@ -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
|