modAL 1.3.6.dev14__tar.gz → 1.4.1__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.3.6.dev14 → modal-1.4.1}/PKG-INFO +2 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/__init__.py +2 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_functions.py +0 -3
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/pty.py +5 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/async_utils.py +27 -9
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/grpc_utils.py +2 -3
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/sandbox_fs_utils.py +13 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/task_command_router_client.py +20 -4
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/app.py +7 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/container.py +36 -5
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/import_refs.py +13 -4
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/run.py +4 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/shell.py +5 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/client.py +3 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/client.pyi +2 -10
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/config.py +30 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/dict.py +2 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/dict.pyi +4 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/functions.pyi +13 -23
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/mount.py +7 -5
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/output.py +2 -5
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/parallel_map.py +18 -59
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/parallel_map.pyi +6 -13
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/queue.py +2 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/queue.pyi +4 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/runner.py +14 -4
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/runner.pyi +1 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox.py +309 -10
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox.pyi +232 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/secret.py +2 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/secret.pyi +4 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/serving.py +2 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/volume.py +2 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/volume.pyi +4 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/requires.txt +1 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_grpc.py +16 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2.py +1060 -1040
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2.pyi +39 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2.py +43 -30
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2.pyi +21 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_version/__init__.py +1 -1
- {modal-1.3.6.dev14 → modal-1.4.1}/pyproject.toml +2 -2
- {modal-1.3.6.dev14 → modal-1.4.1}/LICENSE +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/README.md +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/__main__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_billing.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_clustered_functions.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_container_entrypoint.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_grpc_client.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_ipython.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_load_context.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_location.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_logs.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_object.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/manager.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/rich.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_output/status.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_partial_function.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_resolver.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_resources.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_serialization.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_server.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_traceback.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_tunnel.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_tunnel.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_type_manager.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/blob_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/browser_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/deprecation.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/function_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/logger.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_vendor/version.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/_watcher.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/app.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/app.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/billing.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/2025.06.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/README.md +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/builder/base-images.json +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/call_graph.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/_download.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/_traceback.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/billing.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/changelog.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/cluster.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/config.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/dashboard.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/dict.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/entry_point.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/environment.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/launch.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/profile.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/queues.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/secret.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/selector.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/token.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/utils.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cli/volume.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cls.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/cls.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/container_process.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/container_process.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/environments.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/environments.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/exception.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/flash.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/experimental/ipython.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_io.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_io.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/functions.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/image.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/image.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/io_streams.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/io_streams.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/mount.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/network_file_system.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/network_file_system.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/object.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/object.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/partial_function.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/partial_function.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/proxy.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/proxy.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/py.typed +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/retries.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/running_app.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox_fs.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/sandbox_fs.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/schedule.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/scheduler_placement.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/server.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/server.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/serving.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/snapshot.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/snapshot.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/stream_type.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/token_flow.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal/token_flow.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/__init__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/py.typed +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/modal_version/__main__.py +0 -0
- {modal-1.3.6.dev14 → modal-1.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
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.12.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 Sandbox
|
|
41
|
+
from .sandbox import Probe, Sandbox
|
|
42
42
|
from .schedule import Cron, Period
|
|
43
43
|
from .scheduler_placement import SchedulerPlacement
|
|
44
44
|
from .secret import Secret
|
|
@@ -68,6 +68,7 @@ __all__ = [
|
|
|
68
68
|
"Image",
|
|
69
69
|
"NetworkFileSystem",
|
|
70
70
|
"Period",
|
|
71
|
+
"Probe",
|
|
71
72
|
"Proxy",
|
|
72
73
|
"Queue",
|
|
73
74
|
"Retries",
|
|
@@ -1562,7 +1562,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1562
1562
|
input_queue: _SynchronizedQueue,
|
|
1563
1563
|
order_outputs: bool,
|
|
1564
1564
|
return_exceptions: bool,
|
|
1565
|
-
wrap_returned_exceptions: bool,
|
|
1566
1565
|
) -> AsyncGenerator[Any, None]:
|
|
1567
1566
|
"""mdmd:hidden
|
|
1568
1567
|
|
|
@@ -1588,7 +1587,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1588
1587
|
self.client,
|
|
1589
1588
|
order_outputs,
|
|
1590
1589
|
return_exceptions,
|
|
1591
|
-
wrap_returned_exceptions,
|
|
1592
1590
|
count_update_callback,
|
|
1593
1591
|
)
|
|
1594
1592
|
) as stream:
|
|
@@ -1602,7 +1600,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1602
1600
|
self.client,
|
|
1603
1601
|
order_outputs,
|
|
1604
1602
|
return_exceptions,
|
|
1605
|
-
wrap_returned_exceptions,
|
|
1606
1603
|
count_update_callback,
|
|
1607
1604
|
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
|
1608
1605
|
)
|
|
@@ -147,6 +147,11 @@ async def get_app_logs_loop(
|
|
|
147
147
|
# doesn't support server-side search)
|
|
148
148
|
if search_text and search_text not in log.data:
|
|
149
149
|
return
|
|
150
|
+
# Search results may lack trailing newlines; ensure each
|
|
151
|
+
# match is a complete line so the LineBufferedOutput buffer
|
|
152
|
+
# doesn't merge it with the next match.
|
|
153
|
+
if search_text and not log.data.endswith("\n"):
|
|
154
|
+
log.data += "\n"
|
|
150
155
|
if pty_shell_finish_event:
|
|
151
156
|
await output_mgr.put_pty_content(log)
|
|
152
157
|
else:
|
|
@@ -349,19 +349,18 @@ def retry(
|
|
|
349
349
|
direct_fn=None,
|
|
350
350
|
*,
|
|
351
351
|
n_attempts=3,
|
|
352
|
-
base_delay=0,
|
|
352
|
+
base_delay=0.0,
|
|
353
353
|
delay_factor=2,
|
|
354
|
+
max_delay: Optional[float] = None,
|
|
354
355
|
attempt_timeout: Optional[float] = 90,
|
|
355
|
-
|
|
356
|
+
total_timeout: Optional[float] = None,
|
|
356
357
|
):
|
|
357
358
|
"""Decorator that calls an async function multiple times, with a given timeout.
|
|
358
359
|
|
|
359
360
|
If a `base_delay` is provided, the function is given an exponentially
|
|
360
361
|
increasing delay on each run, up until the maximum number of attempts.
|
|
361
362
|
|
|
362
|
-
If `
|
|
363
|
-
increases exponentially (starting from `attempt_timeout`, multiplied by
|
|
364
|
-
`attempt_timeout_factor` after each failed attempt).
|
|
363
|
+
If `max_delay` is set, the delay between attempts is capped at this value.
|
|
365
364
|
|
|
366
365
|
Usage:
|
|
367
366
|
|
|
@@ -377,6 +376,7 @@ def retry(
|
|
|
377
376
|
pass
|
|
378
377
|
```
|
|
379
378
|
"""
|
|
379
|
+
ATTEMPT_TIMEOUT_FLOOR = 2.0
|
|
380
380
|
|
|
381
381
|
def decorator(fn):
|
|
382
382
|
@functools.wraps(fn)
|
|
@@ -387,17 +387,35 @@ def retry(
|
|
|
387
387
|
local_n_attempts = n_attempts
|
|
388
388
|
|
|
389
389
|
delay = base_delay
|
|
390
|
-
|
|
390
|
+
if total_timeout is not None:
|
|
391
|
+
total_deadline = time.time() + total_timeout
|
|
392
|
+
else:
|
|
393
|
+
total_deadline = None
|
|
394
|
+
|
|
391
395
|
for i in range(local_n_attempts):
|
|
392
396
|
t0 = time.time()
|
|
397
|
+
|
|
398
|
+
timeouts = []
|
|
399
|
+
if attempt_timeout is not None:
|
|
400
|
+
timeouts.append(attempt_timeout)
|
|
401
|
+
if total_deadline is not None:
|
|
402
|
+
timeouts.append(max(total_deadline - time.time(), ATTEMPT_TIMEOUT_FLOOR))
|
|
403
|
+
timeout = min(timeouts) if timeouts else None
|
|
404
|
+
|
|
393
405
|
try:
|
|
394
|
-
return await asyncio.wait_for(fn(*args, **kwargs), timeout=
|
|
406
|
+
return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
|
|
395
407
|
except asyncio.CancelledError:
|
|
396
408
|
logger.debug(f"Function {fn} was cancelled")
|
|
397
409
|
raise
|
|
398
410
|
except Exception as e:
|
|
399
411
|
if i >= local_n_attempts - 1:
|
|
400
412
|
raise
|
|
413
|
+
|
|
414
|
+
# We check if the timeout will be reached **after** the sleep, so we can raise an error early
|
|
415
|
+
# without needing to actually sleep.
|
|
416
|
+
if total_deadline is not None and time.time() + delay + ATTEMPT_TIMEOUT_FLOOR >= total_deadline:
|
|
417
|
+
raise
|
|
418
|
+
|
|
401
419
|
logger.debug(
|
|
402
420
|
f"Failed invoking function {fn}: {e}"
|
|
403
421
|
f" (took {time.time() - t0}s, sleeping {delay}s"
|
|
@@ -405,8 +423,8 @@ def retry(
|
|
|
405
423
|
)
|
|
406
424
|
await asyncio.sleep(delay)
|
|
407
425
|
delay *= delay_factor
|
|
408
|
-
if
|
|
409
|
-
|
|
426
|
+
if max_delay is not None:
|
|
427
|
+
delay = min(delay, max_delay)
|
|
410
428
|
|
|
411
429
|
return f_wrapped
|
|
412
430
|
|
|
@@ -90,8 +90,6 @@ RETRYABLE_GRPC_STATUS_CODES = [
|
|
|
90
90
|
]
|
|
91
91
|
SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
|
|
92
92
|
# Initial per-attempt timeout for TCP/TLS handshake in connect_channel.
|
|
93
|
-
# Doubles each attempt (attempt_timeout_factor=2): 0.5 + 1 + 2 + 4 + 8 = 15.5s + ~1.5s inter-attempt delays.
|
|
94
|
-
CONNECT_TIMEOUT = 0.5
|
|
95
93
|
DEFAULT_MAX_RETRIES = 3
|
|
96
94
|
|
|
97
95
|
|
|
@@ -225,7 +223,8 @@ def create_channel(
|
|
|
225
223
|
return channel
|
|
226
224
|
|
|
227
225
|
|
|
228
|
-
|
|
226
|
+
# With 18 attempts, the max delays between calls is: 0.1 + 0.2 + 0.4 + 0.8 + 1.6 + 3.2 + 5*11 ~ 62
|
|
227
|
+
@retry(n_attempts=18, base_delay=0.1, attempt_timeout=10.0, max_delay=5.0, total_timeout=63.0)
|
|
229
228
|
async def connect_channel(channel: grpclib.client.Channel):
|
|
230
229
|
"""Connect to socket and raise exceptions when there is a connection issue."""
|
|
231
230
|
await channel.__connect__()
|
|
@@ -28,6 +28,7 @@ _EXEC_SANDBOX_UNAVAILABLE_ERROR_TYPES = (NotFoundError, ServiceError, Connection
|
|
|
28
28
|
class ErrorPayload:
|
|
29
29
|
error_kind: str
|
|
30
30
|
message: str
|
|
31
|
+
detail: str = ""
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def _stderr_to_text(stderr: Union[str, bytes]) -> str:
|
|
@@ -51,11 +52,18 @@ def try_parse_error_payload(stderr: Union[str, bytes]) -> Optional[ErrorPayload]
|
|
|
51
52
|
return None
|
|
52
53
|
if not isinstance(message, str) or not message.strip():
|
|
53
54
|
return None
|
|
54
|
-
|
|
55
|
+
detail = payload.get("detail", "")
|
|
56
|
+
if not isinstance(detail, str):
|
|
57
|
+
detail = ""
|
|
58
|
+
return ErrorPayload(error_kind=error_kind, message=message, detail=detail)
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
def raise_read_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
58
62
|
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
|
+
)
|
|
59
67
|
if payload.error_kind == "NotFound":
|
|
60
68
|
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
61
69
|
if payload.error_kind == "IsDirectory":
|
|
@@ -106,6 +114,10 @@ def translate_exec_unexpected_error(operation: str, path: str, exc: Exception) -
|
|
|
106
114
|
|
|
107
115
|
def raise_write_file_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
108
116
|
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
|
+
)
|
|
109
121
|
if payload.error_kind == "NotDirectory" or payload.error_kind == "AlreadyExists":
|
|
110
122
|
raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
|
|
111
123
|
if payload.error_kind == "IsDirectory":
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import base64
|
|
4
4
|
import json
|
|
5
|
+
import socket
|
|
5
6
|
import ssl
|
|
6
7
|
import time
|
|
7
8
|
import urllib.parse
|
|
@@ -11,7 +12,6 @@ from typing import AsyncGenerator, Optional
|
|
|
11
12
|
|
|
12
13
|
import grpclib.client
|
|
13
14
|
import grpclib.config
|
|
14
|
-
import grpclib.events
|
|
15
15
|
from grpclib import GRPCError, Status
|
|
16
16
|
from grpclib.exceptions import StreamTerminatedError
|
|
17
17
|
|
|
@@ -22,8 +22,21 @@ from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
|
22
22
|
|
|
23
23
|
from .._grpc_client import grpc_error_converter
|
|
24
24
|
from .._utils.grpc_utils import PermanentCloseableChannel
|
|
25
|
-
from .async_utils import aclosing
|
|
26
|
-
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
25
|
+
from .async_utils import aclosing, retry
|
|
26
|
+
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
|
|
30
|
+
async def _connect_channel(channel: grpclib.client.Channel):
|
|
31
|
+
"""Connect to the command router channel.
|
|
32
|
+
|
|
33
|
+
Uses a longer retry budget than grpc_utils.connect_channel. In rare cases the sandbox
|
|
34
|
+
may take a long time to start on the worker after scheduling.
|
|
35
|
+
|
|
36
|
+
Retries with exponential backoff (1, 2, 4, 8, 10, 10, ...) capped at 10s per delay.
|
|
37
|
+
Total sleep between attempts: 1 + 2 + 4 + 8 + 10*29 = 305s (~5 min).
|
|
38
|
+
"""
|
|
39
|
+
await channel.__connect__()
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
def _b64url_decode(data: str) -> bytes:
|
|
@@ -169,7 +182,10 @@ class TaskCommandRouterClient:
|
|
|
169
182
|
closed_error_message="Unable to perform operation on a detached sandbox",
|
|
170
183
|
)
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
try:
|
|
186
|
+
await _connect_channel(channel)
|
|
187
|
+
except socket.gaierror as exc:
|
|
188
|
+
raise ConnectionError(f"Could not resolve hostname '{host}': {exc}") from exc
|
|
173
189
|
loop = asyncio.get_running_loop()
|
|
174
190
|
jwt_refresh_lock = asyncio.Lock()
|
|
175
191
|
|
|
@@ -164,7 +164,7 @@ def logs(
|
|
|
164
164
|
Get recent logs based on an app ID:
|
|
165
165
|
|
|
166
166
|
```
|
|
167
|
-
modal app logs ap-123456
|
|
167
|
+
modal app logs ap-123456
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
Get recent logs for a currently deployed App based on its name:
|
|
@@ -179,6 +179,12 @@ def logs(
|
|
|
179
179
|
modal app logs my-app -f
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
Fetch the last 1000 entries:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
modal app logs my-app --tail 1000
|
|
186
|
+
```
|
|
187
|
+
|
|
182
188
|
Fetch logs from the last 2 hours:
|
|
183
189
|
|
|
184
190
|
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
|
+
import warnings
|
|
2
3
|
from datetime import datetime, timezone
|
|
3
4
|
from typing import Optional, Union
|
|
4
5
|
|
|
@@ -27,7 +28,11 @@ container_cli = typer.Typer(name="container", help="Manage and connect to runnin
|
|
|
27
28
|
|
|
28
29
|
@container_cli.command("list")
|
|
29
30
|
@synchronizer.create_blocking
|
|
30
|
-
async def list_(
|
|
31
|
+
async def list_(
|
|
32
|
+
app_id: str = typer.Option("", "--app-id", help="List containers running for a specific App."),
|
|
33
|
+
env: Optional[str] = ENV_OPTION,
|
|
34
|
+
json: bool = False,
|
|
35
|
+
):
|
|
31
36
|
"""List all containers that are currently running."""
|
|
32
37
|
env = ensure_env(env)
|
|
33
38
|
client = await _Client.from_env()
|
|
@@ -113,6 +118,12 @@ async def logs(
|
|
|
113
118
|
modal container logs ta-123456 --since 2026-03-01T05:00:00 --until 2026-03-01T08:00:00
|
|
114
119
|
```
|
|
115
120
|
|
|
121
|
+
Fetch the last 1000 entries:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
modal container logs ta-123456 --tail 1000
|
|
125
|
+
```
|
|
126
|
+
|
|
116
127
|
Fetch all container logs:
|
|
117
128
|
|
|
118
129
|
```
|
|
@@ -175,20 +186,40 @@ async def logs(
|
|
|
175
186
|
task_info_resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=task_id))
|
|
176
187
|
app_id = task_info_resp.app_id
|
|
177
188
|
|
|
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
|
+
|
|
178
194
|
now = datetime.now(timezone.utc)
|
|
179
195
|
if all_logs:
|
|
180
|
-
since_dt =
|
|
196
|
+
since_dt = container_started_dt
|
|
181
197
|
if task_info_resp.info.finished_at:
|
|
182
198
|
until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
|
|
183
199
|
else:
|
|
184
200
|
until_dt = now
|
|
185
201
|
else:
|
|
186
|
-
since_dt = _parse_time_arg(since, default=
|
|
187
|
-
|
|
202
|
+
since_dt = _parse_time_arg(since, default=container_started_dt)
|
|
203
|
+
if task_info_resp.info.finished_at:
|
|
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)
|
|
188
208
|
|
|
189
|
-
if
|
|
209
|
+
if since is not None and until is not None and since_dt >= until_dt:
|
|
210
|
+
# User provided both --since and --until, but did so incorrectly
|
|
190
211
|
raise UsageError("--since must be before --until.")
|
|
191
212
|
|
|
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
|
+
|
|
192
223
|
if since_dt is not None:
|
|
193
224
|
effective_until = until_dt or now
|
|
194
225
|
if effective_until - since_dt > _MAX_FETCH_RANGE:
|
|
@@ -127,6 +127,7 @@ class AutoRunPriority:
|
|
|
127
127
|
|
|
128
128
|
def list_cli_commands(
|
|
129
129
|
module_members: dict[str, typing.Any],
|
|
130
|
+
include_service_functions: bool = False,
|
|
130
131
|
) -> list[CLICommand]:
|
|
131
132
|
"""
|
|
132
133
|
Extracts all runnables found either directly in the input module, or in any of the Apps listed in that module
|
|
@@ -149,8 +150,8 @@ def list_cli_commands(
|
|
|
149
150
|
all_runnables[local_entrypoint].append(f"{app_name}.{name}")
|
|
150
151
|
priorities[local_entrypoint] = AutoRunPriority.APP_LOCAL_ENTRYPOINT
|
|
151
152
|
for name, function in app.registered_functions.items():
|
|
152
|
-
if function.info.is_service_class():
|
|
153
|
-
continue # Skip class and server service functions
|
|
153
|
+
if function.info.is_service_class() and not include_service_functions:
|
|
154
|
+
continue # Skip class and server service functions for all commands except `modal shell`.
|
|
154
155
|
all_runnables[function].append(f"{app_name}.{name}")
|
|
155
156
|
priorities[function] = AutoRunPriority.APP_FUNCTION
|
|
156
157
|
for cls_name, cls in app.registered_classes.items():
|
|
@@ -322,7 +323,12 @@ def _get_runnable_app(runnable: Runnable) -> App:
|
|
|
322
323
|
|
|
323
324
|
|
|
324
325
|
def import_and_filter(
|
|
325
|
-
import_ref: ImportRef,
|
|
326
|
+
import_ref: ImportRef,
|
|
327
|
+
*,
|
|
328
|
+
base_cmd: str,
|
|
329
|
+
accept_local_entrypoint=True,
|
|
330
|
+
accept_webhook=False,
|
|
331
|
+
accept_service_functions=False,
|
|
326
332
|
) -> tuple[Optional[Runnable], list[CLICommand]]:
|
|
327
333
|
"""Takes a function ref string and returns a single determined "runnable" to use, and a list of all available
|
|
328
334
|
runnables.
|
|
@@ -339,7 +345,10 @@ def import_and_filter(
|
|
|
339
345
|
"""
|
|
340
346
|
# all commands:
|
|
341
347
|
module = import_file_or_module(import_ref, base_cmd)
|
|
342
|
-
cli_commands = list_cli_commands(
|
|
348
|
+
cli_commands = list_cli_commands(
|
|
349
|
+
dict(inspect.getmembers(module)),
|
|
350
|
+
include_service_functions=accept_service_functions,
|
|
351
|
+
)
|
|
343
352
|
|
|
344
353
|
# all commands that satisfy local entrypoint/accept webhook limitations AND object path prefix
|
|
345
354
|
|
|
@@ -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, get_args
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
import typer
|
|
@@ -504,7 +504,9 @@ 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:
|
|
507
|
+
strategy: str = typer.Option(
|
|
508
|
+
"rolling", help="Deployment strategy", click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE))
|
|
509
|
+
),
|
|
508
510
|
):
|
|
509
511
|
"""Deploy a Modal application.
|
|
510
512
|
|
|
@@ -142,7 +142,11 @@ 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,
|
|
145
|
+
import_ref,
|
|
146
|
+
base_cmd="modal shell",
|
|
147
|
+
accept_local_entrypoint=False,
|
|
148
|
+
accept_webhook=True,
|
|
149
|
+
accept_service_functions=True,
|
|
146
150
|
)
|
|
147
151
|
if not runnable:
|
|
148
152
|
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 _check_config, _is_remote, config, logger
|
|
25
|
+
from .config import _agent_environment, _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,6 +56,8 @@ 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)
|
|
59
61
|
return metadata
|
|
60
62
|
|
|
61
63
|
|
|
@@ -31,11 +31,7 @@ class _Client:
|
|
|
31
31
|
client_type: int
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
server_url: str,
|
|
36
|
-
client_type: int,
|
|
37
|
-
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.3.6.dev14",
|
|
34
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.4.1"
|
|
39
35
|
):
|
|
40
36
|
"""mdmd:hidden
|
|
41
37
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -167,11 +163,7 @@ class Client:
|
|
|
167
163
|
client_type: int
|
|
168
164
|
|
|
169
165
|
def __init__(
|
|
170
|
-
self,
|
|
171
|
-
server_url: str,
|
|
172
|
-
client_type: int,
|
|
173
|
-
credentials: typing.Optional[tuple[str, str]],
|
|
174
|
-
version: str = "1.3.6.dev14",
|
|
166
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.4.1"
|
|
175
167
|
):
|
|
176
168
|
"""mdmd:hidden
|
|
177
169
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -121,6 +121,36 @@ 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
|
+
|
|
124
154
|
def _read_user_config():
|
|
125
155
|
config_data = {}
|
|
126
156
|
if not _is_remote() and os.path.exists(user_config_path):
|
|
@@ -412,7 +412,8 @@ class _Dict(modal._object._Object):
|
|
|
412
412
|
...
|
|
413
413
|
|
|
414
414
|
@synchronicity.classproperty
|
|
415
|
-
|
|
415
|
+
@classmethod
|
|
416
|
+
def objects(cls) -> type[_DictManager]: ...
|
|
416
417
|
@property
|
|
417
418
|
def name(self) -> typing.Optional[str]: ...
|
|
418
419
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
|
@@ -653,7 +654,8 @@ class Dict(modal.object.Object):
|
|
|
653
654
|
...
|
|
654
655
|
|
|
655
656
|
@synchronicity.classproperty
|
|
656
|
-
|
|
657
|
+
@classmethod
|
|
658
|
+
def objects(cls) -> type[DictManager]: ...
|
|
657
659
|
@property
|
|
658
660
|
def name(self) -> typing.Optional[str]: ...
|
|
659
661
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|