modAL 1.2.1.dev22__tar.gz → 1.2.2__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.2.1.dev22 → modal-1.2.2}/PKG-INFO +2 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_clustered_functions.py +1 -3
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_functions.py +106 -97
- modal-1.2.2/modal/_grpc_client.py +171 -0
- modal-1.2.2/modal/_load_context.py +105 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_object.py +47 -18
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_output.py +3 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_resolver.py +21 -35
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/container_io_manager.py +21 -22
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/async_utils.py +12 -3
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/auth_token_manager.py +1 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/blob_utils.py +3 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/function_utils.py +1 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/grpc_utils.py +80 -51
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/mount_utils.py +26 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/task_command_router_client.py +3 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/app.py +10 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/app.pyi +3 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/config.py +3 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/container.py +1 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/dict.py +5 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/network_file_system.py +1 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/queues.py +5 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/secret.py +1 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/client.py +5 -115
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/client.pyi +2 -99
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cloud_bucket_mount.py +2 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cloud_bucket_mount.pyi +4 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cls.py +71 -33
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cls.pyi +3 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/config.py +1 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/container_process.py +4 -8
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/dict.py +26 -17
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/dict.pyi +2 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/environments.py +16 -8
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/environments.pyi +6 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/__init__.py +2 -3
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/flash.py +7 -12
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_io.py +13 -27
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/functions.pyi +10 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/image.py +47 -26
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/image.pyi +13 -9
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/io_streams.py +61 -91
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/io_streams.pyi +33 -95
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/mount.py +36 -26
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/mount.pyi +33 -7
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/network_file_system.py +19 -11
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/network_file_system.pyi +12 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/object.pyi +35 -8
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/parallel_map.py +29 -31
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/parallel_map.pyi +3 -9
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/proxy.py +14 -6
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/proxy.pyi +10 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/queue.py +31 -23
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/queue.pyi +12 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/runner.py +62 -58
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/runner.pyi +4 -3
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/sandbox.py +46 -60
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/sandbox.pyi +0 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/secret.py +61 -44
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/secret.pyi +21 -4
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/serving.py +7 -11
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/serving.pyi +7 -8
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/snapshot.py +11 -8
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/token_flow.py +1 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/volume.py +45 -29
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/volume.pyi +2 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api.proto +23 -9
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2.py +1031 -1015
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2.pyi +90 -19
- modal-1.2.2/modal_proto/modal_api_grpc.py +194 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_version/__init__.py +1 -1
- {modal-1.2.1.dev22 → modal-1.2.2}/pyproject.toml +1 -1
- modal-1.2.1.dev22/modal_proto/modal_api_grpc.py +0 -194
- {modal-1.2.1.dev22 → modal-1.2.2}/LICENSE +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/README.md +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/__main__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_billing.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_container_entrypoint.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_ipython.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_location.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_partial_function.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_pty.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_resources.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_serialization.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_traceback.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_tunnel.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_tunnel.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_type_manager.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/logger.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/package_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/_watcher.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/billing.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/README.md +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/builder/base-images.json +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/call_graph.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/_download.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/_traceback.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/app.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/cluster.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/entry_point.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/environment.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/import_refs.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/launch.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/profile.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/launch_instance_ssh.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/run_marimo.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/run.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/token.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/utils.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/cli/volume.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/container_process.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/exception.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/experimental/ipython.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_io.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/functions.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/gpu.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/object.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/output.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/partial_function.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/partial_function.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/py.typed +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/retries.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/running_app.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/schedule.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/scheduler_placement.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/snapshot.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/stream_type.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal/token_flow.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/__init__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/py.typed +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router.proto +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/modal_version/__main__.py +0 -0
- {modal-1.2.1.dev22 → modal-1.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -13,7 +13,7 @@ Classifier: Topic :: System :: Distributed Computing
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Requires-Python:
|
|
16
|
+
Requires-Python: <3.14,>=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: aiohttp
|
|
@@ -5,7 +5,6 @@ from dataclasses import dataclass
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
from modal._utils.async_utils import synchronize_api
|
|
8
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
9
8
|
from modal.client import _Client
|
|
10
9
|
from modal.exception import InvalidError
|
|
11
10
|
from modal_proto import api_pb2
|
|
@@ -61,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
|
|
|
61
60
|
os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
|
|
62
61
|
|
|
63
62
|
if world_size > 1:
|
|
64
|
-
resp
|
|
65
|
-
client.stub.TaskClusterHello,
|
|
63
|
+
resp = await client.stub.TaskClusterHello(
|
|
66
64
|
api_pb2.TaskClusterHelloRequest(
|
|
67
65
|
task_id=task_id,
|
|
68
66
|
container_ip=container_ip,
|
|
@@ -19,7 +19,8 @@ from synchronicity.combined_types import MethodWithAio
|
|
|
19
19
|
from modal_proto import api_pb2
|
|
20
20
|
from modal_proto.modal_api_grpc import ModalClientModal
|
|
21
21
|
|
|
22
|
-
from .
|
|
22
|
+
from ._load_context import LoadContext
|
|
23
|
+
from ._object import _Object, live_method, live_method_gen
|
|
23
24
|
from ._pty import get_pty_info
|
|
24
25
|
from ._resolver import Resolver
|
|
25
26
|
from ._resources import convert_fn_config_to_resources_config
|
|
@@ -53,7 +54,7 @@ from ._utils.function_utils import (
|
|
|
53
54
|
get_function_type,
|
|
54
55
|
is_async,
|
|
55
56
|
)
|
|
56
|
-
from ._utils.grpc_utils import
|
|
57
|
+
from ._utils.grpc_utils import Retry, RetryWarningMessage
|
|
57
58
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
58
59
|
from .call_graph import InputInfo, _reconstruct_call_graph
|
|
59
60
|
from .client import _Client
|
|
@@ -164,21 +165,22 @@ class _Invocation:
|
|
|
164
165
|
|
|
165
166
|
if from_spawn_map:
|
|
166
167
|
request.from_spawn_map = True
|
|
167
|
-
response = await
|
|
168
|
-
client.stub.FunctionMap,
|
|
168
|
+
response = await client.stub.FunctionMap(
|
|
169
169
|
request,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
retry=Retry(
|
|
171
|
+
max_retries=None,
|
|
172
|
+
max_delay=30.0,
|
|
173
|
+
warning_message=RetryWarningMessage(
|
|
174
|
+
message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
|
|
175
|
+
"more function calls. This may be due to hitting rate limits or function backlog limits.",
|
|
176
|
+
warning_interval=10,
|
|
177
|
+
errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
|
|
178
|
+
),
|
|
179
|
+
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
177
180
|
),
|
|
178
|
-
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
179
181
|
)
|
|
180
182
|
else:
|
|
181
|
-
response = await
|
|
183
|
+
response = await client.stub.FunctionMap(request)
|
|
182
184
|
|
|
183
185
|
function_call_id = response.function_call_id
|
|
184
186
|
if response.pipelined_inputs:
|
|
@@ -198,10 +200,7 @@ class _Invocation:
|
|
|
198
200
|
request_put = api_pb2.FunctionPutInputsRequest(
|
|
199
201
|
function_id=function_id, inputs=[item], function_call_id=function_call_id
|
|
200
202
|
)
|
|
201
|
-
inputs_response: api_pb2.FunctionPutInputsResponse = await
|
|
202
|
-
client.stub.FunctionPutInputs,
|
|
203
|
-
request_put,
|
|
204
|
-
)
|
|
203
|
+
inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
|
|
205
204
|
processed_inputs = inputs_response.inputs
|
|
206
205
|
if not processed_inputs:
|
|
207
206
|
raise Exception("Could not create function call - the input queue seems to be full")
|
|
@@ -243,10 +242,9 @@ class _Invocation:
|
|
|
243
242
|
start_idx=index,
|
|
244
243
|
end_idx=index,
|
|
245
244
|
)
|
|
246
|
-
response: api_pb2.FunctionGetOutputsResponse = await
|
|
247
|
-
self.stub.FunctionGetOutputs,
|
|
245
|
+
response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
|
|
248
246
|
request,
|
|
249
|
-
attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
247
|
+
retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
|
|
250
248
|
)
|
|
251
249
|
|
|
252
250
|
if len(response.outputs) > 0:
|
|
@@ -266,10 +264,7 @@ class _Invocation:
|
|
|
266
264
|
|
|
267
265
|
item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
|
|
268
266
|
request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
|
|
269
|
-
await
|
|
270
|
-
self.stub.FunctionRetryInputs,
|
|
271
|
-
request,
|
|
272
|
-
)
|
|
267
|
+
await self.stub.FunctionRetryInputs(request)
|
|
273
268
|
|
|
274
269
|
async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
|
|
275
270
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
|
@@ -373,10 +368,8 @@ class _Invocation:
|
|
|
373
368
|
start_idx=current_index,
|
|
374
369
|
end_idx=batch_end_index,
|
|
375
370
|
)
|
|
376
|
-
response: api_pb2.FunctionGetOutputsResponse = await
|
|
377
|
-
|
|
378
|
-
request,
|
|
379
|
-
attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
371
|
+
response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
|
|
372
|
+
request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
|
|
380
373
|
)
|
|
381
374
|
|
|
382
375
|
outputs = list(response.outputs)
|
|
@@ -448,7 +441,7 @@ class _InputPlaneInvocation:
|
|
|
448
441
|
)
|
|
449
442
|
|
|
450
443
|
metadata = await client.get_input_plane_metadata(input_plane_region)
|
|
451
|
-
response = await
|
|
444
|
+
response = await stub.AttemptStart(request, metadata=metadata)
|
|
452
445
|
attempt_token = response.attempt_token
|
|
453
446
|
|
|
454
447
|
return _InputPlaneInvocation(
|
|
@@ -468,10 +461,9 @@ class _InputPlaneInvocation:
|
|
|
468
461
|
requested_at=time.time(),
|
|
469
462
|
)
|
|
470
463
|
metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
|
|
471
|
-
await_response: api_pb2.AttemptAwaitResponse = await
|
|
472
|
-
self.stub.AttemptAwait,
|
|
464
|
+
await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
|
|
473
465
|
await_request,
|
|
474
|
-
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
466
|
+
retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
|
|
475
467
|
metadata=metadata,
|
|
476
468
|
)
|
|
477
469
|
|
|
@@ -511,11 +503,7 @@ class _InputPlaneInvocation:
|
|
|
511
503
|
input=self.input_item,
|
|
512
504
|
attempt_token=self.attempt_token,
|
|
513
505
|
)
|
|
514
|
-
retry_response = await
|
|
515
|
-
self.stub.AttemptRetry,
|
|
516
|
-
retry_request,
|
|
517
|
-
metadata=metadata,
|
|
518
|
-
)
|
|
506
|
+
retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
|
|
519
507
|
return retry_response.attempt_token
|
|
520
508
|
|
|
521
509
|
async def run_generator(self):
|
|
@@ -669,7 +657,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
669
657
|
@staticmethod
|
|
670
658
|
def from_local(
|
|
671
659
|
info: FunctionInfo,
|
|
672
|
-
app,
|
|
660
|
+
app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
|
|
673
661
|
image: _Image,
|
|
674
662
|
env: Optional[dict[str, Optional[str]]] = None,
|
|
675
663
|
secrets: Optional[Collection[_Secret]] = None,
|
|
@@ -895,12 +883,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
895
883
|
is_web_endpoint, is_generator, restrict_output
|
|
896
884
|
)
|
|
897
885
|
|
|
898
|
-
async def _preload(
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
assert
|
|
886
|
+
async def _preload(
|
|
887
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
888
|
+
):
|
|
889
|
+
assert load_context.app_id
|
|
902
890
|
req = api_pb2.FunctionPrecreateRequest(
|
|
903
|
-
app_id=
|
|
891
|
+
app_id=load_context.app_id,
|
|
904
892
|
function_name=info.function_name,
|
|
905
893
|
function_type=function_type,
|
|
906
894
|
existing_function_id=existing_object_id or "",
|
|
@@ -916,11 +904,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
916
904
|
elif webhook_config:
|
|
917
905
|
req.webhook_config.CopyFrom(webhook_config)
|
|
918
906
|
|
|
919
|
-
response = await
|
|
920
|
-
self._hydrate(response.function_id,
|
|
907
|
+
response = await load_context.client.stub.FunctionPrecreate(req)
|
|
908
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
921
909
|
|
|
922
|
-
async def _load(
|
|
923
|
-
|
|
910
|
+
async def _load(
|
|
911
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
912
|
+
):
|
|
924
913
|
with FunctionCreationStatus(resolver, tag) as function_creation_status:
|
|
925
914
|
timeout_secs = timeout
|
|
926
915
|
|
|
@@ -1116,18 +1105,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1116
1105
|
),
|
|
1117
1106
|
)
|
|
1118
1107
|
|
|
1119
|
-
assert
|
|
1108
|
+
assert load_context.app_id
|
|
1120
1109
|
assert (function_definition is None) != (function_data is None) # xor
|
|
1121
1110
|
request = api_pb2.FunctionCreateRequest(
|
|
1122
|
-
app_id=
|
|
1111
|
+
app_id=load_context.app_id,
|
|
1123
1112
|
function=function_definition,
|
|
1124
1113
|
function_data=function_data,
|
|
1125
1114
|
existing_function_id=existing_object_id or "",
|
|
1126
1115
|
)
|
|
1127
1116
|
try:
|
|
1128
|
-
response: api_pb2.FunctionCreateResponse = await
|
|
1129
|
-
resolver.client.stub.FunctionCreate, request
|
|
1130
|
-
)
|
|
1117
|
+
response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
|
|
1131
1118
|
except GRPCError as exc:
|
|
1132
1119
|
if exc.status == Status.INVALID_ARGUMENT:
|
|
1133
1120
|
raise InvalidError(exc.message)
|
|
@@ -1142,10 +1129,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1142
1129
|
serve_mounts = {m for m in all_mounts if m.is_local()}
|
|
1143
1130
|
serve_mounts |= image._serve_mounts
|
|
1144
1131
|
obj._serve_mounts = frozenset(serve_mounts)
|
|
1145
|
-
self._hydrate(response.function_id,
|
|
1132
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1146
1133
|
|
|
1147
1134
|
rep = f"Function({tag})"
|
|
1148
|
-
|
|
1135
|
+
# Pass a *reference* to the App's LoadContext - this is important since the App is
|
|
1136
|
+
# the only way to infer a LoadContext for an `@app.function`, and the App doesn't
|
|
1137
|
+
# get its client until *after* the Function is created.
|
|
1138
|
+
load_context = app._root_load_context if app else LoadContext.empty()
|
|
1139
|
+
obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
|
|
1149
1140
|
|
|
1150
1141
|
obj._raw_f = info.raw_f
|
|
1151
1142
|
obj._info = info
|
|
@@ -1187,7 +1178,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1187
1178
|
|
|
1188
1179
|
parent = self
|
|
1189
1180
|
|
|
1190
|
-
async def _load(
|
|
1181
|
+
async def _load(
|
|
1182
|
+
param_bound_func: _Function,
|
|
1183
|
+
resolver: Resolver,
|
|
1184
|
+
load_context: LoadContext,
|
|
1185
|
+
existing_object_id: Optional[str],
|
|
1186
|
+
):
|
|
1191
1187
|
if not parent.is_hydrated:
|
|
1192
1188
|
# While the base Object.hydrate() method appears to be idempotent, it's not always safe
|
|
1193
1189
|
await parent.hydrate()
|
|
@@ -1220,7 +1216,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1220
1216
|
param_bound_func._hydrate_from_other(parent)
|
|
1221
1217
|
return
|
|
1222
1218
|
|
|
1223
|
-
environment_name = _get_environment_name(None, resolver)
|
|
1224
1219
|
assert parent is not None and parent.is_hydrated
|
|
1225
1220
|
|
|
1226
1221
|
if options:
|
|
@@ -1260,11 +1255,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1260
1255
|
function_id=parent.object_id,
|
|
1261
1256
|
serialized_params=serialized_params,
|
|
1262
1257
|
function_options=options_pb,
|
|
1263
|
-
environment_name=environment_name
|
|
1258
|
+
environment_name=load_context.environment_name
|
|
1264
1259
|
or "", # TODO: investigate shouldn't environment name always be specified here?
|
|
1265
1260
|
)
|
|
1266
1261
|
|
|
1267
|
-
response = await
|
|
1262
|
+
response = await parent._client.stub.FunctionBindParams(req)
|
|
1268
1263
|
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
|
1269
1264
|
|
|
1270
1265
|
def _deps():
|
|
@@ -1277,7 +1272,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1277
1272
|
return [dep for dep in all_deps if not dep.is_hydrated]
|
|
1278
1273
|
return []
|
|
1279
1274
|
|
|
1280
|
-
fun: _Function = _Function._from_loader(
|
|
1275
|
+
fun: _Function = _Function._from_loader(
|
|
1276
|
+
_load,
|
|
1277
|
+
"Function(parametrized)",
|
|
1278
|
+
hydrate_lazily=True,
|
|
1279
|
+
deps=_deps,
|
|
1280
|
+
load_context_overrides=self._load_context_overrides,
|
|
1281
|
+
)
|
|
1281
1282
|
|
|
1282
1283
|
fun._info = self._info
|
|
1283
1284
|
fun._obj = obj
|
|
@@ -1328,7 +1329,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1328
1329
|
scaledown_window=scaledown_window,
|
|
1329
1330
|
)
|
|
1330
1331
|
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
|
|
1331
|
-
await
|
|
1332
|
+
await self.client.stub.FunctionUpdateSchedulingParams(request)
|
|
1332
1333
|
|
|
1333
1334
|
# One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
|
|
1334
1335
|
# and then we could return them here (would need some ad hoc dataclass, which I don't love)
|
|
@@ -1375,34 +1376,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1375
1376
|
cls,
|
|
1376
1377
|
app_name: str,
|
|
1377
1378
|
name: str,
|
|
1378
|
-
|
|
1379
|
-
|
|
1379
|
+
*,
|
|
1380
|
+
load_context_overrides: LoadContext,
|
|
1380
1381
|
):
|
|
1381
1382
|
# internal function lookup implementation that allows lookup of class "service functions"
|
|
1382
1383
|
# in addition to non-class functions
|
|
1383
|
-
async def _load_remote(
|
|
1384
|
-
|
|
1384
|
+
async def _load_remote(
|
|
1385
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
1386
|
+
):
|
|
1385
1387
|
request = api_pb2.FunctionGetRequest(
|
|
1386
1388
|
app_name=app_name,
|
|
1387
1389
|
object_tag=name,
|
|
1388
|
-
environment_name=
|
|
1390
|
+
environment_name=load_context.environment_name,
|
|
1389
1391
|
)
|
|
1390
1392
|
try:
|
|
1391
|
-
response = await
|
|
1393
|
+
response = await load_context.client.stub.FunctionGet(request)
|
|
1392
1394
|
except NotFoundError as exc:
|
|
1393
1395
|
# refine the error message
|
|
1394
|
-
env_context =
|
|
1396
|
+
env_context = (
|
|
1397
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
1398
|
+
)
|
|
1395
1399
|
raise NotFoundError(
|
|
1396
1400
|
f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
1397
1401
|
) from None
|
|
1398
1402
|
|
|
1399
1403
|
print_server_warnings(response.server_warnings)
|
|
1400
1404
|
|
|
1401
|
-
self._hydrate(response.function_id,
|
|
1405
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1402
1406
|
|
|
1403
|
-
environment_rep =
|
|
1407
|
+
environment_rep = (
|
|
1408
|
+
f", environment_name={load_context_overrides.environment_name!r}"
|
|
1409
|
+
if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
|
|
1410
|
+
else ""
|
|
1411
|
+
)
|
|
1404
1412
|
rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
|
|
1405
|
-
return cls._from_loader(
|
|
1413
|
+
return cls._from_loader(
|
|
1414
|
+
_load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1415
|
+
)
|
|
1406
1416
|
|
|
1407
1417
|
@classmethod
|
|
1408
1418
|
def from_name(
|
|
@@ -1412,6 +1422,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1412
1422
|
*,
|
|
1413
1423
|
namespace=None, # mdmd:line-hidden
|
|
1414
1424
|
environment_name: Optional[str] = None,
|
|
1425
|
+
client: Optional[_Client] = None,
|
|
1415
1426
|
) -> "_Function":
|
|
1416
1427
|
"""Reference a Function from a deployed App by its name.
|
|
1417
1428
|
|
|
@@ -1435,7 +1446,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1435
1446
|
)
|
|
1436
1447
|
|
|
1437
1448
|
warn_if_passing_namespace(namespace, "modal.Function.from_name")
|
|
1438
|
-
return cls._from_name(
|
|
1449
|
+
return cls._from_name(
|
|
1450
|
+
app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
|
|
1451
|
+
)
|
|
1439
1452
|
|
|
1440
1453
|
@property
|
|
1441
1454
|
def tag(self) -> str:
|
|
@@ -1658,8 +1671,8 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1658
1671
|
input_queue,
|
|
1659
1672
|
self.client,
|
|
1660
1673
|
)
|
|
1661
|
-
|
|
1662
|
-
fc
|
|
1674
|
+
fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
|
|
1675
|
+
fc._num_inputs = num_inputs # set the cached value of num_inputs
|
|
1663
1676
|
return fc
|
|
1664
1677
|
|
|
1665
1678
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
@@ -1888,10 +1901,9 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1888
1901
|
@live_method
|
|
1889
1902
|
async def get_current_stats(self) -> FunctionStats:
|
|
1890
1903
|
"""Return a `FunctionStats` object describing the current function's queue and runner counts."""
|
|
1891
|
-
resp = await
|
|
1892
|
-
self.client.stub.FunctionGetCurrentStats,
|
|
1904
|
+
resp = await self.client.stub.FunctionGetCurrentStats(
|
|
1893
1905
|
api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
|
|
1894
|
-
total_timeout=10.0,
|
|
1906
|
+
retry=Retry(total_timeout=10.0),
|
|
1895
1907
|
)
|
|
1896
1908
|
return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
|
|
1897
1909
|
|
|
@@ -1929,19 +1941,16 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1929
1941
|
def _invocation(self):
|
|
1930
1942
|
return _Invocation(self.client.stub, self.object_id, self.client)
|
|
1931
1943
|
|
|
1932
|
-
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
1933
|
-
if not metadata:
|
|
1934
|
-
return
|
|
1935
|
-
assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
|
|
1936
|
-
self._num_inputs = metadata.num_inputs
|
|
1937
|
-
|
|
1938
1944
|
@live_method
|
|
1939
1945
|
async def num_inputs(self) -> int:
|
|
1940
1946
|
"""Get the number of inputs in the function call."""
|
|
1941
|
-
|
|
1942
|
-
|
|
1947
|
+
if self._num_inputs is None:
|
|
1948
|
+
request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
|
|
1949
|
+
resp = await self.client.stub.FunctionCallFromId(request)
|
|
1950
|
+
self._num_inputs = resp.num_inputs # cached
|
|
1943
1951
|
return self._num_inputs
|
|
1944
1952
|
|
|
1953
|
+
@live_method
|
|
1945
1954
|
async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
|
|
1946
1955
|
"""Get the result of the index-th input of the function call.
|
|
1947
1956
|
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
@@ -1985,6 +1994,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1985
1994
|
async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
|
|
1986
1995
|
yield item
|
|
1987
1996
|
|
|
1997
|
+
@live_method
|
|
1988
1998
|
async def get_call_graph(self) -> list[InputInfo]:
|
|
1989
1999
|
"""Returns a structure representing the call graph from a given root
|
|
1990
2000
|
call ID, along with the status of execution for each node.
|
|
@@ -1994,9 +2004,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1994
2004
|
"""
|
|
1995
2005
|
assert self._client and self._client.stub
|
|
1996
2006
|
request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
|
|
1997
|
-
response = await
|
|
2007
|
+
response = await self._client.stub.FunctionGetCallGraph(request)
|
|
1998
2008
|
return _reconstruct_call_graph(response)
|
|
1999
2009
|
|
|
2010
|
+
@live_method
|
|
2000
2011
|
async def cancel(
|
|
2001
2012
|
self,
|
|
2002
2013
|
# if true, containers running the inputs are forcibly terminated
|
|
@@ -2012,7 +2023,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2012
2023
|
function_call_id=self.object_id, terminate_containers=terminate_containers
|
|
2013
2024
|
)
|
|
2014
2025
|
assert self._client and self._client.stub
|
|
2015
|
-
await
|
|
2026
|
+
await self._client.stub.FunctionCallCancel(request)
|
|
2016
2027
|
|
|
2017
2028
|
@staticmethod
|
|
2018
2029
|
async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
|
|
@@ -2034,20 +2045,18 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2034
2045
|
if you no longer have access to the original object returned from `Function.spawn`.
|
|
2035
2046
|
|
|
2036
2047
|
"""
|
|
2037
|
-
if client is None:
|
|
2038
|
-
client = await _Client.from_env()
|
|
2039
2048
|
|
|
2040
|
-
async def _load(
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2049
|
+
async def _load(
|
|
2050
|
+
self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
2051
|
+
):
|
|
2052
|
+
# this loader doesn't do anything in practice, but it will get the client from the load_context
|
|
2053
|
+
self._hydrate(function_call_id, load_context.client, None)
|
|
2044
2054
|
|
|
2045
2055
|
rep = f"FunctionCall.from_id({function_call_id!r})"
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
return fc
|
|
2056
|
+
|
|
2057
|
+
return _FunctionCall._from_loader(
|
|
2058
|
+
_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
|
|
2059
|
+
)
|
|
2051
2060
|
|
|
2052
2061
|
@staticmethod
|
|
2053
2062
|
async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Collection, Generic, Literal, Mapping, Optional, TypeVar, Union, overload
|
|
3
|
+
|
|
4
|
+
import grpclib.client
|
|
5
|
+
from google.protobuf.message import Message
|
|
6
|
+
from grpclib import GRPCError, Status
|
|
7
|
+
|
|
8
|
+
from ._traceback import suppress_tb_frames
|
|
9
|
+
from ._utils.grpc_utils import Retry, _retry_transient_errors
|
|
10
|
+
from .config import config, logger
|
|
11
|
+
from .exception import InvalidError, NotFoundError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .client import _Client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_Value = Union[str, bytes]
|
|
18
|
+
_MetadataLike = Union[Mapping[str, _Value], Collection[tuple[str, _Value]]]
|
|
19
|
+
RequestType = TypeVar("RequestType", bound=Message)
|
|
20
|
+
ResponseType = TypeVar("ResponseType", bound=Message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class grpc_error_converter:
|
|
24
|
+
def __enter__(self):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
|
|
28
|
+
# skip all internal frames from grpclib
|
|
29
|
+
use_full_traceback = config.get("traceback")
|
|
30
|
+
with suppress_tb_frames(1):
|
|
31
|
+
if isinstance(exc, GRPCError):
|
|
32
|
+
if exc.status == Status.NOT_FOUND:
|
|
33
|
+
if use_full_traceback:
|
|
34
|
+
raise NotFoundError(exc.message)
|
|
35
|
+
else:
|
|
36
|
+
raise NotFoundError(exc.message) from None # from None to skip the grpc-internal cause
|
|
37
|
+
|
|
38
|
+
if not use_full_traceback:
|
|
39
|
+
# just include the frame in grpclib that actually raises the GRPCError
|
|
40
|
+
tb = exc.__traceback__
|
|
41
|
+
while tb.tb_next:
|
|
42
|
+
tb = tb.tb_next
|
|
43
|
+
exc.with_traceback(tb)
|
|
44
|
+
raise exc from None # from None to skip the grpc-internal cause
|
|
45
|
+
raise exc
|
|
46
|
+
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_DEFAULT_RETRY = Retry()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
|
|
54
|
+
# Calls a grpclib.UnaryUnaryMethod using a specific Client instance, respecting
|
|
55
|
+
# if that client is closed etc. and possibly introducing Modal-specific retry logic
|
|
56
|
+
wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]
|
|
57
|
+
client: "_Client"
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType],
|
|
62
|
+
client: "_Client",
|
|
63
|
+
server_url: str,
|
|
64
|
+
):
|
|
65
|
+
self.wrapped_method = wrapped_method
|
|
66
|
+
self.client = client
|
|
67
|
+
self.server_url = server_url
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
return self.wrapped_method.name
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
async def __call__(
|
|
75
|
+
self,
|
|
76
|
+
req: RequestType,
|
|
77
|
+
*,
|
|
78
|
+
retry: Retry = _DEFAULT_RETRY,
|
|
79
|
+
timeout: None = None,
|
|
80
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
81
|
+
) -> ResponseType: ...
|
|
82
|
+
|
|
83
|
+
@overload
|
|
84
|
+
async def __call__(
|
|
85
|
+
self,
|
|
86
|
+
req: RequestType,
|
|
87
|
+
*,
|
|
88
|
+
retry: None,
|
|
89
|
+
timeout: Optional[float] = None,
|
|
90
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
91
|
+
) -> ResponseType: ...
|
|
92
|
+
|
|
93
|
+
async def __call__(
|
|
94
|
+
self,
|
|
95
|
+
req: RequestType,
|
|
96
|
+
*,
|
|
97
|
+
retry: Optional[Retry] = _DEFAULT_RETRY,
|
|
98
|
+
timeout: Optional[float] = None,
|
|
99
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
100
|
+
) -> ResponseType:
|
|
101
|
+
with suppress_tb_frames(1):
|
|
102
|
+
if timeout is not None and retry is not None:
|
|
103
|
+
raise InvalidError("Retry must be None when timeout is set")
|
|
104
|
+
|
|
105
|
+
if retry is None:
|
|
106
|
+
return await self.direct(req, timeout=timeout, metadata=metadata)
|
|
107
|
+
|
|
108
|
+
return await _retry_transient_errors(
|
|
109
|
+
self, # type: ignore
|
|
110
|
+
req,
|
|
111
|
+
retry=retry,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def direct(
|
|
116
|
+
self,
|
|
117
|
+
req: RequestType,
|
|
118
|
+
*,
|
|
119
|
+
timeout: Optional[float] = None,
|
|
120
|
+
metadata: Optional[_MetadataLike] = None,
|
|
121
|
+
) -> ResponseType:
|
|
122
|
+
from .client import _Client
|
|
123
|
+
|
|
124
|
+
if self.client._snapshotted:
|
|
125
|
+
logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
|
|
126
|
+
self.client = await _Client.from_env()
|
|
127
|
+
|
|
128
|
+
# Note: We override the grpclib method's channel (see grpclib's code [1]). I think this is fine
|
|
129
|
+
# since grpclib's code doesn't seem to change very much, but we could also recreate the
|
|
130
|
+
# grpclib stub if we aren't comfortable with this. The downside is then we need to cache
|
|
131
|
+
# the grpclib stub so the rest of our code becomes a bit more complicated.
|
|
132
|
+
#
|
|
133
|
+
# We need to override the channel because after the process is forked or the client is
|
|
134
|
+
# snapshotted, the existing channel may be stale / unusable.
|
|
135
|
+
#
|
|
136
|
+
# [1]: https://github.com/vmagamedov/grpclib/blob/62f968a4c84e3f64e6966097574ff0a59969ea9b/grpclib/client.py#L844
|
|
137
|
+
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
138
|
+
with suppress_tb_frames(1), grpc_error_converter():
|
|
139
|
+
return await self.client._call_unary(self.wrapped_method, req, timeout=timeout, metadata=metadata)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
143
|
+
wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType]
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType],
|
|
148
|
+
client: "_Client",
|
|
149
|
+
server_url: str,
|
|
150
|
+
):
|
|
151
|
+
self.wrapped_method = wrapped_method
|
|
152
|
+
self.client = client
|
|
153
|
+
self.server_url = server_url
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def name(self) -> str:
|
|
157
|
+
return self.wrapped_method.name
|
|
158
|
+
|
|
159
|
+
async def unary_stream(
|
|
160
|
+
self,
|
|
161
|
+
request,
|
|
162
|
+
metadata: Optional[Any] = None,
|
|
163
|
+
):
|
|
164
|
+
from .client import _Client
|
|
165
|
+
|
|
166
|
+
if self.client._snapshotted:
|
|
167
|
+
logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
|
|
168
|
+
self.client = await _Client.from_env()
|
|
169
|
+
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
170
|
+
async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
|
|
171
|
+
yield response
|