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.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- 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 +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- 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 +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.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 +5 -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 +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- 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_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- 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 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- 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 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- 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 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- 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 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {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))}
|
test/scheduler_placement_test.py
DELETED
@@ -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
|
test/serialization_test.py
DELETED
@@ -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
|
test/stub_composition_test.py
DELETED
@@ -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
|