modAL 1.2.3__tar.gz → 1.2.3.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.2.3 → modal-1.2.3.dev0}/PKG-INFO +1 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_functions.py +4 -10
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_load_context.py +1 -2
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_object.py +6 -12
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_partial_function.py +1 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/async_utils.py +5 -15
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/grpc_utils.py +24 -157
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/app.py +21 -47
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/app.pyi +4 -8
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/run.py +1 -24
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/client.py +9 -9
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/client.pyi +17 -8
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cloud_bucket_mount.py +1 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cls.py +4 -6
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/config.py +0 -5
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/functions.pyi +8 -8
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/image.py +4 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/io_streams.py +32 -101
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/io_streams.pyi +5 -30
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/sandbox.py +21 -8
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/sandbox.pyi +9 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api.proto +0 -29
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_grpc.py +0 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2.py +1021 -1052
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2.pyi +6 -96
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_version/__init__.py +1 -1
- {modal-1.2.3 → modal-1.2.3.dev0}/LICENSE +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/README.md +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/__main__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_billing.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_clustered_functions.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_container_entrypoint.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_grpc_client.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_ipython.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_location.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_output.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_pty.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_resolver.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_resources.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_serialization.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_traceback.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_tunnel.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_tunnel.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_type_manager.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/blob_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/function_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/logger.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/package_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/_watcher.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/billing.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/README.md +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/builder/base-images.json +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/call_graph.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/_download.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/_traceback.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/app.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/cluster.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/config.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/container.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/dict.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/entry_point.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/environment.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/import_refs.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/launch.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/network_file_system.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/profile.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/launch_instance_ssh.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/run_marimo.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/queues.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/secret.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/token.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/utils.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cli/volume.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/cls.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/container_process.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/container_process.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/dict.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/dict.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/environments.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/environments.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/exception.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/flash.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/experimental/ipython.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_io.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_io.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/functions.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/gpu.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/image.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/mount.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/mount.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/network_file_system.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/network_file_system.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/object.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/object.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/output.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/parallel_map.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/parallel_map.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/partial_function.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/partial_function.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/proxy.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/proxy.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/py.typed +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/queue.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/queue.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/retries.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/runner.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/runner.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/running_app.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/schedule.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/scheduler_placement.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/secret.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/secret.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/serving.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/serving.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/snapshot.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/snapshot.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/stream_type.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/token_flow.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/token_flow.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/volume.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal/volume.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/__init__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/py.typed +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router.proto +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/modal_version/__main__.py +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/pyproject.toml +0 -0
- {modal-1.2.3 → modal-1.2.3.dev0}/setup.cfg +0 -0
|
@@ -90,6 +90,7 @@ from .parallel_map import (
|
|
|
90
90
|
from .proxy import _Proxy
|
|
91
91
|
from .retries import Retries, RetryManager
|
|
92
92
|
from .schedule import Schedule
|
|
93
|
+
from .scheduler_placement import SchedulerPlacement
|
|
93
94
|
from .secret import _Secret
|
|
94
95
|
from .volume import _Volume
|
|
95
96
|
|
|
@@ -536,7 +537,6 @@ class _InputPlaneInvocation:
|
|
|
536
537
|
async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
|
|
537
538
|
if not input_plane_region:
|
|
538
539
|
return []
|
|
539
|
-
assert client._auth_token_manager, "Client is not open"
|
|
540
540
|
token = await client._auth_token_manager.get_token()
|
|
541
541
|
return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
|
|
542
542
|
|
|
@@ -589,7 +589,7 @@ class _FunctionSpec:
|
|
|
589
589
|
cpu: Optional[Union[float, tuple[float, float]]]
|
|
590
590
|
memory: Optional[Union[int, tuple[int, int]]]
|
|
591
591
|
ephemeral_disk: Optional[int]
|
|
592
|
-
scheduler_placement: Optional[
|
|
592
|
+
scheduler_placement: Optional[SchedulerPlacement]
|
|
593
593
|
proxy: Optional[_Proxy]
|
|
594
594
|
|
|
595
595
|
|
|
@@ -683,8 +683,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
683
683
|
batch_max_size: Optional[int] = None,
|
|
684
684
|
batch_wait_ms: Optional[int] = None,
|
|
685
685
|
cloud: Optional[str] = None,
|
|
686
|
-
|
|
687
|
-
nonpreemptible: bool = False,
|
|
686
|
+
scheduler_placement: Optional[SchedulerPlacement] = None,
|
|
688
687
|
is_builder_function: bool = False,
|
|
689
688
|
is_auto_snapshot: bool = False,
|
|
690
689
|
enable_memory_snapshot: bool = False,
|
|
@@ -747,11 +746,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
747
746
|
if env:
|
|
748
747
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
749
748
|
|
|
750
|
-
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
751
|
-
if region or nonpreemptible:
|
|
752
|
-
regions = [region] if isinstance(region, str) else (list(region) if region else None)
|
|
753
|
-
scheduler_placement = api_pb2.SchedulerPlacement(regions=regions, nonpreemptible=nonpreemptible)
|
|
754
|
-
|
|
755
749
|
function_spec = _FunctionSpec(
|
|
756
750
|
mounts=all_mounts,
|
|
757
751
|
secrets=secrets,
|
|
@@ -1024,7 +1018,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1024
1018
|
untrusted=restrict_modal_access,
|
|
1025
1019
|
max_inputs=max_inputs or 0,
|
|
1026
1020
|
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
|
|
1027
|
-
scheduler_placement=scheduler_placement,
|
|
1021
|
+
scheduler_placement=scheduler_placement.proto if scheduler_placement else None,
|
|
1028
1022
|
is_class=info.is_service_class(),
|
|
1029
1023
|
class_parameter_info=info.class_parameter_info(),
|
|
1030
1024
|
i6pn_enabled=i6pn_enabled,
|
|
@@ -66,9 +66,8 @@ class LoadContext:
|
|
|
66
66
|
|
|
67
67
|
Returns a new instance (no in place mutation)"""
|
|
68
68
|
|
|
69
|
-
is_valid_client = self._client is not None and not self._client._snapshotted
|
|
70
69
|
return LoadContext(
|
|
71
|
-
client=
|
|
70
|
+
client=await _Client.from_env() if self._client is None else self.client,
|
|
72
71
|
environment_name=self._environment_name or config.get("environment") or "",
|
|
73
72
|
app_id=self._app_id,
|
|
74
73
|
)
|
|
@@ -43,7 +43,7 @@ class _Object:
|
|
|
43
43
|
_prefix_to_type: ClassVar[dict[str, type]] = {}
|
|
44
44
|
|
|
45
45
|
# For constructors
|
|
46
|
-
_load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
46
|
+
_load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
47
47
|
_preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
48
48
|
_rep: str
|
|
49
49
|
_is_another_app: bool
|
|
@@ -308,17 +308,11 @@ class _Object:
|
|
|
308
308
|
# memory snapshots capture references which must be rehydrated
|
|
309
309
|
# on restore to handle staleness.
|
|
310
310
|
logger.debug(f"rehydrating {self} after snapshot")
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
root_load_context = LoadContext.empty()
|
|
317
|
-
resolver = Resolver()
|
|
318
|
-
await resolver.load(typing.cast(_Object, self), root_load_context)
|
|
319
|
-
else:
|
|
320
|
-
logger.debug(f"reloading non-lazy {self} by replacing client")
|
|
321
|
-
self._client = client or await _Client.from_env()
|
|
311
|
+
self._is_hydrated = False # un-hydrate and re-resolve
|
|
312
|
+
# Set the client on LoadContext before loading
|
|
313
|
+
root_load_context = LoadContext(client=client)
|
|
314
|
+
resolver = Resolver()
|
|
315
|
+
await resolver.load(typing.cast(_Object, self), root_load_context)
|
|
322
316
|
self._is_rehydrated = True
|
|
323
317
|
logger.debug(f"rehydrated {self} with client {id(self.client)}")
|
|
324
318
|
elif not self._hydrate_lazily:
|
|
@@ -219,7 +219,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
219
219
|
# of the type PartialFunction and this descriptor would be triggered when accessing it,
|
|
220
220
|
#
|
|
221
221
|
# However, modal classes are *actually* Cls instances (which isn't reflected in type checkers
|
|
222
|
-
# due to Python's lack of type
|
|
222
|
+
# due to Python's lack of type chekcing intersection types), so at runtime the Cls instance would
|
|
223
223
|
# use its __getattr__ rather than this descriptor.
|
|
224
224
|
assert self.raw_f is not None # Should only be relevant in a method context
|
|
225
225
|
k = self.raw_f.__name__
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import asyncio
|
|
3
3
|
import concurrent.futures
|
|
4
|
-
import contextlib
|
|
5
4
|
import functools
|
|
6
5
|
import inspect
|
|
7
6
|
import itertools
|
|
8
7
|
import sys
|
|
9
8
|
import time
|
|
10
9
|
import typing
|
|
11
|
-
import warnings
|
|
12
10
|
from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Iterable, Iterator
|
|
13
11
|
from contextlib import asynccontextmanager
|
|
14
12
|
from dataclasses import dataclass
|
|
@@ -136,8 +134,7 @@ class TaskContext:
|
|
|
136
134
|
_loops: set[asyncio.Task]
|
|
137
135
|
|
|
138
136
|
def __init__(self, grace: Optional[float] = None):
|
|
139
|
-
self._grace = grace
|
|
140
|
-
self._cancellation_grace: float = 1.0 # extra graceperiod for the cancellation itself to "bubble up"
|
|
137
|
+
self._grace = grace
|
|
141
138
|
self._loops = set()
|
|
142
139
|
|
|
143
140
|
async def start(self):
|
|
@@ -169,29 +166,22 @@ class TaskContext:
|
|
|
169
166
|
# still needs to be handled
|
|
170
167
|
# (https://stackoverflow.com/a/63356323/2475114)
|
|
171
168
|
if gather_future:
|
|
172
|
-
|
|
169
|
+
try:
|
|
173
170
|
await gather_future
|
|
171
|
+
except asyncio.CancelledError:
|
|
172
|
+
pass
|
|
174
173
|
|
|
175
|
-
cancelled_tasks: list[asyncio.Task] = []
|
|
176
174
|
for task in self._tasks:
|
|
177
175
|
if task.done() and not task.cancelled():
|
|
178
176
|
# Raise any exceptions if they happened.
|
|
179
177
|
# Only tasks without a done_callback will still be present in self._tasks
|
|
180
178
|
task.result()
|
|
181
179
|
|
|
182
|
-
if task.done():
|
|
180
|
+
if task.done() or task in self._loops: # Note: Legacy code, we can probably cancel loops.
|
|
183
181
|
continue
|
|
184
182
|
|
|
185
183
|
# Cancel any remaining unfinished tasks.
|
|
186
184
|
task.cancel()
|
|
187
|
-
cancelled_tasks.append(task)
|
|
188
|
-
|
|
189
|
-
cancellation_gather = asyncio.gather(*cancelled_tasks, return_exceptions=True)
|
|
190
|
-
try:
|
|
191
|
-
await asyncio.wait_for(cancellation_gather, timeout=self._cancellation_grace)
|
|
192
|
-
except asyncio.TimeoutError:
|
|
193
|
-
warnings.warn(f"Internal warning: Tasks did not cancel in a timely manner: {cancelled_tasks}")
|
|
194
|
-
|
|
195
185
|
await asyncio.sleep(0) # wake up coroutines waiting for cancellations
|
|
196
186
|
|
|
197
187
|
async def __aexit__(self, exc_type, value, tb):
|
|
@@ -9,8 +9,7 @@ import urllib.parse
|
|
|
9
9
|
import uuid
|
|
10
10
|
from collections.abc import AsyncIterator
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from
|
|
13
|
-
from typing import Any, Optional, Sequence, TypeVar
|
|
12
|
+
from typing import Any, Optional, TypeVar
|
|
14
13
|
|
|
15
14
|
import grpclib.client
|
|
16
15
|
import grpclib.config
|
|
@@ -18,18 +17,14 @@ import grpclib.events
|
|
|
18
17
|
import grpclib.protocol
|
|
19
18
|
import grpclib.stream
|
|
20
19
|
from google.protobuf.message import Message
|
|
21
|
-
from google.protobuf.symbol_database import SymbolDatabase
|
|
22
20
|
from grpclib import GRPCError, Status
|
|
23
|
-
from grpclib.encoding.base import StatusDetailsCodecBase
|
|
24
21
|
from grpclib.exceptions import StreamTerminatedError
|
|
25
22
|
from grpclib.protocol import H2Protocol
|
|
26
23
|
|
|
27
24
|
from modal.exception import AuthError, ConnectionError
|
|
28
|
-
from modal_proto import api_pb2
|
|
29
25
|
from modal_version import __version__
|
|
30
26
|
|
|
31
27
|
from .._traceback import suppress_tb_frames
|
|
32
|
-
from ..config import config
|
|
33
28
|
from .async_utils import retry
|
|
34
29
|
from .logger import logger
|
|
35
30
|
|
|
@@ -73,7 +68,6 @@ RETRYABLE_GRPC_STATUS_CODES = [
|
|
|
73
68
|
Status.INTERNAL,
|
|
74
69
|
Status.UNKNOWN,
|
|
75
70
|
]
|
|
76
|
-
SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
|
|
77
71
|
|
|
78
72
|
|
|
79
73
|
@dataclass
|
|
@@ -113,56 +107,6 @@ class ConnectionManager:
|
|
|
113
107
|
self._channels.clear()
|
|
114
108
|
|
|
115
109
|
|
|
116
|
-
@cache
|
|
117
|
-
def _sym_db() -> SymbolDatabase:
|
|
118
|
-
from google.protobuf.symbol_database import Default
|
|
119
|
-
|
|
120
|
-
return Default()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
|
|
124
|
-
"""grpclib compatible details codec.
|
|
125
|
-
|
|
126
|
-
The server can encode the details using `google.rpc.Status` using grpclib's default codec and this custom codec
|
|
127
|
-
can decode it into a `api_pb2.RPCStatus`.
|
|
128
|
-
"""
|
|
129
|
-
|
|
130
|
-
def encode(
|
|
131
|
-
self,
|
|
132
|
-
status: Status,
|
|
133
|
-
message: Optional[str],
|
|
134
|
-
details: Optional[Sequence[Message]],
|
|
135
|
-
) -> bytes:
|
|
136
|
-
details_proto = api_pb2.RPCStatus(code=status.value, message=message or "")
|
|
137
|
-
if details is not None:
|
|
138
|
-
for detail in details:
|
|
139
|
-
detail_container = details_proto.details.add()
|
|
140
|
-
detail_container.Pack(detail)
|
|
141
|
-
return details_proto.SerializeToString()
|
|
142
|
-
|
|
143
|
-
def decode(
|
|
144
|
-
self,
|
|
145
|
-
status: Status,
|
|
146
|
-
message: Optional[str],
|
|
147
|
-
data: bytes,
|
|
148
|
-
) -> Any:
|
|
149
|
-
sym_db = _sym_db()
|
|
150
|
-
details_proto = api_pb2.RPCStatus.FromString(data)
|
|
151
|
-
|
|
152
|
-
details = []
|
|
153
|
-
for detail_container in details_proto.details:
|
|
154
|
-
# If we do not know how to decode an emssage, we'll ignore it.
|
|
155
|
-
with contextlib.suppress(Exception):
|
|
156
|
-
msg_type = sym_db.GetSymbol(detail_container.TypeName())
|
|
157
|
-
detail = msg_type()
|
|
158
|
-
detail_container.Unpack(detail)
|
|
159
|
-
details.append(detail)
|
|
160
|
-
return details
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
custom_detail_codec = CustomProtoStatusDetailsCodec()
|
|
164
|
-
|
|
165
|
-
|
|
166
110
|
def create_channel(
|
|
167
111
|
server_url: str,
|
|
168
112
|
metadata: dict[str, str] = {},
|
|
@@ -181,7 +125,7 @@ def create_channel(
|
|
|
181
125
|
)
|
|
182
126
|
|
|
183
127
|
if o.scheme == "unix":
|
|
184
|
-
channel = grpclib.client.Channel(path=o.path, config=config
|
|
128
|
+
channel = grpclib.client.Channel(path=o.path, config=config) # probably pointless to use a pool ever
|
|
185
129
|
elif o.scheme in ("http", "https"):
|
|
186
130
|
target = o.netloc
|
|
187
131
|
parts = target.split(":")
|
|
@@ -189,7 +133,7 @@ def create_channel(
|
|
|
189
133
|
ssl = o.scheme.endswith("s")
|
|
190
134
|
host = parts[0]
|
|
191
135
|
port = int(parts[1]) if len(parts) == 2 else 443 if ssl else 80
|
|
192
|
-
channel = grpclib.client.Channel(host, port, ssl=ssl, config=config
|
|
136
|
+
channel = grpclib.client.Channel(host, port, ssl=ssl, config=config)
|
|
193
137
|
else:
|
|
194
138
|
raise Exception(f"Unknown scheme: {o.scheme}")
|
|
195
139
|
|
|
@@ -253,50 +197,6 @@ async def retry_transient_errors(
|
|
|
253
197
|
return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
|
|
254
198
|
|
|
255
199
|
|
|
256
|
-
def get_server_retry_policy(exc: Exception) -> Optional[api_pb2.RPCRetryPolicy]:
|
|
257
|
-
"""Get server retry policy."""
|
|
258
|
-
if not isinstance(exc, GRPCError) or not exc.details:
|
|
259
|
-
return None
|
|
260
|
-
|
|
261
|
-
# Server should not set multiple retry instructions, but if there is more than one, pick the first one
|
|
262
|
-
for entry in exc.details:
|
|
263
|
-
if isinstance(entry, api_pb2.RPCRetryPolicy):
|
|
264
|
-
return entry
|
|
265
|
-
return None
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def process_exception_before_retry(
|
|
269
|
-
exc: Exception,
|
|
270
|
-
final_attempt: bool,
|
|
271
|
-
fn_name: str,
|
|
272
|
-
n_retries: int,
|
|
273
|
-
delay: float,
|
|
274
|
-
idempotency_key: str,
|
|
275
|
-
):
|
|
276
|
-
"""Process exception before retry, used by `_retry_transient_errors`."""
|
|
277
|
-
with suppress_tb_frames(1):
|
|
278
|
-
if final_attempt:
|
|
279
|
-
logger.debug(
|
|
280
|
-
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
|
|
281
|
-
)
|
|
282
|
-
if isinstance(exc, OSError):
|
|
283
|
-
raise ConnectionError(str(exc))
|
|
284
|
-
elif isinstance(exc, asyncio.TimeoutError):
|
|
285
|
-
raise ConnectionError(str(exc))
|
|
286
|
-
else:
|
|
287
|
-
raise exc
|
|
288
|
-
|
|
289
|
-
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
290
|
-
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
291
|
-
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
292
|
-
# TODO: update to newer version (>=0.4.8) once stable
|
|
293
|
-
# Also be sure to remove the AttributeError from the set of exceptions
|
|
294
|
-
# we handle in the retry logic once we drop this check!
|
|
295
|
-
raise exc
|
|
296
|
-
|
|
297
|
-
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
|
|
298
|
-
|
|
299
|
-
|
|
300
200
|
async def _retry_transient_errors(
|
|
301
201
|
fn: typing.Union[
|
|
302
202
|
"modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
|
|
@@ -319,15 +219,12 @@ async def _retry_transient_errors(
|
|
|
319
219
|
|
|
320
220
|
delay = retry.base_delay
|
|
321
221
|
n_retries = 0
|
|
322
|
-
n_throttled_retries = 0
|
|
323
222
|
|
|
324
223
|
status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
|
|
325
224
|
|
|
326
225
|
idempotency_key = str(uuid.uuid4())
|
|
327
226
|
|
|
328
227
|
t0 = time.time()
|
|
329
|
-
last_server_retry_warning_time = None
|
|
330
|
-
|
|
331
228
|
if retry.total_timeout is not None:
|
|
332
229
|
total_deadline = t0 + retry.total_timeout
|
|
333
230
|
else:
|
|
@@ -339,77 +236,29 @@ async def _retry_transient_errors(
|
|
|
339
236
|
attempt_metadata = [
|
|
340
237
|
("x-idempotency-key", idempotency_key),
|
|
341
238
|
("x-retry-attempt", str(n_retries)),
|
|
342
|
-
("x-throttle-retry-attempt", str(n_throttled_retries)),
|
|
343
239
|
*metadata,
|
|
344
240
|
]
|
|
345
241
|
if n_retries > 0:
|
|
346
242
|
attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
|
|
347
|
-
if n_throttled_retries > 0:
|
|
348
|
-
attempt_metadata.append(("x-throttle-retry-delay", str(time.time() - t0)))
|
|
349
|
-
|
|
350
243
|
timeouts = []
|
|
351
244
|
if retry.attempt_timeout is not None:
|
|
352
245
|
timeouts.append(retry.attempt_timeout)
|
|
353
|
-
if total_deadline is not None:
|
|
246
|
+
if retry.total_timeout is not None and total_deadline is not None:
|
|
354
247
|
timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
|
|
355
248
|
if timeouts:
|
|
356
249
|
timeout = min(timeouts) # In case the function provided both types of timeouts
|
|
357
250
|
else:
|
|
358
251
|
timeout = None
|
|
359
|
-
|
|
360
252
|
try:
|
|
361
253
|
with suppress_tb_frames(1):
|
|
362
254
|
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
363
255
|
except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
|
|
364
|
-
# Note that we only catch AttributeError to handle a specific case that works around a bug
|
|
365
|
-
# in grpclib<=0.4.7. See above (search for `write_appdata`).
|
|
366
|
-
|
|
367
|
-
# Server side instruction for retries
|
|
368
|
-
max_throttle_wait: Optional[int] = config.get("max_throttle_wait")
|
|
369
|
-
if (
|
|
370
|
-
max_throttle_wait != 0
|
|
371
|
-
and isinstance(exc, GRPCError)
|
|
372
|
-
and (server_retry_policy := get_server_retry_policy(exc))
|
|
373
|
-
):
|
|
374
|
-
server_delay = server_retry_policy.retry_after_secs
|
|
375
|
-
|
|
376
|
-
now = time.time()
|
|
377
|
-
|
|
378
|
-
# We check if the timeout will be reached **after** the sleep, so we can raise an error early
|
|
379
|
-
# without needing to actually sleep.
|
|
380
|
-
total_timeout_will_be_reached = (
|
|
381
|
-
retry.total_timeout is not None and (now + server_delay - t0) >= retry.total_timeout
|
|
382
|
-
)
|
|
383
|
-
max_throttle_will_be_reached = (
|
|
384
|
-
max_throttle_wait is not None and (now + server_delay - t0) >= max_throttle_wait
|
|
385
|
-
)
|
|
386
|
-
final_attempt = total_timeout_will_be_reached or max_throttle_will_be_reached
|
|
387
|
-
|
|
388
|
-
with suppress_tb_frames(1):
|
|
389
|
-
process_exception_before_retry(
|
|
390
|
-
exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
now = time.time()
|
|
394
|
-
if last_server_retry_warning_time is None or (
|
|
395
|
-
now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
|
|
396
|
-
):
|
|
397
|
-
last_server_retry_warning_time = now
|
|
398
|
-
logger.warning(
|
|
399
|
-
f"Warning: Received {exc.status} status: {exc.message}. "
|
|
400
|
-
f"Will retry in {server_delay:0.2f} seconds."
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
n_throttled_retries += 1
|
|
404
|
-
await asyncio.sleep(server_delay)
|
|
405
|
-
continue
|
|
406
|
-
|
|
407
|
-
# Client handles retry
|
|
408
256
|
if isinstance(exc, GRPCError) and exc.status not in status_codes:
|
|
409
257
|
if exc.status == Status.UNAUTHENTICATED:
|
|
410
258
|
raise AuthError(exc.message)
|
|
411
259
|
else:
|
|
412
260
|
raise exc
|
|
261
|
+
|
|
413
262
|
if retry.max_retries is not None and n_retries >= retry.max_retries:
|
|
414
263
|
final_attempt = True
|
|
415
264
|
elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
|
|
@@ -418,7 +267,25 @@ async def _retry_transient_errors(
|
|
|
418
267
|
final_attempt = False
|
|
419
268
|
|
|
420
269
|
with suppress_tb_frames(1):
|
|
421
|
-
|
|
270
|
+
if final_attempt:
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
|
|
273
|
+
f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
|
|
274
|
+
)
|
|
275
|
+
if isinstance(exc, OSError):
|
|
276
|
+
raise ConnectionError(str(exc))
|
|
277
|
+
elif isinstance(exc, asyncio.TimeoutError):
|
|
278
|
+
raise ConnectionError(str(exc))
|
|
279
|
+
else:
|
|
280
|
+
raise exc
|
|
281
|
+
|
|
282
|
+
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
283
|
+
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
284
|
+
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
285
|
+
# TODO: update to newer version (>=0.4.8) once stable
|
|
286
|
+
raise exc
|
|
287
|
+
|
|
288
|
+
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
|
|
422
289
|
|
|
423
290
|
n_retries += 1
|
|
424
291
|
|
|
@@ -719,7 +719,6 @@ class _App:
|
|
|
719
719
|
] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
|
|
720
720
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
721
721
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
722
|
-
nonpreemptible: bool = False, # Whether to run the function on a nonpreemptible instance.
|
|
723
722
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
724
723
|
block_network: bool = False, # Whether to block network access
|
|
725
724
|
restrict_modal_access: bool = False, # Whether to allow this function access to other Modal resources
|
|
@@ -732,6 +731,9 @@ class _App:
|
|
|
732
731
|
include_source: Optional[bool] = None,
|
|
733
732
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
734
733
|
# Parameters below here are experimental. Use with caution!
|
|
734
|
+
_experimental_scheduler_placement: Optional[
|
|
735
|
+
SchedulerPlacement
|
|
736
|
+
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
735
737
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
736
738
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
737
739
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
@@ -741,8 +743,6 @@ class _App:
|
|
|
741
743
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
742
744
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
743
745
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
744
|
-
_experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
|
|
745
|
-
# using `region` and `nonpreemptible`
|
|
746
746
|
) -> _FunctionDecoratorType:
|
|
747
747
|
"""Decorator to register a new Modal Function with this App."""
|
|
748
748
|
if isinstance(_warn_parentheses_missing, _Image):
|
|
@@ -762,24 +762,6 @@ class _App:
|
|
|
762
762
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
763
763
|
)
|
|
764
764
|
|
|
765
|
-
if _experimental_scheduler_placement is not None:
|
|
766
|
-
deprecation_warning(
|
|
767
|
-
(2025, 11, 17),
|
|
768
|
-
"The `_experimental_scheduler_placement` parameter is deprecated."
|
|
769
|
-
" Please use the `region` and `nonpreemptible` parameters instead.",
|
|
770
|
-
)
|
|
771
|
-
if region is not None or nonpreemptible:
|
|
772
|
-
raise InvalidError(
|
|
773
|
-
"Cannot use `_experimental_scheduler_placement` together with "
|
|
774
|
-
"`region` or `nonpreemptible` parameters."
|
|
775
|
-
)
|
|
776
|
-
# Extract regions and lifecycle from scheduler placement
|
|
777
|
-
if _experimental_scheduler_placement.proto.regions:
|
|
778
|
-
region = list(_experimental_scheduler_placement.proto.regions)
|
|
779
|
-
if _experimental_scheduler_placement.proto._lifecycle:
|
|
780
|
-
# Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
|
|
781
|
-
nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
|
|
782
|
-
|
|
783
765
|
secrets = secrets or []
|
|
784
766
|
if env:
|
|
785
767
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
@@ -789,7 +771,7 @@ class _App:
|
|
|
789
771
|
def wrapped(
|
|
790
772
|
f: Union[_PartialFunction, Callable[..., Any], None],
|
|
791
773
|
) -> _Function:
|
|
792
|
-
nonlocal is_generator, cloud, serialized
|
|
774
|
+
nonlocal is_generator, cloud, serialized
|
|
793
775
|
|
|
794
776
|
# Check if the decorated object is a class
|
|
795
777
|
if inspect.isclass(f):
|
|
@@ -878,6 +860,12 @@ class _App:
|
|
|
878
860
|
if is_generator is None:
|
|
879
861
|
is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
|
|
880
862
|
|
|
863
|
+
scheduler_placement: Optional[SchedulerPlacement] = _experimental_scheduler_placement
|
|
864
|
+
if region:
|
|
865
|
+
if scheduler_placement:
|
|
866
|
+
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
|
867
|
+
scheduler_placement = SchedulerPlacement(region=region)
|
|
868
|
+
|
|
881
869
|
function = _Function.from_local(
|
|
882
870
|
info,
|
|
883
871
|
app=self,
|
|
@@ -904,13 +892,12 @@ class _App:
|
|
|
904
892
|
timeout=timeout,
|
|
905
893
|
startup_timeout=startup_timeout or timeout,
|
|
906
894
|
cloud=cloud,
|
|
907
|
-
region=region,
|
|
908
|
-
nonpreemptible=nonpreemptible,
|
|
909
895
|
webhook_config=webhook_config,
|
|
910
896
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
911
897
|
block_network=block_network,
|
|
912
898
|
restrict_modal_access=restrict_modal_access,
|
|
913
899
|
max_inputs=max_inputs,
|
|
900
|
+
scheduler_placement=scheduler_placement,
|
|
914
901
|
i6pn_enabled=i6pn_enabled,
|
|
915
902
|
cluster_size=cluster_size, # Experimental: Clustered functions
|
|
916
903
|
rdma=rdma,
|
|
@@ -963,7 +950,6 @@ class _App:
|
|
|
963
950
|
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
|
|
964
951
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
965
952
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
966
|
-
nonpreemptible: bool = False, # Whether to run the function on a non-preemptible instance.
|
|
967
953
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
968
954
|
block_network: bool = False, # Whether to block network access
|
|
969
955
|
restrict_modal_access: bool = False, # Whether to allow this class access to other Modal resources
|
|
@@ -974,6 +960,9 @@ class _App:
|
|
|
974
960
|
include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
|
|
975
961
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
976
962
|
# Parameters below here are experimental. Use with caution!
|
|
963
|
+
_experimental_scheduler_placement: Optional[
|
|
964
|
+
SchedulerPlacement
|
|
965
|
+
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
977
966
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
978
967
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
979
968
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
@@ -983,8 +972,6 @@ class _App:
|
|
|
983
972
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
984
973
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
985
974
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
986
|
-
_experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
|
|
987
|
-
# using `region` and `nonpreemptible`
|
|
988
975
|
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
|
989
976
|
"""
|
|
990
977
|
Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
|
|
@@ -992,6 +979,12 @@ class _App:
|
|
|
992
979
|
if _warn_parentheses_missing:
|
|
993
980
|
raise InvalidError("Did you forget parentheses? Suggestion: `@app.cls()`.")
|
|
994
981
|
|
|
982
|
+
scheduler_placement = _experimental_scheduler_placement
|
|
983
|
+
if region:
|
|
984
|
+
if scheduler_placement:
|
|
985
|
+
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
|
986
|
+
scheduler_placement = SchedulerPlacement(region=region)
|
|
987
|
+
|
|
995
988
|
if allow_concurrent_inputs is not None:
|
|
996
989
|
deprecation_warning(
|
|
997
990
|
(2025, 4, 9),
|
|
@@ -1000,24 +993,6 @@ class _App:
|
|
|
1000
993
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
1001
994
|
)
|
|
1002
995
|
|
|
1003
|
-
if _experimental_scheduler_placement is not None:
|
|
1004
|
-
deprecation_warning(
|
|
1005
|
-
(2025, 11, 17),
|
|
1006
|
-
"The `_experimental_scheduler_placement` parameter is deprecated."
|
|
1007
|
-
" Please use the `region` and `nonpreemptible` parameters instead.",
|
|
1008
|
-
)
|
|
1009
|
-
if region is not None or nonpreemptible:
|
|
1010
|
-
raise InvalidError(
|
|
1011
|
-
"Cannot use `_experimental_scheduler_placement` together with "
|
|
1012
|
-
"`region` or `nonpreemptible` parameters."
|
|
1013
|
-
)
|
|
1014
|
-
# Extract regions and lifecycle from scheduler placement
|
|
1015
|
-
if _experimental_scheduler_placement.proto.regions:
|
|
1016
|
-
region = list(_experimental_scheduler_placement.proto.regions)
|
|
1017
|
-
if _experimental_scheduler_placement.proto._lifecycle:
|
|
1018
|
-
# Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
|
|
1019
|
-
nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
|
|
1020
|
-
|
|
1021
996
|
secrets = secrets or []
|
|
1022
997
|
if env:
|
|
1023
998
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
@@ -1111,12 +1086,11 @@ class _App:
|
|
|
1111
1086
|
timeout=timeout,
|
|
1112
1087
|
startup_timeout=startup_timeout or timeout,
|
|
1113
1088
|
cloud=cloud,
|
|
1114
|
-
region=region,
|
|
1115
|
-
nonpreemptible=nonpreemptible,
|
|
1116
1089
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
1117
1090
|
block_network=block_network,
|
|
1118
1091
|
restrict_modal_access=restrict_modal_access,
|
|
1119
1092
|
max_inputs=max_inputs,
|
|
1093
|
+
scheduler_placement=scheduler_placement,
|
|
1120
1094
|
i6pn_enabled=i6pn_enabled,
|
|
1121
1095
|
cluster_size=cluster_size,
|
|
1122
1096
|
rdma=rdma,
|