modAL 1.4.1__tar.gz → 1.4.1.dev0__tar.gz
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-1.4.1 → modal-1.4.1.dev0}/PKG-INFO +2 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/__init__.py +1 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/sandbox_fs_utils.py +1 -13
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/task_command_router_client.py +1 -5
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/container.py +4 -25
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/import_refs.py +4 -13
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/run.py +2 -4
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/shell.py +1 -5
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/client.py +1 -3
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/client.pyi +10 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/config.py +0 -30
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/dict.py +1 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/dict.pyi +2 -4
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/functions.pyi +6 -6
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/mount.py +5 -7
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/queue.py +1 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/queue.pyi +2 -4
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/sandbox.py +0 -108
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/sandbox.pyi +0 -127
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/secret.py +1 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/secret.pyi +2 -4
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/volume.py +1 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/volume.pyi +2 -4
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/requires.txt +1 -1
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/api_grpc.py +0 -16
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/api_pb2.py +1040 -1060
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/api_pb2.pyi +1 -39
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/api_pb2_grpc.py +0 -33
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/api_pb2_grpc.pyi +0 -10
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/modal_api_grpc.py +0 -1
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/task_command_router_pb2.py +30 -43
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/task_command_router_pb2.pyi +1 -21
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_version/__init__.py +1 -1
- {modal-1.4.1 → modal-1.4.1.dev0}/pyproject.toml +2 -2
- {modal-1.4.1 → modal-1.4.1.dev0}/LICENSE +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/README.md +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/__main__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_billing.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_clustered_functions.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_functions.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_grpc_client.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_ipython.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_load_context.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_location.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_logs.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_object.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_output/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_output/manager.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_output/pty.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_output/rich.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_output/status.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_partial_function.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_resolver.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_resources.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/asgi.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_serialization.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_server.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_traceback.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_tunnel.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_tunnel.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_type_manager.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/logger.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_utils/time_utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_vendor/version.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/_watcher.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/app.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/app.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/billing.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/README.md +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/builder/base-images.json +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/call_graph.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/_download.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/_traceback.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/app.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/billing.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/changelog.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/cluster.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/config.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/dashboard.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/dict.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/entry_point.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/environment.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/launch.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/profile.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/queues.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/secret.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/selector.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/token.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/utils.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cli/volume.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cls.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/cls.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/container_process.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/container_process.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/environments.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/environments.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/exception.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/experimental/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/experimental/flash.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/experimental/ipython.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/file_io.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/file_io.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/functions.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/image.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/image.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/io_streams.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/io_streams.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/mount.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/network_file_system.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/network_file_system.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/object.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/object.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/output.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/parallel_map.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/parallel_map.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/partial_function.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/partial_function.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/proxy.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/proxy.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/py.typed +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/retries.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/runner.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/runner.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/running_app.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/sandbox_fs.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/sandbox_fs.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/schedule.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/scheduler_placement.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/server.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/server.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/serving.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/serving.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/snapshot.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/snapshot.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/stream_type.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/token_flow.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal/token_flow.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/__init__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/py.typed +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/modal_version/__main__.py +0 -0
- {modal-1.4.1 → modal-1.4.1.dev0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.4.1
|
|
3
|
+
Version: 1.4.1.dev0
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,7 +23,7 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
|
|
|
23
23
|
Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
|
|
24
24
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
|
-
Requires-Dist: synchronicity~=0.
|
|
26
|
+
Requires-Dist: synchronicity~=0.11.1
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Requires-Dist: typer>=0.9
|
|
29
29
|
Requires-Dist: types-certifi
|
|
@@ -38,7 +38,7 @@ try:
|
|
|
38
38
|
from .proxy import Proxy
|
|
39
39
|
from .queue import Queue
|
|
40
40
|
from .retries import Retries
|
|
41
|
-
from .sandbox import
|
|
41
|
+
from .sandbox import Sandbox
|
|
42
42
|
from .schedule import Cron, Period
|
|
43
43
|
from .scheduler_placement import SchedulerPlacement
|
|
44
44
|
from .secret import Secret
|
|
@@ -68,7 +68,6 @@ __all__ = [
|
|
|
68
68
|
"Image",
|
|
69
69
|
"NetworkFileSystem",
|
|
70
70
|
"Period",
|
|
71
|
-
"Probe",
|
|
72
71
|
"Proxy",
|
|
73
72
|
"Queue",
|
|
74
73
|
"Retries",
|
|
@@ -28,7 +28,6 @@ _EXEC_SANDBOX_UNAVAILABLE_ERROR_TYPES = (NotFoundError, ServiceError, Connection
|
|
|
28
28
|
class ErrorPayload:
|
|
29
29
|
error_kind: str
|
|
30
30
|
message: str
|
|
31
|
-
detail: str = ""
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def _stderr_to_text(stderr: Union[str, bytes]) -> str:
|
|
@@ -52,18 +51,11 @@ def try_parse_error_payload(stderr: Union[str, bytes]) -> Optional[ErrorPayload]
|
|
|
52
51
|
return None
|
|
53
52
|
if not isinstance(message, str) or not message.strip():
|
|
54
53
|
return None
|
|
55
|
-
|
|
56
|
-
if not isinstance(detail, str):
|
|
57
|
-
detail = ""
|
|
58
|
-
return ErrorPayload(error_kind=error_kind, message=message, detail=detail)
|
|
54
|
+
return ErrorPayload(error_kind=error_kind, message=message)
|
|
59
55
|
|
|
60
56
|
|
|
61
57
|
def raise_read_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
62
58
|
if payload := try_parse_error_payload(stderr):
|
|
63
|
-
logger.debug(
|
|
64
|
-
f"sandbox-fs-tools read error: path={remote_path}, "
|
|
65
|
-
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
66
|
-
)
|
|
67
59
|
if payload.error_kind == "NotFound":
|
|
68
60
|
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
69
61
|
if payload.error_kind == "IsDirectory":
|
|
@@ -114,10 +106,6 @@ def translate_exec_unexpected_error(operation: str, path: str, exc: Exception) -
|
|
|
114
106
|
|
|
115
107
|
def raise_write_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
116
108
|
if payload := try_parse_error_payload(stderr):
|
|
117
|
-
logger.debug(
|
|
118
|
-
f"sandbox-fs-tools write error: path={remote_path}, "
|
|
119
|
-
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
120
|
-
)
|
|
121
109
|
if payload.error_kind == "NotDirectory" or payload.error_kind == "AlreadyExists":
|
|
122
110
|
raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
|
|
123
111
|
if payload.error_kind == "IsDirectory":
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import base64
|
|
4
4
|
import json
|
|
5
|
-
import socket
|
|
6
5
|
import ssl
|
|
7
6
|
import time
|
|
8
7
|
import urllib.parse
|
|
@@ -182,10 +181,7 @@ class TaskCommandRouterClient:
|
|
|
182
181
|
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
183
182
|
)
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
await _connect_channel(channel)
|
|
187
|
-
except socket.gaierror as exc:
|
|
188
|
-
raise ConnectionError(f"Could not resolve hostname '{host}': {exc}") from exc
|
|
184
|
+
await _connect_channel(channel)
|
|
189
185
|
loop = asyncio.get_running_loop()
|
|
190
186
|
jwt_refresh_lock = asyncio.Lock()
|
|
191
187
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
|
-
import warnings
|
|
3
2
|
from datetime import datetime, timezone
|
|
4
3
|
from typing import Optional, Union
|
|
5
4
|
|
|
@@ -186,40 +185,20 @@ async def logs(
|
|
|
186
185
|
task_info_resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=task_id))
|
|
187
186
|
app_id = task_info_resp.app_id
|
|
188
187
|
|
|
189
|
-
if not task_info_resp.info.started_at:
|
|
190
|
-
# Unlikely race or Modal backend issue, don't treat as a usage exception
|
|
191
|
-
return
|
|
192
|
-
container_started_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
|
|
193
|
-
|
|
194
188
|
now = datetime.now(timezone.utc)
|
|
195
189
|
if all_logs:
|
|
196
|
-
since_dt =
|
|
190
|
+
since_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
|
|
197
191
|
if task_info_resp.info.finished_at:
|
|
198
192
|
until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
|
|
199
193
|
else:
|
|
200
194
|
until_dt = now
|
|
201
195
|
else:
|
|
202
|
-
since_dt = _parse_time_arg(since, default=
|
|
203
|
-
if
|
|
204
|
-
default_until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
|
|
205
|
-
else:
|
|
206
|
-
default_until_dt = now
|
|
207
|
-
until_dt = _parse_time_arg(until, default=default_until_dt)
|
|
196
|
+
since_dt = _parse_time_arg(since, default=now) if since else None
|
|
197
|
+
until_dt = _parse_time_arg(until, default=now) if until else None
|
|
208
198
|
|
|
209
|
-
if
|
|
210
|
-
# User provided both --since and --until, but did so incorrectly
|
|
199
|
+
if since_dt is not None and until_dt is not None and since_dt >= until_dt:
|
|
211
200
|
raise UsageError("--since must be before --until.")
|
|
212
201
|
|
|
213
|
-
if since is not None and until is None and since_dt >= until_dt:
|
|
214
|
-
# User provided only --since and it is after the container finished
|
|
215
|
-
warnings.warn("--since time is after the Container finished, no logs to fetch.", UserWarning)
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
if until is not None and since is None and since_dt >= until_dt:
|
|
219
|
-
# User provided only --until and it is before the Container started
|
|
220
|
-
warnings.warn("--until time is before the Container started, no logs to fetch.", UserWarning)
|
|
221
|
-
return
|
|
222
|
-
|
|
223
202
|
if since_dt is not None:
|
|
224
203
|
effective_until = until_dt or now
|
|
225
204
|
if effective_until - since_dt > _MAX_FETCH_RANGE:
|
|
@@ -127,7 +127,6 @@ class AutoRunPriority:
|
|
|
127
127
|
|
|
128
128
|
def list_cli_commands(
|
|
129
129
|
module_members: dict[str, typing.Any],
|
|
130
|
-
include_service_functions: bool = False,
|
|
131
130
|
) -> list[CLICommand]:
|
|
132
131
|
"""
|
|
133
132
|
Extracts all runnables found either directly in the input module, or in any of the Apps listed in that module
|
|
@@ -150,8 +149,8 @@ def list_cli_commands(
|
|
|
150
149
|
all_runnables[local_entrypoint].append(f"{app_name}.{name}")
|
|
151
150
|
priorities[local_entrypoint] = AutoRunPriority.APP_LOCAL_ENTRYPOINT
|
|
152
151
|
for name, function in app.registered_functions.items():
|
|
153
|
-
if function.info.is_service_class()
|
|
154
|
-
continue # Skip class and server service functions
|
|
152
|
+
if function.info.is_service_class():
|
|
153
|
+
continue # Skip class and server service functions since they don't have associated cli commands
|
|
155
154
|
all_runnables[function].append(f"{app_name}.{name}")
|
|
156
155
|
priorities[function] = AutoRunPriority.APP_FUNCTION
|
|
157
156
|
for cls_name, cls in app.registered_classes.items():
|
|
@@ -323,12 +322,7 @@ def _get_runnable_app(runnable: Runnable) -> App:
|
|
|
323
322
|
|
|
324
323
|
|
|
325
324
|
def import_and_filter(
|
|
326
|
-
import_ref: ImportRef,
|
|
327
|
-
*,
|
|
328
|
-
base_cmd: str,
|
|
329
|
-
accept_local_entrypoint=True,
|
|
330
|
-
accept_webhook=False,
|
|
331
|
-
accept_service_functions=False,
|
|
325
|
+
import_ref: ImportRef, *, base_cmd: str, accept_local_entrypoint=True, accept_webhook=False
|
|
332
326
|
) -> tuple[Optional[Runnable], list[CLICommand]]:
|
|
333
327
|
"""Takes a function ref string and returns a single determined "runnable" to use, and a list of all available
|
|
334
328
|
runnables.
|
|
@@ -345,10 +339,7 @@ def import_and_filter(
|
|
|
345
339
|
"""
|
|
346
340
|
# all commands:
|
|
347
341
|
module = import_file_or_module(import_ref, base_cmd)
|
|
348
|
-
cli_commands = list_cli_commands(
|
|
349
|
-
dict(inspect.getmembers(module)),
|
|
350
|
-
include_service_functions=accept_service_functions,
|
|
351
|
-
)
|
|
342
|
+
cli_commands = list_cli_commands(dict(inspect.getmembers(module)))
|
|
352
343
|
|
|
353
344
|
# all commands that satisfy local entrypoint/accept webhook limitations AND object path prefix
|
|
354
345
|
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import time
|
|
8
8
|
import typing
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Callable, Optional
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
import typer
|
|
@@ -504,9 +504,7 @@ def deploy(
|
|
|
504
504
|
False, "-m", help="Interpret argument as a Python module path instead of a file/script path"
|
|
505
505
|
),
|
|
506
506
|
timestamps: bool = typer.Option(False, "--timestamps", help="Show timestamps for each log line."),
|
|
507
|
-
strategy:
|
|
508
|
-
"rolling", help="Deployment strategy", click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE))
|
|
509
|
-
),
|
|
507
|
+
strategy: DEPLOYMENT_STRATEGY_TYPE = typer.Option("rolling", help="Deployment strategy"),
|
|
510
508
|
):
|
|
511
509
|
"""Deploy a Modal application.
|
|
512
510
|
|
|
@@ -142,11 +142,7 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
|
|
|
142
142
|
def _function_spec_from_ref(ref: str, use_module_mode: bool) -> _FunctionSpec:
|
|
143
143
|
import_ref = parse_import_ref(ref, use_module_mode=use_module_mode)
|
|
144
144
|
runnable, all_usable_commands = import_and_filter(
|
|
145
|
-
import_ref,
|
|
146
|
-
base_cmd="modal shell",
|
|
147
|
-
accept_local_entrypoint=False,
|
|
148
|
-
accept_webhook=True,
|
|
149
|
-
accept_service_functions=True,
|
|
145
|
+
import_ref, base_cmd="modal shell", accept_local_entrypoint=False, accept_webhook=True
|
|
150
146
|
)
|
|
151
147
|
if not runnable:
|
|
152
148
|
help_header = (
|
|
@@ -22,7 +22,7 @@ from ._utils import async_utils
|
|
|
22
22
|
from ._utils.async_utils import TaskContext, synchronize_api
|
|
23
23
|
from ._utils.auth_token_manager import _AuthTokenManager
|
|
24
24
|
from ._utils.grpc_utils import ConnectionManager
|
|
25
|
-
from .config import
|
|
25
|
+
from .config import _check_config, _is_remote, config, logger
|
|
26
26
|
from .exception import AuthError, ClientClosed
|
|
27
27
|
|
|
28
28
|
HEARTBEAT_INTERVAL: float = config.get("heartbeat_interval")
|
|
@@ -56,8 +56,6 @@ def _get_metadata(client_type: int, credentials: Optional[tuple[str, str]], vers
|
|
|
56
56
|
"x-modal-token-secret": token_secret,
|
|
57
57
|
}
|
|
58
58
|
)
|
|
59
|
-
if agent_env := _agent_environment():
|
|
60
|
-
metadata["x-modal-agent-harness"] = urllib.parse.quote(agent_env)
|
|
61
59
|
return metadata
|
|
62
60
|
|
|
63
61
|
|
|
@@ -31,7 +31,11 @@ class _Client:
|
|
|
31
31
|
client_type: int
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
|
-
self,
|
|
34
|
+
self,
|
|
35
|
+
server_url: str,
|
|
36
|
+
client_type: int,
|
|
37
|
+
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
+
version: str = "1.4.1.dev0",
|
|
35
39
|
):
|
|
36
40
|
"""mdmd:hidden
|
|
37
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -163,7 +167,11 @@ class Client:
|
|
|
163
167
|
client_type: int
|
|
164
168
|
|
|
165
169
|
def __init__(
|
|
166
|
-
self,
|
|
170
|
+
self,
|
|
171
|
+
server_url: str,
|
|
172
|
+
client_type: int,
|
|
173
|
+
credentials: typing.Optional[tuple[str, str]],
|
|
174
|
+
version: str = "1.4.1.dev0",
|
|
167
175
|
):
|
|
168
176
|
"""mdmd:hidden
|
|
169
177
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -121,36 +121,6 @@ def _is_remote() -> bool:
|
|
|
121
121
|
return os.environ.get("MODAL_IS_REMOTE") == "1"
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
def _agent_environment() -> Optional[str]:
|
|
125
|
-
"""Detect if the current process is running inside an AI agent harness.
|
|
126
|
-
|
|
127
|
-
Returns the agent name if detected, or None otherwise.
|
|
128
|
-
|
|
129
|
-
Checks the emerging `AGENT` standard variable first, then falls back to
|
|
130
|
-
tool-specific environment variables used by known agent harnesses.
|
|
131
|
-
"""
|
|
132
|
-
# Emerging standard: AGENT=<name>
|
|
133
|
-
if agent := os.environ.get("AGENT"):
|
|
134
|
-
return agent
|
|
135
|
-
|
|
136
|
-
# Tool-specific environment variables
|
|
137
|
-
_TOOL_SPECIFIC_ENV_VARS: list[tuple[str, str]] = [
|
|
138
|
-
("CLAUDECODE", "claude-code"),
|
|
139
|
-
("GEMINI_CLI", "gemini-cli"),
|
|
140
|
-
("CURSOR_AGENT", "cursor"),
|
|
141
|
-
("CLINE_ACTIVE", "cline"),
|
|
142
|
-
("AUGMENT_AGENT", "augment"),
|
|
143
|
-
("OPENCODE_CLIENT", "opencode"),
|
|
144
|
-
("GOOSE_TERMINAL", "goose"),
|
|
145
|
-
("TRAE_AI_SHELL_ID", "trae"),
|
|
146
|
-
]
|
|
147
|
-
for env_var, agent_name in _TOOL_SPECIFIC_ENV_VARS:
|
|
148
|
-
if os.environ.get(env_var):
|
|
149
|
-
return agent_name
|
|
150
|
-
|
|
151
|
-
return None
|
|
152
|
-
|
|
153
|
-
|
|
154
124
|
def _read_user_config():
|
|
155
125
|
config_data = {}
|
|
156
126
|
if not _is_remote() and os.path.exists(user_config_path):
|
|
@@ -412,8 +412,7 @@ class _Dict(modal._object._Object):
|
|
|
412
412
|
...
|
|
413
413
|
|
|
414
414
|
@synchronicity.classproperty
|
|
415
|
-
|
|
416
|
-
def objects(cls) -> type[_DictManager]: ...
|
|
415
|
+
def objects(cls) -> _DictManager: ...
|
|
417
416
|
@property
|
|
418
417
|
def name(self) -> typing.Optional[str]: ...
|
|
419
418
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -654,8 +653,7 @@ class Dict(modal.object.Object):
|
|
|
654
653
|
...
|
|
655
654
|
|
|
656
655
|
@synchronicity.classproperty
|
|
657
|
-
|
|
658
|
-
def objects(cls) -> type[DictManager]: ...
|
|
656
|
+
def objects(cls) -> DictManager: ...
|
|
659
657
|
@property
|
|
660
658
|
def name(self) -> typing.Optional[str]: ...
|
|
661
659
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -348,7 +348,7 @@ class Function(
|
|
|
348
348
|
|
|
349
349
|
_call_generator: ___call_generator_spec
|
|
350
350
|
|
|
351
|
-
class __remote_spec(typing_extensions.Protocol[
|
|
351
|
+
class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
352
352
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
|
|
353
353
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
354
354
|
...
|
|
@@ -357,7 +357,7 @@ class Function(
|
|
|
357
357
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
358
358
|
...
|
|
359
359
|
|
|
360
|
-
remote: __remote_spec[modal._functions.
|
|
360
|
+
remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
|
|
361
361
|
|
|
362
362
|
class __remote_gen_spec(typing_extensions.Protocol):
|
|
363
363
|
def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
|
|
@@ -384,7 +384,7 @@ class Function(
|
|
|
384
384
|
"""
|
|
385
385
|
...
|
|
386
386
|
|
|
387
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
|
387
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
388
388
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
389
389
|
"""[Experimental] Calls the function with the given arguments, without waiting for the results.
|
|
390
390
|
|
|
@@ -407,7 +407,7 @@ class Function(
|
|
|
407
407
|
"""
|
|
408
408
|
...
|
|
409
409
|
|
|
410
|
-
_experimental_spawn: ___experimental_spawn_spec[modal._functions.
|
|
410
|
+
_experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
|
|
411
411
|
|
|
412
412
|
class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
|
|
413
413
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
|
|
@@ -415,7 +415,7 @@ class Function(
|
|
|
415
415
|
|
|
416
416
|
_spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
|
|
417
417
|
|
|
418
|
-
class __spawn_spec(typing_extensions.Protocol[
|
|
418
|
+
class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
|
|
419
419
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
420
420
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
421
421
|
|
|
@@ -436,7 +436,7 @@ class Function(
|
|
|
436
436
|
"""
|
|
437
437
|
...
|
|
438
438
|
|
|
439
|
-
spawn: __spawn_spec[modal._functions.
|
|
439
|
+
spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
|
|
440
440
|
|
|
441
441
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
|
|
442
442
|
"""Return the inner Python object wrapped by this Modal Function."""
|
|
@@ -699,18 +699,16 @@ Mount = synchronize_api(_Mount)
|
|
|
699
699
|
|
|
700
700
|
def _create_client_mount():
|
|
701
701
|
# TODO(erikbern): make this a static method on the Mount class
|
|
702
|
-
import importlib
|
|
703
|
-
|
|
704
702
|
import synchronicity
|
|
705
703
|
|
|
704
|
+
import modal
|
|
705
|
+
|
|
706
|
+
# Get the base_path because it also contains `modal_proto`.
|
|
707
|
+
modal_parent_dir, _ = os.path.split(modal.__path__[0])
|
|
706
708
|
client_mount = _Mount._new()
|
|
707
709
|
|
|
708
710
|
for pkg_name in MODAL_PACKAGES:
|
|
709
|
-
|
|
710
|
-
# assuming all packages are siblings of `modal`. In Bazel runfiles,
|
|
711
|
-
# modal and modal_proto live in different directories.
|
|
712
|
-
pkg_module = importlib.import_module(pkg_name)
|
|
713
|
-
package_base_path = Path(pkg_module.__path__[0])
|
|
711
|
+
package_base_path = Path(modal_parent_dir) / pkg_name
|
|
714
712
|
client_mount = client_mount.add_local_dir(
|
|
715
713
|
package_base_path,
|
|
716
714
|
remote_path=f"/pkg/{pkg_name}",
|
|
@@ -291,8 +291,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
291
291
|
raise RuntimeError("Queue() is not allowed. Please use `Queue.from_name(...)` or `Queue.ephemeral()` instead.")
|
|
292
292
|
|
|
293
293
|
@classproperty
|
|
294
|
-
|
|
295
|
-
def objects(cls) -> type[_QueueManager]:
|
|
294
|
+
def objects(cls) -> _QueueManager:
|
|
296
295
|
return _QueueManager
|
|
297
296
|
|
|
298
297
|
@property
|
|
@@ -431,8 +431,7 @@ class _Queue(modal._object._Object):
|
|
|
431
431
|
...
|
|
432
432
|
|
|
433
433
|
@synchronicity.classproperty
|
|
434
|
-
|
|
435
|
-
def objects(cls) -> type[_QueueManager]: ...
|
|
434
|
+
def objects(cls) -> _QueueManager: ...
|
|
436
435
|
@property
|
|
437
436
|
def name(self) -> typing.Optional[str]: ...
|
|
438
437
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -727,8 +726,7 @@ class Queue(modal.object.Object):
|
|
|
727
726
|
...
|
|
728
727
|
|
|
729
728
|
@synchronicity.classproperty
|
|
730
|
-
|
|
731
|
-
def objects(cls) -> type[QueueManager]: ...
|
|
729
|
+
def objects(cls) -> QueueManager: ...
|
|
732
730
|
@property
|
|
733
731
|
def name(self) -> typing.Optional[str]: ...
|
|
734
732
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -42,7 +42,6 @@ from .exception import (
|
|
|
42
42
|
InvalidError,
|
|
43
43
|
SandboxTerminatedError,
|
|
44
44
|
SandboxTimeoutError,
|
|
45
|
-
TimeoutError,
|
|
46
45
|
)
|
|
47
46
|
from .file_io import FileWatchEvent, FileWatchEventType, _FileIO, ls, mkdir, rm, watch
|
|
48
47
|
from .image import _Image
|
|
@@ -119,73 +118,6 @@ class SandboxConnectCredentials:
|
|
|
119
118
|
token: str
|
|
120
119
|
|
|
121
120
|
|
|
122
|
-
@dataclass(frozen=True)
|
|
123
|
-
class Probe:
|
|
124
|
-
"""Probe configuration for the Sandbox Readiness Probe.
|
|
125
|
-
|
|
126
|
-
Usage:
|
|
127
|
-
```py notest
|
|
128
|
-
# Wait until a file exists.
|
|
129
|
-
readiness_probe = modal.Probe.with_exec(
|
|
130
|
-
"sh", "-c", "test -f /tmp/ready",
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Wait until a TCP port is accepting connections.
|
|
134
|
-
readiness_probe = modal.Probe.with_tcp(8080)
|
|
135
|
-
|
|
136
|
-
app = modal.App.lookup('sandbox-readiness-probe', create_if_missing=True)
|
|
137
|
-
sandbox = modal.Sandbox.create(
|
|
138
|
-
"python3", "-m", "http.server", "8080",
|
|
139
|
-
readiness_probe=readiness_probe,
|
|
140
|
-
app=app,
|
|
141
|
-
)
|
|
142
|
-
sandbox.wait_until_ready()
|
|
143
|
-
```
|
|
144
|
-
"""
|
|
145
|
-
|
|
146
|
-
tcp_port: Optional[int] = None
|
|
147
|
-
exec_argv: Optional[tuple[str, ...]] = None
|
|
148
|
-
interval_ms: int = 100
|
|
149
|
-
|
|
150
|
-
def __post_init__(self):
|
|
151
|
-
if (self.tcp_port is None) == (self.exec_argv is None):
|
|
152
|
-
raise InvalidError("Probe must be created with Probe.with_tcp(...) or Probe.with_exec(...)")
|
|
153
|
-
|
|
154
|
-
@classmethod
|
|
155
|
-
def with_tcp(cls, port: int, *, interval_ms: int = 100) -> "Probe":
|
|
156
|
-
if not isinstance(port, int):
|
|
157
|
-
raise InvalidError("Probe.with_tcp() expects an integer `port`")
|
|
158
|
-
if port <= 0 or port > 65535:
|
|
159
|
-
raise InvalidError(f"Probe.with_tcp() expects `port` in [1, 65535], got {port}")
|
|
160
|
-
if not isinstance(interval_ms, int):
|
|
161
|
-
raise InvalidError("Probe.with_tcp() expects an integer `interval_ms`")
|
|
162
|
-
if interval_ms <= 0:
|
|
163
|
-
raise InvalidError(f"Probe.with_tcp() expects `interval_ms` > 0, got {interval_ms}")
|
|
164
|
-
return cls(tcp_port=port, interval_ms=interval_ms)
|
|
165
|
-
|
|
166
|
-
@classmethod
|
|
167
|
-
def with_exec(cls, *argv: str, interval_ms: int = 100) -> "Probe":
|
|
168
|
-
if len(argv) == 0:
|
|
169
|
-
raise InvalidError("Probe.with_exec() requires at least one argument")
|
|
170
|
-
if not all(isinstance(arg, str) for arg in argv):
|
|
171
|
-
raise InvalidError("Probe.with_exec() expects all arguments to be strings")
|
|
172
|
-
if not isinstance(interval_ms, int):
|
|
173
|
-
raise InvalidError("Probe.with_exec() expects an integer `interval_ms`")
|
|
174
|
-
if interval_ms <= 0:
|
|
175
|
-
raise InvalidError(f"Probe.with_exec() expects `interval_ms` > 0, got {interval_ms}")
|
|
176
|
-
return cls(exec_argv=tuple(argv), interval_ms=interval_ms)
|
|
177
|
-
|
|
178
|
-
def _to_proto(self) -> api_pb2.Probe:
|
|
179
|
-
if self.tcp_port is not None:
|
|
180
|
-
return api_pb2.Probe(tcp_port=self.tcp_port, interval_ms=self.interval_ms)
|
|
181
|
-
if self.exec_argv is not None:
|
|
182
|
-
return api_pb2.Probe(
|
|
183
|
-
exec_command=api_pb2.Probe.ExecCommand(argv=list(self.exec_argv)),
|
|
184
|
-
interval_ms=self.interval_ms,
|
|
185
|
-
)
|
|
186
|
-
raise InvalidError("Probe must be created with Probe.with_tcp(...) or Probe.with_exec(...)")
|
|
187
|
-
|
|
188
|
-
|
|
189
121
|
class _Sandbox(_Object, type_prefix="sb"):
|
|
190
122
|
"""A `Sandbox` object lets you interact with a running sandbox. This API is similar to Python's
|
|
191
123
|
[asyncio.subprocess.Process](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.subprocess.Process).
|
|
@@ -234,7 +166,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
234
166
|
h2_ports: Sequence[int] = [],
|
|
235
167
|
unencrypted_ports: Sequence[int] = [],
|
|
236
168
|
proxy: Optional[_Proxy] = None,
|
|
237
|
-
readiness_probe: Optional[Probe] = None,
|
|
238
169
|
experimental_options: Optional[dict[str, bool]] = None,
|
|
239
170
|
enable_snapshot: bool = False,
|
|
240
171
|
verbose: bool = False,
|
|
@@ -348,7 +279,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
348
279
|
open_ports=api_pb2.PortSpecs(ports=open_ports),
|
|
349
280
|
network_access=network_access,
|
|
350
281
|
proxy_id=(proxy.object_id if proxy else None),
|
|
351
|
-
readiness_probe=(readiness_probe._to_proto() if readiness_probe else None),
|
|
352
282
|
enable_snapshot=enable_snapshot,
|
|
353
283
|
verbose=verbose,
|
|
354
284
|
name=name,
|
|
@@ -409,8 +339,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
409
339
|
proxy: Optional[_Proxy] = None,
|
|
410
340
|
# If True, the sandbox will receive a MODAL_IDENTITY_TOKEN env var for OIDC-based auth.
|
|
411
341
|
include_oidc_identity_token: bool = False,
|
|
412
|
-
# Probe used to determine when the sandbox has become ready.
|
|
413
|
-
readiness_probe: Optional[Probe] = None,
|
|
414
342
|
# Enable verbose logging for sandbox operations.
|
|
415
343
|
verbose: bool = False,
|
|
416
344
|
experimental_options: Optional[dict[str, bool]] = None,
|
|
@@ -475,7 +403,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
475
403
|
h2_ports=h2_ports,
|
|
476
404
|
unencrypted_ports=unencrypted_ports,
|
|
477
405
|
proxy=proxy,
|
|
478
|
-
readiness_probe=readiness_probe,
|
|
479
406
|
experimental_options=experimental_options,
|
|
480
407
|
_experimental_enable_snapshot=_experimental_enable_snapshot,
|
|
481
408
|
include_oidc_identity_token=include_oidc_identity_token,
|
|
@@ -512,7 +439,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
512
439
|
unencrypted_ports: Sequence[int] = [],
|
|
513
440
|
proxy: Optional[_Proxy] = None,
|
|
514
441
|
include_oidc_identity_token: bool = False,
|
|
515
|
-
readiness_probe: Optional[Probe] = None,
|
|
516
442
|
experimental_options: Optional[dict[str, bool]] = None,
|
|
517
443
|
_experimental_enable_snapshot: bool = False,
|
|
518
444
|
client: Optional[_Client] = None,
|
|
@@ -564,7 +490,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
564
490
|
h2_ports=h2_ports,
|
|
565
491
|
unencrypted_ports=unencrypted_ports,
|
|
566
492
|
proxy=proxy,
|
|
567
|
-
readiness_probe=readiness_probe,
|
|
568
493
|
experimental_options=experimental_options,
|
|
569
494
|
enable_snapshot=_experimental_enable_snapshot,
|
|
570
495
|
verbose=verbose,
|
|
@@ -1033,39 +958,6 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1033
958
|
raise SandboxTerminatedError()
|
|
1034
959
|
break
|
|
1035
960
|
|
|
1036
|
-
async def wait_until_ready(self, *, timeout: int = 300) -> None:
|
|
1037
|
-
"""Wait for the Sandbox readiness probe to report that the Sandbox is ready.
|
|
1038
|
-
|
|
1039
|
-
The Sandbox must be configured with a `readiness_probe` in order to use this method.
|
|
1040
|
-
|
|
1041
|
-
Usage:
|
|
1042
|
-
```py notest
|
|
1043
|
-
app = modal.App.lookup('sandbox-wait-until-ready', create_if_missing=True)
|
|
1044
|
-
sandbox = modal.Sandbox.create(
|
|
1045
|
-
"python3", "-m", "http.server", "8080",
|
|
1046
|
-
readiness_probe=modal.Probe.with_tcp(8080),
|
|
1047
|
-
app=app,
|
|
1048
|
-
)
|
|
1049
|
-
sandbox.wait_until_ready()
|
|
1050
|
-
```
|
|
1051
|
-
"""
|
|
1052
|
-
if timeout <= 0:
|
|
1053
|
-
raise InvalidError(f"`timeout` must be positive, got: {timeout}")
|
|
1054
|
-
|
|
1055
|
-
deadline = time.monotonic() + timeout
|
|
1056
|
-
remaining_timeout = deadline - time.monotonic()
|
|
1057
|
-
while remaining_timeout > 0:
|
|
1058
|
-
req = api_pb2.SandboxWaitUntilReadyRequest(
|
|
1059
|
-
sandbox_id=self.object_id,
|
|
1060
|
-
timeout=min(remaining_timeout, 50.0),
|
|
1061
|
-
)
|
|
1062
|
-
resp = await self._client.stub.SandboxWaitUntilReady(req)
|
|
1063
|
-
if resp.ready_at > 0:
|
|
1064
|
-
return
|
|
1065
|
-
|
|
1066
|
-
remaining_timeout = deadline - time.monotonic()
|
|
1067
|
-
raise TimeoutError()
|
|
1068
|
-
|
|
1069
961
|
async def tunnels(self, timeout: int = 50) -> dict[int, Tunnel]:
|
|
1070
962
|
"""Get Tunnel metadata for the sandbox.
|
|
1071
963
|
|