modAL 1.3.1.dev32__tar.gz → 1.3.1.dev34__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.1.dev32 → modal-1.3.1.dev34}/PKG-INFO +1 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/__init__.py +2 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_container_entrypoint.py +13 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_functions.py +5 -2
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_object.py +5 -3
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_resolver.py +1 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/user_code_imports.py +131 -63
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_server.py +12 -6
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/function_utils.py +3 -4
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/app.py +156 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/app.pyi +118 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/import_refs.py +2 -2
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/client.pyi +2 -2
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/experimental/__init__.py +1 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/experimental/flash.py +1 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/functions.pyi +1 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/runner.py +10 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/server.pyi +4 -4
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api.proto +3 -2
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api_pb2.py +529 -529
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api_pb2.pyi +6 -3
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_version/__init__.py +2 -2
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/LICENSE +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/README.md +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/__main__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_billing.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_clustered_functions.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_grpc_client.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_ipython.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_load_context.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_location.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_output.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_partial_function.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_pty.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_resources.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_serialization.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_traceback.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_tunnel.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_tunnel.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_type_manager.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/async_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/blob_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/deprecation.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/logger.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/mount_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/_watcher.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/billing.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/2025.06.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/README.md +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/builder/base-images.json +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/call_graph.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/_download.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/_traceback.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/app.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/cluster.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/config.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/container.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/dict.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/entry_point.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/environment.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/launch.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/profile.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/queues.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/run.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/secret.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/shell.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/token.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/utils.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cli/volume.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/client.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cls.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/cls.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/config.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/container_process.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/container_process.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/dict.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/dict.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/environments.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/environments.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/exception.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/experimental/ipython.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/file_io.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/file_io.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/functions.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/gpu.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/image.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/image.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/io_streams.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/io_streams.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/mount.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/mount.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/network_file_system.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/network_file_system.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/object.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/object.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/output.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/parallel_map.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/parallel_map.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/partial_function.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/partial_function.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/proxy.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/proxy.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/py.typed +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/queue.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/queue.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/retries.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/runner.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/running_app.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/sandbox.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/sandbox.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/schedule.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/scheduler_placement.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/secret.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/secret.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/server.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/serving.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/serving.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/snapshot.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/snapshot.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/stream_type.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/token_flow.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/token_flow.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/volume.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal/volume.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/requires.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/__init__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api_grpc.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/py.typed +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router.proto +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/modal_version/__main__.py +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/pyproject.toml +0 -0
- {modal-1.3.1.dev32 → modal-1.3.1.dev34}/setup.cfg +0 -0
|
@@ -41,6 +41,7 @@ try:
|
|
|
41
41
|
from .schedule import Cron, Period
|
|
42
42
|
from .scheduler_placement import SchedulerPlacement
|
|
43
43
|
from .secret import Secret
|
|
44
|
+
from .server import Server
|
|
44
45
|
from .snapshot import SandboxSnapshot
|
|
45
46
|
from .volume import Volume
|
|
46
47
|
except Exception:
|
|
@@ -75,6 +76,7 @@ __all__ = [
|
|
|
75
76
|
"SandboxSnapshot",
|
|
76
77
|
"SchedulerPlacement",
|
|
77
78
|
"Secret",
|
|
79
|
+
"Server",
|
|
78
80
|
"Tunnel",
|
|
79
81
|
"Volume",
|
|
80
82
|
"asgi_app",
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
from modal._runtime.user_code_imports import (
|
|
6
6
|
Service,
|
|
7
7
|
import_class_service,
|
|
8
|
+
import_server_service,
|
|
8
9
|
import_single_function_service,
|
|
9
10
|
)
|
|
10
11
|
|
|
@@ -300,7 +301,18 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
|
300
301
|
param_kwargs = {}
|
|
301
302
|
|
|
302
303
|
with execution_context._import_context():
|
|
303
|
-
if function_def.
|
|
304
|
+
if function_def.is_server:
|
|
305
|
+
service_base_function_id = container_args.app_layout.function_ids[function_def.function_name]
|
|
306
|
+
service_function_hydration_data = [
|
|
307
|
+
o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
|
|
308
|
+
][0]
|
|
309
|
+
service = import_server_service(
|
|
310
|
+
function_def,
|
|
311
|
+
service_function_hydration_data,
|
|
312
|
+
client,
|
|
313
|
+
ser_usr_cls,
|
|
314
|
+
)
|
|
315
|
+
elif function_def.is_class:
|
|
304
316
|
# this is a bit ugly - match the function and class based on function name to get metadata
|
|
305
317
|
# This metadata is required in order to hydrate the class in case it's not globally
|
|
306
318
|
# decorated (or serialized)
|
|
@@ -689,6 +689,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
689
689
|
nonpreemptible: bool = False,
|
|
690
690
|
is_builder_function: bool = False,
|
|
691
691
|
is_auto_snapshot: bool = False,
|
|
692
|
+
is_server: bool = False,
|
|
692
693
|
enable_memory_snapshot: bool = False,
|
|
693
694
|
block_network: bool = False,
|
|
694
695
|
restrict_modal_access: bool = False,
|
|
@@ -722,7 +723,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
722
723
|
f"Function {raw_f} has a schedule, so it needs to support being called with no arguments"
|
|
723
724
|
)
|
|
724
725
|
else:
|
|
725
|
-
# must be a "class service function"
|
|
726
726
|
assert info.user_cls
|
|
727
727
|
assert not webhook_config
|
|
728
728
|
assert not schedule
|
|
@@ -734,7 +734,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
734
734
|
]
|
|
735
735
|
|
|
736
736
|
retry_policy = _parse_retries(
|
|
737
|
-
retries,
|
|
737
|
+
retries,
|
|
738
|
+
f"Function '{info.get_tag()}'" if info.raw_f else f"Class '{info.get_tag()}'",
|
|
738
739
|
)
|
|
739
740
|
|
|
740
741
|
if retry_policy is not None:
|
|
@@ -1054,6 +1055,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1054
1055
|
supported_input_formats=supported_input_formats,
|
|
1055
1056
|
supported_output_formats=supported_output_formats,
|
|
1056
1057
|
http_config=http_config,
|
|
1058
|
+
is_server=is_server,
|
|
1057
1059
|
)
|
|
1058
1060
|
|
|
1059
1061
|
if isinstance(gpu, list):
|
|
@@ -1093,6 +1095,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1093
1095
|
supported_input_formats=supported_input_formats,
|
|
1094
1096
|
supported_output_formats=supported_output_formats,
|
|
1095
1097
|
http_config=http_config,
|
|
1098
|
+
is_server=function_definition.is_server,
|
|
1096
1099
|
)
|
|
1097
1100
|
|
|
1098
1101
|
ranked_functions = []
|
|
@@ -127,9 +127,11 @@ class _Object:
|
|
|
127
127
|
assert isinstance(object_id, str) and self._type_prefix is not None
|
|
128
128
|
if not object_id.startswith(self._type_prefix):
|
|
129
129
|
raise ExecutionError(
|
|
130
|
-
f"Can not hydrate {type(self)}:"
|
|
130
|
+
f"Can not hydrate {type(self)}: "
|
|
131
131
|
f" it has type prefix {self._type_prefix}"
|
|
132
|
-
f" but the object_id starts with {object_id[:3]}"
|
|
132
|
+
f" but the object_id starts with {object_id[:3]}. "
|
|
133
|
+
"This usually means the object name was previously used for a different type. "
|
|
134
|
+
"Rename the object/app or stop the previous deployment and redeploy."
|
|
133
135
|
)
|
|
134
136
|
self._object_id = object_id
|
|
135
137
|
self._client = client
|
|
@@ -137,7 +139,7 @@ class _Object:
|
|
|
137
139
|
self._is_hydrated = True
|
|
138
140
|
|
|
139
141
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
140
|
-
# override this
|
|
142
|
+
# override this if it's a subclass that needs additional data (other than an object_id) for a functioning Handle
|
|
141
143
|
pass
|
|
142
144
|
|
|
143
145
|
def _get_metadata(self) -> Optional[Message]:
|
|
@@ -34,7 +34,7 @@ class StatusRow:
|
|
|
34
34
|
|
|
35
35
|
def warning(self, warning: api_pb2.Warning):
|
|
36
36
|
if self._step_node is not None:
|
|
37
|
-
self._step_node.add(f"
|
|
37
|
+
self._step_node.add(f"⚠️ {warning.message}")
|
|
38
38
|
|
|
39
39
|
def finish(self, message):
|
|
40
40
|
if self._step_node is not None and self._spinner is not None:
|
|
@@ -13,6 +13,7 @@ from typing import Any, Callable, Generator, Optional, Sequence
|
|
|
13
13
|
import modal._object
|
|
14
14
|
import modal._runtime.container_io_manager
|
|
15
15
|
import modal.cls
|
|
16
|
+
import modal.server
|
|
16
17
|
from modal import Function
|
|
17
18
|
from modal._functions import _Function
|
|
18
19
|
from modal._partial_function import (
|
|
@@ -129,21 +130,23 @@ class Service(metaclass=ABCMeta):
|
|
|
129
130
|
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
|
130
131
|
) -> dict[str, "FinalizedFunction"]: ...
|
|
131
132
|
|
|
132
|
-
@abstractmethod
|
|
133
133
|
@contextmanager
|
|
134
134
|
def lifecycle_presnapshot(
|
|
135
135
|
self,
|
|
136
136
|
event_loop: UserCodeEventLoop,
|
|
137
137
|
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
138
|
-
) -> Generator[None, None, None]:
|
|
138
|
+
) -> Generator[None, None, None]:
|
|
139
|
+
# Default no-op implementation for services without pre-snapshot lifecycle handling
|
|
140
|
+
yield
|
|
139
141
|
|
|
140
|
-
@abstractmethod
|
|
141
142
|
@contextmanager
|
|
142
143
|
def lifecycle_postsnapshot(
|
|
143
144
|
self,
|
|
144
145
|
event_loop: UserCodeEventLoop,
|
|
145
146
|
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
146
|
-
) -> Generator[None, None, None]:
|
|
147
|
+
) -> Generator[None, None, None]:
|
|
148
|
+
# Default no-op implementation for services without post-snapshot lifecycle handling
|
|
149
|
+
yield
|
|
147
150
|
|
|
148
151
|
@contextmanager
|
|
149
152
|
def execution_context(
|
|
@@ -303,13 +306,28 @@ class ImportedFunction(Service):
|
|
|
303
306
|
)
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
|
|
310
|
+
class _LifecycleManager:
|
|
311
|
+
"""Lifecycle manager for class-based services (Cls and Server).
|
|
312
|
+
|
|
313
|
+
Handles pre snapshot, post snapshot, and exit lifecycle handling
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
user_cls_instance: Any
|
|
317
|
+
function_def: api_pb2.Function
|
|
318
|
+
|
|
306
319
|
@contextmanager
|
|
307
320
|
def lifecycle_presnapshot(
|
|
308
321
|
self,
|
|
309
322
|
event_loop: UserCodeEventLoop,
|
|
310
323
|
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
311
324
|
):
|
|
312
|
-
#
|
|
325
|
+
# Identify all "enter" methods that need to run before we snapshot.
|
|
326
|
+
if not self.function_def.is_auto_snapshot:
|
|
327
|
+
pre_snapshot_methods = _find_callables_for_obj(
|
|
328
|
+
self.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
|
329
|
+
)
|
|
330
|
+
call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
|
|
313
331
|
yield
|
|
314
332
|
|
|
315
333
|
@contextmanager
|
|
@@ -318,12 +336,26 @@ class ImportedFunction(Service):
|
|
|
318
336
|
event_loop: UserCodeEventLoop,
|
|
319
337
|
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
320
338
|
):
|
|
321
|
-
#
|
|
322
|
-
|
|
339
|
+
# Identify the "enter" methods to run after resuming from a snapshot.
|
|
340
|
+
flash_entry = _FlashContainerEntry(self.function_def.http_config)
|
|
341
|
+
if not self.function_def.is_auto_snapshot:
|
|
342
|
+
post_snapshot_methods = _find_callables_for_obj(
|
|
343
|
+
self.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
|
|
344
|
+
)
|
|
345
|
+
call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
|
|
346
|
+
flash_entry.enter()
|
|
347
|
+
try:
|
|
348
|
+
yield
|
|
349
|
+
finally:
|
|
350
|
+
if not self.function_def.is_auto_snapshot:
|
|
351
|
+
flash_entry.stop()
|
|
352
|
+
exit_methods = _find_callables_for_obj(self.user_cls_instance, _PartialFunctionFlags.EXIT)
|
|
353
|
+
call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
|
|
354
|
+
flash_entry.close()
|
|
323
355
|
|
|
324
356
|
|
|
325
357
|
@dataclass
|
|
326
|
-
class ImportedClass(Service):
|
|
358
|
+
class ImportedClass(_LifecycleManager, Service):
|
|
327
359
|
user_cls_instance: Any
|
|
328
360
|
app: "modal.app._App"
|
|
329
361
|
service_deps: Optional[Sequence["modal._object._Object"]]
|
|
@@ -373,42 +405,18 @@ class ImportedClass(Service):
|
|
|
373
405
|
finalized_functions[method_name] = finalized_function
|
|
374
406
|
return finalized_functions
|
|
375
407
|
|
|
376
|
-
@contextmanager
|
|
377
|
-
def lifecycle_presnapshot(
|
|
378
|
-
self,
|
|
379
|
-
event_loop: UserCodeEventLoop,
|
|
380
|
-
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
|
381
|
-
):
|
|
382
|
-
# Identify all "enter" methods that need to run before we snapshot.
|
|
383
|
-
if not self.function_def.is_auto_snapshot:
|
|
384
|
-
pre_snapshot_methods = _find_callables_for_obj(
|
|
385
|
-
self.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
|
386
|
-
)
|
|
387
|
-
call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
|
|
388
|
-
yield
|
|
389
408
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
|
|
403
|
-
flash_entry.enter()
|
|
404
|
-
try:
|
|
405
|
-
yield
|
|
406
|
-
finally:
|
|
407
|
-
if not self.function_def.is_auto_snapshot:
|
|
408
|
-
flash_entry.stop()
|
|
409
|
-
exit_methods = _find_callables_for_obj(self.user_cls_instance, _PartialFunctionFlags.EXIT)
|
|
410
|
-
call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
|
|
411
|
-
flash_entry.close()
|
|
409
|
+
@dataclass
|
|
410
|
+
class ImportedServer(_LifecycleManager, Service):
|
|
411
|
+
user_cls_instance: Any
|
|
412
|
+
app: "modal.app._App"
|
|
413
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
|
414
|
+
function_def: api_pb2.Function
|
|
415
|
+
|
|
416
|
+
def get_finalized_functions(
|
|
417
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
|
418
|
+
) -> dict[str, "FinalizedFunction"]:
|
|
419
|
+
return {}
|
|
412
420
|
|
|
413
421
|
|
|
414
422
|
def get_user_class_instance(_cls: modal.cls._Cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> typing.Any:
|
|
@@ -497,29 +505,15 @@ def import_single_function_service(
|
|
|
497
505
|
)
|
|
498
506
|
|
|
499
507
|
|
|
500
|
-
def
|
|
508
|
+
def _get_cls_or_user_cls(
|
|
501
509
|
function_def: api_pb2.Function,
|
|
502
|
-
service_function_hydration_data: api_pb2.Object,
|
|
503
|
-
class_id: str,
|
|
504
|
-
client: "modal.client.Client",
|
|
505
510
|
ser_user_cls: Optional[type],
|
|
506
|
-
|
|
507
|
-
cls_kwargs,
|
|
508
|
-
) -> Service:
|
|
509
|
-
"""
|
|
510
|
-
This imports a full class to be able to execute any @method or webhook decorated methods.
|
|
511
|
-
|
|
512
|
-
See import_function.
|
|
513
|
-
"""
|
|
514
|
-
active_app: Optional["modal.app._App"]
|
|
515
|
-
service_deps: Optional[Sequence["modal._object._Object"]]
|
|
516
|
-
cls_or_user_cls: typing.Union[type, modal.cls.Cls]
|
|
517
|
-
|
|
511
|
+
) -> typing.Union[type, modal.cls.Cls, modal.server.Server]:
|
|
518
512
|
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
|
519
513
|
assert ser_user_cls is not None
|
|
520
514
|
cls_or_user_cls = ser_user_cls
|
|
521
515
|
else:
|
|
522
|
-
# Load the module dynamically
|
|
516
|
+
# Load the module dynamically for non-serialized class.
|
|
523
517
|
module = importlib.import_module(function_def.module_name)
|
|
524
518
|
qual_name: str = function_def.function_name
|
|
525
519
|
|
|
@@ -527,9 +521,8 @@ def import_class_service(
|
|
|
527
521
|
raise LocalFunctionError("Attempted to load a class defined in a function scope")
|
|
528
522
|
|
|
529
523
|
parts = qual_name.split(".")
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
): # the "function name" of a class service "function placeholder" is expected to be "ClassName.*"
|
|
524
|
+
# Class service functions have pattern "ClassName.*", servers use "ClassName"
|
|
525
|
+
if not (len(parts) == 1 or (len(parts) == 2 and parts[1] == "*")):
|
|
533
526
|
raise ExecutionError(
|
|
534
527
|
f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
|
|
535
528
|
)
|
|
@@ -537,6 +530,31 @@ def import_class_service(
|
|
|
537
530
|
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
|
538
531
|
cls_name = parts[0]
|
|
539
532
|
cls_or_user_cls = getattr(module, cls_name)
|
|
533
|
+
return cls_or_user_cls
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def import_class_service(
|
|
537
|
+
function_def: api_pb2.Function,
|
|
538
|
+
service_function_hydration_data: api_pb2.Object,
|
|
539
|
+
class_id: str,
|
|
540
|
+
client: "modal.client.Client",
|
|
541
|
+
ser_user_cls: Optional[type],
|
|
542
|
+
cls_args,
|
|
543
|
+
cls_kwargs,
|
|
544
|
+
) -> Service:
|
|
545
|
+
"""
|
|
546
|
+
This imports a full class to be able to execute any @method or webhook decorated methods.
|
|
547
|
+
|
|
548
|
+
See import_function.
|
|
549
|
+
"""
|
|
550
|
+
active_app: Optional["modal.app._App"]
|
|
551
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
|
552
|
+
cls_or_user_cls: typing.Union[type, modal.cls.Cls]
|
|
553
|
+
|
|
554
|
+
cls_or_user_cls = typing.cast(
|
|
555
|
+
typing.Union[type, modal.cls.Cls],
|
|
556
|
+
_get_cls_or_user_cls(function_def, ser_user_cls),
|
|
557
|
+
)
|
|
540
558
|
|
|
541
559
|
if isinstance(cls_or_user_cls, modal.cls.Cls):
|
|
542
560
|
_cls = typing.cast(modal.cls._Cls, synchronizer._translate_in(cls_or_user_cls))
|
|
@@ -572,6 +590,56 @@ def import_class_service(
|
|
|
572
590
|
)
|
|
573
591
|
|
|
574
592
|
|
|
593
|
+
def import_server_service(
|
|
594
|
+
function_def: api_pb2.Function,
|
|
595
|
+
service_function_hydration_data: api_pb2.Object,
|
|
596
|
+
client: "modal.client.Client",
|
|
597
|
+
ser_user_cls: Optional[type],
|
|
598
|
+
) -> Service:
|
|
599
|
+
"""
|
|
600
|
+
This imports a class as a server to server HTTP requests.
|
|
601
|
+
|
|
602
|
+
See import_function.
|
|
603
|
+
"""
|
|
604
|
+
active_app: Optional["modal.app._App"]
|
|
605
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
|
606
|
+
cls_or_user_cls: typing.Union[type, modal.server.Server]
|
|
607
|
+
|
|
608
|
+
cls_or_user_cls = typing.cast(
|
|
609
|
+
typing.Union[type, modal.server.Server],
|
|
610
|
+
_get_cls_or_user_cls(function_def, ser_user_cls),
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if isinstance(cls_or_user_cls, modal.server.Server):
|
|
614
|
+
_server = typing.cast(modal._server._Server, synchronizer._translate_in(cls_or_user_cls))
|
|
615
|
+
server_service_function: _Function = _server._get_service_function()
|
|
616
|
+
service_deps = server_service_function.deps(only_explicit_mounts=True)
|
|
617
|
+
active_app = _server._get_app()
|
|
618
|
+
|
|
619
|
+
else:
|
|
620
|
+
# Undecorated user class (serialized or local scope-decoration).
|
|
621
|
+
service_deps = None # we can't infer service deps for now
|
|
622
|
+
active_app = get_active_app_fallback(function_def)
|
|
623
|
+
_client = typing.cast("modal.client._Client", synchronizer._translate_in(client))
|
|
624
|
+
_service_function: modal._functions._Function[Any, Any, Any] = modal._functions._Function._new_hydrated(
|
|
625
|
+
service_function_hydration_data.object_id,
|
|
626
|
+
_client,
|
|
627
|
+
service_function_hydration_data.function_handle_metadata,
|
|
628
|
+
is_another_app=True, # this skips re-loading the function, which is required since it doesn't have a loader
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
_server = modal._server._Server._from_local(cls_or_user_cls, active_app, _service_function)
|
|
632
|
+
|
|
633
|
+
user_cls = _server._get_user_cls()
|
|
634
|
+
# Create server object with lifecycle methods registered.
|
|
635
|
+
return ImportedServer(
|
|
636
|
+
user_cls_instance=user_cls(),
|
|
637
|
+
app=active_app,
|
|
638
|
+
service_deps=service_deps,
|
|
639
|
+
function_def=function_def,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
575
643
|
def get_active_app_fallback(function_def: api_pb2.Function) -> _App:
|
|
576
644
|
# This branch is reached in the special case that the imported function/class is:
|
|
577
645
|
# 1) not serialized, and
|
|
@@ -41,10 +41,10 @@ class _Server:
|
|
|
41
41
|
See [lifecycle hooks](https://modal.com/docs/guide/lifecycle-functions) for more information.
|
|
42
42
|
|
|
43
43
|
Generally, you will not construct a Server directly.
|
|
44
|
-
Instead, use the [`@app.
|
|
44
|
+
Instead, use the [`@app._experimental_server()`](https://modal.com/docs/reference/modal.App#server) decorator.
|
|
45
45
|
|
|
46
46
|
```python notest
|
|
47
|
-
@app.
|
|
47
|
+
@app._experimental_server(port=8000, proxy_regions=["us-east", "us-west"])
|
|
48
48
|
class MyServer:
|
|
49
49
|
@modal.enter()
|
|
50
50
|
def start_server(self):
|
|
@@ -78,8 +78,14 @@ class _Server:
|
|
|
78
78
|
# ============ Live Methods ============
|
|
79
79
|
|
|
80
80
|
@live_method
|
|
81
|
-
async def get_urls(self) -> Optional[
|
|
82
|
-
|
|
81
|
+
async def get_urls(self) -> Optional[dict[str, str]]:
|
|
82
|
+
def _extract_region_from_url(url: str) -> str:
|
|
83
|
+
return url.split(".")[-3].removeprefix("modal-")
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
_extract_region_from_url(url): url
|
|
87
|
+
for url in await self._get_service_function()._experimental_get_flash_urls()
|
|
88
|
+
}
|
|
83
89
|
|
|
84
90
|
@live_method
|
|
85
91
|
async def update_autoscaler(
|
|
@@ -129,7 +135,7 @@ class _Server:
|
|
|
129
135
|
) -> "_Server":
|
|
130
136
|
"""Create a Server from a local class definition."""
|
|
131
137
|
|
|
132
|
-
# Note: Validation should be done by the caller (app.
|
|
138
|
+
# Note: Validation should be done by the caller (app._experimental_server()) BEFORE creating the Server.
|
|
133
139
|
# Extract the underlying class if wrapped in a _PartialFunction (e.g., from @modal.clustered())
|
|
134
140
|
user_cls = _Server._extract_user_cls(wrapped_user_cls)
|
|
135
141
|
|
|
@@ -182,7 +188,7 @@ class _Server:
|
|
|
182
188
|
user_cls = _Server._extract_user_cls(wrapped_user_cls)
|
|
183
189
|
|
|
184
190
|
if not inspect.isclass(user_cls):
|
|
185
|
-
raise TypeError("The @app.
|
|
191
|
+
raise TypeError("The @app._experimental_server() decorator must be used on a class.")
|
|
186
192
|
|
|
187
193
|
# Check for modal.parameter() - not allowed on server classes
|
|
188
194
|
params = {k: v for k, v in user_cls.__dict__.items() if is_parameter(v)}
|
|
@@ -160,7 +160,6 @@ class FunctionInfo:
|
|
|
160
160
|
):
|
|
161
161
|
self.raw_f = f
|
|
162
162
|
self.user_cls = user_cls
|
|
163
|
-
|
|
164
163
|
if f is None and user_cls:
|
|
165
164
|
# "service function" for running all methods of a class
|
|
166
165
|
self.implementation_name = f"{user_cls.__name__}.*"
|
|
@@ -264,8 +263,7 @@ class FunctionInfo:
|
|
|
264
263
|
LOAD_ATTR = opcode.opmap["LOAD_ATTR"]
|
|
265
264
|
STORE_ATTR = opcode.opmap["STORE_ATTR"]
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
code = func.__code__
|
|
266
|
+
code = self.raw_f.__code__
|
|
269
267
|
f_attr_ops = set()
|
|
270
268
|
for instr in dis.get_instructions(code):
|
|
271
269
|
if instr.opcode == LOAD_ATTR:
|
|
@@ -663,10 +661,11 @@ class FunctionCreationStatus:
|
|
|
663
661
|
)
|
|
664
662
|
|
|
665
663
|
elif self.response.function.flash_service_urls:
|
|
664
|
+
self.status_row.finish(f"Created function {self.tag}.")
|
|
666
665
|
for flash_service_url in self.response.function.flash_service_urls:
|
|
667
666
|
flash_service_url_status_row = self.resolver.add_status_row()
|
|
668
667
|
flash_service_url_status_row.finish(
|
|
669
|
-
f"Created
|
|
668
|
+
f"Created server endpoints for {self.tag} => "
|
|
670
669
|
f"[magenta underline]{flash_service_url}[/magenta underline]"
|
|
671
670
|
)
|
|
672
671
|
|
|
@@ -30,12 +30,18 @@ from ._partial_function import (
|
|
|
30
30
|
_PartialFunctionFlags,
|
|
31
31
|
verify_concurrent_params,
|
|
32
32
|
)
|
|
33
|
+
from ._server import _Server, validate_http_server_config
|
|
33
34
|
from ._utils.async_utils import synchronize_api
|
|
34
35
|
from ._utils.deprecation import (
|
|
35
36
|
deprecation_warning,
|
|
36
37
|
warn_on_renamed_autoscaler_settings,
|
|
37
38
|
)
|
|
38
|
-
from ._utils.function_utils import
|
|
39
|
+
from ._utils.function_utils import (
|
|
40
|
+
FunctionInfo,
|
|
41
|
+
is_flash_object,
|
|
42
|
+
is_global_object,
|
|
43
|
+
is_method_fn,
|
|
44
|
+
)
|
|
39
45
|
from ._utils.mount_utils import validate_volumes
|
|
40
46
|
from ._utils.name_utils import check_object_name, check_tag_dict
|
|
41
47
|
from .client import _Client
|
|
@@ -1184,6 +1190,155 @@ class _App:
|
|
|
1184
1190
|
|
|
1185
1191
|
return wrapper
|
|
1186
1192
|
|
|
1193
|
+
def _experimental_server(
|
|
1194
|
+
self,
|
|
1195
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
1196
|
+
*,
|
|
1197
|
+
image: Optional[_Image] = None, # The image to run as the container for the server
|
|
1198
|
+
env: Optional[dict[str, Optional[str]]] = None, # Environment variables to set in the container
|
|
1199
|
+
secrets: Optional[Collection[_Secret]] = None, # Secrets to inject into the container as environment variables
|
|
1200
|
+
gpu: Union[GPU_T, list[GPU_T]] = None, # GPU request; either a single GPU type or a list of types
|
|
1201
|
+
serialized: bool = False, # Whether to send the server class over using cloudpickle.
|
|
1202
|
+
volumes: dict[
|
|
1203
|
+
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
|
1204
|
+
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
|
1205
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None, # CPU cores to request
|
|
1206
|
+
memory: Optional[Union[int, tuple[int, int]]] = None, # Memory in MiB to request
|
|
1207
|
+
ephemeral_disk: Optional[int] = None, # Ephemeral disk size in MiB
|
|
1208
|
+
min_containers: Optional[int] = None, # Minimum number of containers to keep warm
|
|
1209
|
+
max_containers: Optional[int] = None, # Maximum number of containers
|
|
1210
|
+
buffer_containers: Optional[int] = None, # Additional idle containers under active load
|
|
1211
|
+
scaledown_window: Optional[int] = None, # Max idle time before scaling down (seconds)
|
|
1212
|
+
proxy: Optional[_Proxy] = None, # Modal Proxy to use in front of this server
|
|
1213
|
+
port: int = 8000, # Port the HTTP server listens on
|
|
1214
|
+
startup_timeout: int = 30, # Maximum startup time in seconds
|
|
1215
|
+
exit_grace_period: int = 0, # Grace period for in-flight requests on shutdown
|
|
1216
|
+
proxy_regions: Optional[Sequence[str]] = ["us-east"], # Required: Regions to deploy proxy endpoints
|
|
1217
|
+
h2_enabled: bool = False, # Enable HTTP/2
|
|
1218
|
+
target_concurrency: Optional[int] = None, # Target concurrency for the server
|
|
1219
|
+
cloud: Optional[str] = None, # Cloud provider (aws, gcp, oci, auto)
|
|
1220
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region(s) to run on
|
|
1221
|
+
nonpreemptible: bool = False, # Whether to use non-preemptible instances
|
|
1222
|
+
enable_memory_snapshot: bool = False, # Enable memory checkpointing
|
|
1223
|
+
i6pn: Optional[bool] = None, # Enable IPv6 container networking
|
|
1224
|
+
include_source: Optional[bool] = None, # Whether to add source to container
|
|
1225
|
+
# Experimental options
|
|
1226
|
+
experimental_options: Optional[dict[str, Any]] = None,
|
|
1227
|
+
) -> Callable[[Union[CLS_T, _PartialFunction]], _Server]:
|
|
1228
|
+
"""
|
|
1229
|
+
Decorator to register a new Modal Server with this App.
|
|
1230
|
+
|
|
1231
|
+
Servers run HTTP servers that are started in an `@enter` method.
|
|
1232
|
+
Unlike `@app.cls()`, servers only expose HTTP endpoints and do not
|
|
1233
|
+
support `.remote()` method calls.
|
|
1234
|
+
|
|
1235
|
+
Example:
|
|
1236
|
+
|
|
1237
|
+
```python
|
|
1238
|
+
@app._experimental_server(port=8000, proxy_regions=["us-east"])
|
|
1239
|
+
class MyServer:
|
|
1240
|
+
@modal.enter()
|
|
1241
|
+
def start(self):
|
|
1242
|
+
self.proc = subprocess.Popen(["python3", "-m", "http.server", "8000"])
|
|
1243
|
+
|
|
1244
|
+
@modal.exit()
|
|
1245
|
+
def stop(self):
|
|
1246
|
+
self.proc.terminate()
|
|
1247
|
+
```
|
|
1248
|
+
"""
|
|
1249
|
+
if _warn_parentheses_missing:
|
|
1250
|
+
raise InvalidError("Did you forget parentheses? Suggestion: `@app._experimental_server()`.")
|
|
1251
|
+
|
|
1252
|
+
# Validate HTTP server config
|
|
1253
|
+
validate_http_server_config(
|
|
1254
|
+
port=port,
|
|
1255
|
+
proxy_regions=proxy_regions,
|
|
1256
|
+
startup_timeout=startup_timeout,
|
|
1257
|
+
exit_grace_period=exit_grace_period,
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
if target_concurrency is not None:
|
|
1261
|
+
if not isinstance(target_concurrency, int) or target_concurrency < 1:
|
|
1262
|
+
raise InvalidError("The `target_concurrency` argument must be a positive integer.")
|
|
1263
|
+
|
|
1264
|
+
http_config = api_pb2.HTTPConfig(
|
|
1265
|
+
port=port,
|
|
1266
|
+
proxy_regions=proxy_regions,
|
|
1267
|
+
startup_timeout=startup_timeout,
|
|
1268
|
+
exit_grace_period=exit_grace_period,
|
|
1269
|
+
h2_enabled=h2_enabled,
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
# Build secrets list
|
|
1273
|
+
secrets_list: list[_Secret] = list(secrets) if secrets else []
|
|
1274
|
+
if env:
|
|
1275
|
+
secrets_list.append(_Secret.from_dict(env))
|
|
1276
|
+
|
|
1277
|
+
def wrapper(wrapped_user_cls: Union[CLS_T, _PartialFunction, Callable]) -> _Server:
|
|
1278
|
+
_Server._validate_wrapped_user_cls_decorators(wrapped_user_cls, enable_memory_snapshot)
|
|
1279
|
+
|
|
1280
|
+
# Validate the server class
|
|
1281
|
+
_Server._validate_construction_mechanism(wrapped_user_cls)
|
|
1282
|
+
|
|
1283
|
+
# Extract the underlying class if wrapped in a _PartialFunction (e.g., from @modal.clustered())
|
|
1284
|
+
cluster_size = None
|
|
1285
|
+
rdma = None
|
|
1286
|
+
user_cls = wrapped_user_cls
|
|
1287
|
+
|
|
1288
|
+
if isinstance(wrapped_user_cls, _PartialFunction):
|
|
1289
|
+
user_cls = wrapped_user_cls.user_cls
|
|
1290
|
+
if wrapped_user_cls.flags & _PartialFunctionFlags.CLUSTERED:
|
|
1291
|
+
cluster_size = wrapped_user_cls.params.cluster_size
|
|
1292
|
+
rdma = wrapped_user_cls.params.rdma
|
|
1293
|
+
|
|
1294
|
+
local_state = self._local_state
|
|
1295
|
+
|
|
1296
|
+
# Create the FunctionInfo for the server, note we treat FunctionInfo as a class for servers
|
|
1297
|
+
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls, name_override=user_cls.__name__)
|
|
1298
|
+
# Create the service function
|
|
1299
|
+
service_function = _Function.from_local(
|
|
1300
|
+
info,
|
|
1301
|
+
app=self,
|
|
1302
|
+
image=image or self._get_default_image(),
|
|
1303
|
+
secrets=[*local_state.secrets_default, *secrets_list],
|
|
1304
|
+
gpu=gpu,
|
|
1305
|
+
network_file_systems={}, # Deprecated: No support for Server level network file systems
|
|
1306
|
+
volumes={**local_state.volumes_default, **volumes},
|
|
1307
|
+
cpu=cpu,
|
|
1308
|
+
memory=memory,
|
|
1309
|
+
ephemeral_disk=ephemeral_disk,
|
|
1310
|
+
min_containers=min_containers,
|
|
1311
|
+
max_containers=max_containers,
|
|
1312
|
+
buffer_containers=buffer_containers,
|
|
1313
|
+
scaledown_window=scaledown_window,
|
|
1314
|
+
proxy=proxy,
|
|
1315
|
+
retries=None, # No support for Server level retries
|
|
1316
|
+
max_concurrent_inputs=None, # No support for Server level concurrent inputs
|
|
1317
|
+
target_concurrent_inputs=target_concurrency, # No support for Server level concurrent inputs
|
|
1318
|
+
batch_max_size=None, # No support for Server level batching
|
|
1319
|
+
batch_wait_ms=None, # No support for Server level batching
|
|
1320
|
+
startup_timeout=startup_timeout,
|
|
1321
|
+
cloud=cloud,
|
|
1322
|
+
region=region,
|
|
1323
|
+
nonpreemptible=nonpreemptible,
|
|
1324
|
+
enable_memory_snapshot=enable_memory_snapshot,
|
|
1325
|
+
single_use_containers=False, # No support for single-use server containers
|
|
1326
|
+
http_config=http_config,
|
|
1327
|
+
is_server=True,
|
|
1328
|
+
i6pn_enabled=i6pn or (cluster_size is not None),
|
|
1329
|
+
cluster_size=cluster_size,
|
|
1330
|
+
rdma=rdma,
|
|
1331
|
+
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
1332
|
+
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
1333
|
+
restrict_output=False,
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
self._add_function(service_function, is_web_endpoint=False)
|
|
1337
|
+
server: _Server = _Server._from_local(wrapped_user_cls, self, service_function)
|
|
1338
|
+
return server
|
|
1339
|
+
|
|
1340
|
+
return wrapper
|
|
1341
|
+
|
|
1187
1342
|
def include(self, /, other_app: "_App", inherit_tags: bool = True) -> typing_extensions.Self:
|
|
1188
1343
|
"""Include another App's objects in this one.
|
|
1189
1344
|
|