modAL 1.3.0__tar.gz → 1.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modal-1.3.0 → modal-1.3.1}/PKG-INFO +3 -2
- {modal-1.3.0 → modal-1.3.1}/modal/__init__.py +2 -0
- {modal-1.3.0 → modal-1.3.1}/modal/__main__.py +2 -1
- {modal-1.3.0 → modal-1.3.1}/modal/_billing.py +9 -5
- {modal-1.3.0 → modal-1.3.1}/modal/_container_entrypoint.py +13 -1
- {modal-1.3.0 → modal-1.3.1}/modal/_functions.py +5 -2
- {modal-1.3.0 → modal-1.3.1}/modal/_grpc_client.py +3 -3
- {modal-1.3.0 → modal-1.3.1}/modal/_load_context.py +26 -2
- {modal-1.3.0 → modal-1.3.1}/modal/_object.py +16 -11
- {modal-1.3.0 → modal-1.3.1}/modal/_output.py +2 -2
- {modal-1.3.0 → modal-1.3.1}/modal/_partial_function.py +1 -22
- {modal-1.3.0 → modal-1.3.1}/modal/_resolver.py +34 -31
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/user_code_imports.py +131 -63
- modal-1.3.1/modal/_server.py +252 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_traceback.py +3 -12
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/async_utils.py +51 -22
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/blob_utils.py +3 -9
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/function_utils.py +3 -4
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/grpc_utils.py +7 -9
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/task_command_router_client.py +25 -38
- {modal-1.3.0 → modal-1.3.1}/modal/app.py +159 -14
- {modal-1.3.0 → modal-1.3.1}/modal/app.pyi +121 -3
- {modal-1.3.0 → modal-1.3.1}/modal/builder/2025.06.txt +4 -2
- {modal-1.3.0 → modal-1.3.1}/modal/cli/_traceback.py +2 -1
- {modal-1.3.0 → modal-1.3.1}/modal/cli/container.py +5 -2
- {modal-1.3.0 → modal-1.3.1}/modal/cli/dict.py +4 -3
- {modal-1.3.0 → modal-1.3.1}/modal/cli/import_refs.py +2 -2
- {modal-1.3.0 → modal-1.3.1}/modal/cli/queues.py +4 -3
- {modal-1.3.0 → modal-1.3.1}/modal/cli/run.py +65 -13
- modal-1.3.1/modal/cli/token.py +108 -0
- {modal-1.3.0 → modal-1.3.1}/modal/client.py +2 -0
- {modal-1.3.0 → modal-1.3.1}/modal/client.pyi +5 -2
- {modal-1.3.0 → modal-1.3.1}/modal/cls.py +0 -3
- {modal-1.3.0 → modal-1.3.1}/modal/exception.py +1 -1
- {modal-1.3.0 → modal-1.3.1}/modal/experimental/__init__.py +1 -1
- {modal-1.3.0 → modal-1.3.1}/modal/experimental/flash.py +3 -7
- {modal-1.3.0 → modal-1.3.1}/modal/functions.pyi +7 -6
- {modal-1.3.0 → modal-1.3.1}/modal/image.py +37 -12
- {modal-1.3.0 → modal-1.3.1}/modal/image.pyi +4 -0
- {modal-1.3.0 → modal-1.3.1}/modal/mount.py +10 -4
- {modal-1.3.0 → modal-1.3.1}/modal/mount.pyi +3 -3
- {modal-1.3.0 → modal-1.3.1}/modal/output.py +2 -2
- {modal-1.3.0 → modal-1.3.1}/modal/partial_function.pyi +0 -1
- {modal-1.3.0 → modal-1.3.1}/modal/runner.py +29 -12
- {modal-1.3.0 → modal-1.3.1}/modal/runner.pyi +5 -1
- {modal-1.3.0 → modal-1.3.1}/modal/sandbox.py +12 -2
- {modal-1.3.0 → modal-1.3.1}/modal/sandbox.pyi +8 -0
- modal-1.3.1/modal/server.py +5 -0
- modal-1.3.1/modal/server.pyi +147 -0
- {modal-1.3.0 → modal-1.3.1}/modal/volume.pyi +2 -2
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/PKG-INFO +3 -2
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/SOURCES.txt +3 -0
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/requires.txt +6 -1
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api.proto +56 -2
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api_grpc.py +16 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2.py +717 -656
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2.pyi +178 -6
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2_grpc.py +33 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2_grpc.pyi +10 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/modal_api_grpc.py +1 -0
- {modal-1.3.0 → modal-1.3.1}/modal_version/__init__.py +2 -2
- {modal-1.3.0 → modal-1.3.1}/pyproject.toml +2 -1
- modal-1.3.0/modal/cli/token.py +0 -60
- {modal-1.3.0 → modal-1.3.1}/LICENSE +0 -0
- {modal-1.3.0 → modal-1.3.1}/README.md +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_clustered_functions.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_clustered_functions.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_ipython.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_location.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_pty.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_resources.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/asgi.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/execution_context.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/telemetry.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_serialization.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_tunnel.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_tunnel.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_type_manager.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/app_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/deprecation.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/docker_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/git_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/hash_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/http_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/logger.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/mount_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/name_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/package_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/shell_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_utils/time_utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_vendor/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_vendor/tblib.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/_watcher.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/billing.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/2023.12.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/2024.04.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/2024.10.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/README.md +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/builder/base-images.json +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/call_graph.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/_download.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/app.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/cluster.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/config.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/entry_point.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/environment.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/launch.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/network_file_system.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/profile.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/vscode.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/secret.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/shell.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/utils.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cli/volume.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/cls.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/config.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/container_process.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/container_process.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/dict.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/dict.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/environments.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/environments.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/experimental/flash.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/experimental/ipython.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/file_io.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/file_io.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/file_pattern_matcher.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/functions.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/gpu.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/io_streams.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/io_streams.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/network_file_system.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/network_file_system.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/object.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/object.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/parallel_map.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/parallel_map.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/partial_function.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/proxy.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/proxy.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/py.typed +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/queue.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/queue.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/retries.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/running_app.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/schedule.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/scheduler_placement.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/secret.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/secret.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/serving.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/serving.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/snapshot.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/snapshot.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/stream_type.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/token_flow.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/token_flow.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal/volume.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal.egg-info/top_level.txt +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/__init__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/py.typed +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router.proto +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.3.0 → modal-1.3.1}/modal_version/__main__.py +0 -0
- {modal-1.3.0 → modal-1.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -20,7 +20,8 @@ Requires-Dist: aiohttp
|
|
|
20
20
|
Requires-Dist: cbor2
|
|
21
21
|
Requires-Dist: certifi
|
|
22
22
|
Requires-Dist: click~=8.1
|
|
23
|
-
Requires-Dist: grpclib<0.4.10,>=0.4.7
|
|
23
|
+
Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
|
|
24
|
+
Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
|
|
24
25
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
25
26
|
Requires-Dist: rich>=12.0.0
|
|
26
27
|
Requires-Dist: synchronicity~=0.11.1
|
|
@@ -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",
|
|
@@ -35,6 +35,7 @@ def main():
|
|
|
35
35
|
):
|
|
36
36
|
raise
|
|
37
37
|
|
|
38
|
+
from rich.markup import escape
|
|
38
39
|
from rich.panel import Panel
|
|
39
40
|
|
|
40
41
|
title = "Error"
|
|
@@ -43,7 +44,7 @@ def main():
|
|
|
43
44
|
content = f"{content}\n\nNote: {' '.join(notes)}"
|
|
44
45
|
|
|
45
46
|
console = make_console(stderr=True)
|
|
46
|
-
panel = Panel(content, title=title, title_align="left", border_style="red")
|
|
47
|
+
panel = Panel(escape(content), title=title, title_align="left", border_style="red")
|
|
47
48
|
console.print(panel, highlight=False)
|
|
48
49
|
sys.exit(1)
|
|
49
50
|
|
|
@@ -52,11 +52,15 @@ async def _workspace_billing_report(
|
|
|
52
52
|
if end is None:
|
|
53
53
|
end = datetime.now(timezone.utc)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
if start.tzinfo is None:
|
|
56
|
+
start = start.replace(tzinfo=timezone.utc)
|
|
57
|
+
elif start.tzinfo != timezone.utc:
|
|
58
|
+
raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
|
|
59
|
+
|
|
60
|
+
if end.tzinfo is None:
|
|
61
|
+
end = end.replace(tzinfo=timezone.utc)
|
|
62
|
+
elif end.tzinfo != timezone.utc:
|
|
63
|
+
raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
|
|
60
64
|
|
|
61
65
|
request = api_pb2.WorkspaceBillingReportRequest(
|
|
62
66
|
resolution=resolution,
|
|
@@ -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 = []
|
|
@@ -6,7 +6,7 @@ from google.protobuf.message import Message
|
|
|
6
6
|
from grpclib import GRPCError, Status
|
|
7
7
|
|
|
8
8
|
from . import exception
|
|
9
|
-
from ._traceback import
|
|
9
|
+
from ._traceback import suppress_tb_frame
|
|
10
10
|
from ._utils.grpc_utils import Retry, _retry_transient_errors
|
|
11
11
|
from .config import config, logger
|
|
12
12
|
|
|
@@ -50,7 +50,7 @@ class grpc_error_converter:
|
|
|
50
50
|
def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
|
|
51
51
|
# skip all internal frames from grpclib
|
|
52
52
|
use_full_traceback = config.get("traceback")
|
|
53
|
-
with
|
|
53
|
+
with suppress_tb_frame():
|
|
54
54
|
if isinstance(exc, GRPCError):
|
|
55
55
|
modal_exc = _STATUS_TO_EXCEPTION[exc.status](exc.message)
|
|
56
56
|
modal_exc._grpc_message = exc.message
|
|
@@ -115,7 +115,7 @@ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
|
|
|
115
115
|
timeout: Optional[float] = None,
|
|
116
116
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
117
117
|
) -> ResponseType:
|
|
118
|
-
with
|
|
118
|
+
with suppress_tb_frame():
|
|
119
119
|
if timeout is not None and retry is not None:
|
|
120
120
|
raise exception.InvalidError("Retry must be None when timeout is set")
|
|
121
121
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
3
|
|
|
4
4
|
from .client import _Client
|
|
5
5
|
from .config import config
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._utils.async_utils import TaskContext
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class LoadContext:
|
|
9
12
|
"""Encapsulates optional metadata values used during object loading.
|
|
@@ -15,6 +18,7 @@ class LoadContext:
|
|
|
15
18
|
_client: Optional[_Client] = None
|
|
16
19
|
_environment_name: Optional[str] = None
|
|
17
20
|
_app_id: Optional[str] = None
|
|
21
|
+
_task_context: Optional["TaskContext"] = None
|
|
18
22
|
|
|
19
23
|
def __init__(
|
|
20
24
|
self,
|
|
@@ -22,10 +26,12 @@ class LoadContext:
|
|
|
22
26
|
client: Optional[_Client] = None,
|
|
23
27
|
environment_name: Optional[str] = None,
|
|
24
28
|
app_id: Optional[str] = None,
|
|
29
|
+
task_context: Optional["TaskContext"] = None,
|
|
25
30
|
):
|
|
26
31
|
self._client = client
|
|
27
32
|
self._environment_name = environment_name
|
|
28
33
|
self._app_id = app_id
|
|
34
|
+
self._task_context = task_context
|
|
29
35
|
|
|
30
36
|
@property
|
|
31
37
|
def client(self) -> _Client:
|
|
@@ -41,6 +47,11 @@ class LoadContext:
|
|
|
41
47
|
def app_id(self) -> Optional[str]:
|
|
42
48
|
return self._app_id
|
|
43
49
|
|
|
50
|
+
@property
|
|
51
|
+
def task_context(self) -> "TaskContext":
|
|
52
|
+
assert self._task_context is not None, "LoadContext has no TaskContext"
|
|
53
|
+
return self._task_context
|
|
54
|
+
|
|
44
55
|
@classmethod
|
|
45
56
|
def empty(cls) -> "LoadContext":
|
|
46
57
|
"""Create an empty LoadContext with all fields set to None.
|
|
@@ -59,6 +70,7 @@ class LoadContext:
|
|
|
59
70
|
client=self._client if self._client is not None else parent._client,
|
|
60
71
|
environment_name=self._environment_name if self._environment_name is not None else parent._environment_name,
|
|
61
72
|
app_id=self._app_id if self._app_id is not None else parent._app_id,
|
|
73
|
+
task_context=self._task_context if self._task_context is not None else parent._task_context,
|
|
62
74
|
) # TODO (elias): apply_defaults?
|
|
63
75
|
|
|
64
76
|
async def apply_defaults(self) -> "LoadContext":
|
|
@@ -71,16 +83,27 @@ class LoadContext:
|
|
|
71
83
|
client=self.client if is_valid_client else await _Client.from_env(),
|
|
72
84
|
environment_name=self._environment_name or config.get("environment") or "",
|
|
73
85
|
app_id=self._app_id,
|
|
86
|
+
task_context=self._task_context,
|
|
74
87
|
)
|
|
75
88
|
|
|
76
89
|
def reset(self) -> "LoadContext":
|
|
90
|
+
"""In-place replace all values with None, such that any inferred values/upgrades
|
|
91
|
+
will work. This is useful in cases where a load context reference may have leaked
|
|
92
|
+
into objects and used/upgraded but you want to make a fresh re-load, e.g. when doing
|
|
93
|
+
multiple `app.run()` calls in the same interpreter session.
|
|
94
|
+
"""
|
|
77
95
|
self._client = None
|
|
78
96
|
self._environment_name = None
|
|
79
97
|
self._app_id = None
|
|
98
|
+
self._task_context = None
|
|
80
99
|
return self
|
|
81
100
|
|
|
82
101
|
async def in_place_upgrade(
|
|
83
|
-
self,
|
|
102
|
+
self,
|
|
103
|
+
client: Optional[_Client] = None,
|
|
104
|
+
environment_name: Optional[str] = None,
|
|
105
|
+
app_id: Optional[str] = None,
|
|
106
|
+
task_context: Optional["TaskContext"] = None,
|
|
84
107
|
) -> "LoadContext":
|
|
85
108
|
"""In-place set values if they aren't already set, or set default values
|
|
86
109
|
|
|
@@ -103,4 +126,5 @@ class LoadContext:
|
|
|
103
126
|
self._client = self._client or client or await _Client.from_env()
|
|
104
127
|
self._environment_name = self._environment_name or environment_name or config.get("environment") or ""
|
|
105
128
|
self._app_id = self._app_id or app_id
|
|
129
|
+
self._task_context = self._task_context or task_context
|
|
106
130
|
return self
|
|
@@ -9,11 +9,11 @@ from typing import Callable, ClassVar, Optional
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
|
-
from modal._traceback import
|
|
12
|
+
from modal._traceback import suppress_tb_frame
|
|
13
13
|
|
|
14
14
|
from ._load_context import LoadContext
|
|
15
15
|
from ._resolver import Resolver
|
|
16
|
-
from ._utils.async_utils import aclosing
|
|
16
|
+
from ._utils.async_utils import TaskContext, aclosing
|
|
17
17
|
from ._utils.deprecation import deprecation_warning
|
|
18
18
|
from .client import _Client
|
|
19
19
|
from .config import config, logger
|
|
@@ -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]:
|
|
@@ -314,9 +316,10 @@ class _Object:
|
|
|
314
316
|
self._is_hydrated = False # un-hydrate and re-resolve
|
|
315
317
|
# we don't set an explicit Client here, relying on the default
|
|
316
318
|
# env client to be applied by LoadContext.apply_default
|
|
317
|
-
root_load_context = LoadContext.empty()
|
|
318
319
|
resolver = Resolver()
|
|
319
|
-
|
|
320
|
+
async with TaskContext() as tc:
|
|
321
|
+
root_load_context = LoadContext(task_context=tc)
|
|
322
|
+
await resolver.load(typing.cast(_Object, self), root_load_context)
|
|
320
323
|
else:
|
|
321
324
|
logger.debug(f"reloading non-lazy {self} by replacing client")
|
|
322
325
|
self._client = client or await _Client.from_env()
|
|
@@ -325,11 +328,13 @@ class _Object:
|
|
|
325
328
|
elif not self._hydrate_lazily:
|
|
326
329
|
self._validate_is_hydrated()
|
|
327
330
|
else:
|
|
328
|
-
# Set the client on LoadContext before loading
|
|
329
|
-
|
|
331
|
+
# Set the client on LoadContext before loading, with a TaskContext for proper
|
|
332
|
+
# exception handling when loading shared dependencies
|
|
330
333
|
resolver = Resolver()
|
|
331
|
-
with
|
|
332
|
-
|
|
334
|
+
async with TaskContext() as tc:
|
|
335
|
+
root_load_context = LoadContext(client=client, task_context=tc)
|
|
336
|
+
with suppress_tb_frame(): # skip this frame by default
|
|
337
|
+
await resolver.load(self, root_load_context)
|
|
333
338
|
return self
|
|
334
339
|
|
|
335
340
|
|
|
@@ -169,9 +169,9 @@ class OutputManager:
|
|
|
169
169
|
|
|
170
170
|
@classmethod
|
|
171
171
|
@contextlib.contextmanager
|
|
172
|
-
def enable_output(cls, show_progress: bool = True) -> Generator[None]:
|
|
172
|
+
def enable_output(cls, show_progress: bool = True, show_timestamps: bool = False) -> Generator[None]:
|
|
173
173
|
if show_progress:
|
|
174
|
-
cls._instance = OutputManager()
|
|
174
|
+
cls._instance = OutputManager(show_timestamps=show_timestamps)
|
|
175
175
|
try:
|
|
176
176
|
yield
|
|
177
177
|
finally:
|
|
@@ -19,7 +19,7 @@ from ._functions import _Function
|
|
|
19
19
|
from ._utils.async_utils import synchronizer
|
|
20
20
|
from ._utils.deprecation import deprecation_warning
|
|
21
21
|
from ._utils.function_utils import callable_has_non_self_params
|
|
22
|
-
from .config import config
|
|
22
|
+
from .config import config
|
|
23
23
|
from .exception import InvalidError
|
|
24
24
|
|
|
25
25
|
MAX_MAX_BATCH_SIZE = 1000
|
|
@@ -125,7 +125,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
125
125
|
user_cls: Optional[type] = None # class
|
|
126
126
|
flags: _PartialFunctionFlags
|
|
127
127
|
params: _PartialFunctionParams
|
|
128
|
-
registered: bool
|
|
129
128
|
|
|
130
129
|
def __init__(
|
|
131
130
|
self,
|
|
@@ -141,7 +140,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
141
140
|
self.user_cls = None
|
|
142
141
|
self.flags = flags
|
|
143
142
|
self.params = params
|
|
144
|
-
self.registered = False
|
|
145
143
|
self.validate_flag_composition()
|
|
146
144
|
|
|
147
145
|
def stack(self, flags: _PartialFunctionFlags, params: _PartialFunctionParams) -> typing_extensions.Self:
|
|
@@ -156,13 +154,11 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
156
154
|
uses_interface_flags = self.flags & _PartialFunctionFlags.interface_flags()
|
|
157
155
|
uses_lifecycle_flags = self.flags & _PartialFunctionFlags.lifecycle_flags()
|
|
158
156
|
if uses_interface_flags and uses_lifecycle_flags:
|
|
159
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
160
157
|
raise InvalidError("Interface decorators cannot be combined with lifecycle decorators.")
|
|
161
158
|
|
|
162
159
|
has_web_interface = self.flags & _PartialFunctionFlags.WEB_INTERFACE
|
|
163
160
|
has_callable_interface = self.flags & _PartialFunctionFlags.CALLABLE_INTERFACE
|
|
164
161
|
if has_web_interface and has_callable_interface:
|
|
165
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
166
162
|
raise InvalidError("Callable decorators cannot be combined with web interface decorators.")
|
|
167
163
|
|
|
168
164
|
def validate_obj_compatibility(
|
|
@@ -174,20 +170,17 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
174
170
|
uses_lifecycle_flags = self.flags & _PartialFunctionFlags.lifecycle_flags()
|
|
175
171
|
uses_interface_flags = self.flags & _PartialFunctionFlags.interface_flags()
|
|
176
172
|
if self.user_cls is not None and (uses_lifecycle_flags or uses_interface_flags):
|
|
177
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
178
173
|
raise InvalidError(
|
|
179
174
|
f"Cannot apply `@modal.{decorator_name}` to a class. Hint: consider applying to a method instead."
|
|
180
175
|
)
|
|
181
176
|
|
|
182
177
|
wrapped_object = self.raw_f or self.user_cls
|
|
183
178
|
if isinstance(wrapped_object, _Function):
|
|
184
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
185
179
|
raise InvalidError(
|
|
186
180
|
f"Cannot stack `@modal.{decorator_name}` on top of `@app.function`."
|
|
187
181
|
" Hint: swap the order of the decorators."
|
|
188
182
|
)
|
|
189
183
|
elif isinstance(wrapped_object, _Cls):
|
|
190
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
191
184
|
raise InvalidError(
|
|
192
185
|
f"Cannot stack `@modal.{decorator_name}` on top of `@app.cls()`."
|
|
193
186
|
" Hint: swap the order of the decorators."
|
|
@@ -196,15 +189,12 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
196
189
|
# Run some assertions about a callable wrappee defined by the specific decorator used
|
|
197
190
|
if self.raw_f is not None:
|
|
198
191
|
if not callable(self.raw_f):
|
|
199
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
200
192
|
raise InvalidError(f"The object wrapped by `@modal.{decorator_name}` must be callable.")
|
|
201
193
|
|
|
202
194
|
if require_sync and inspect.iscoroutinefunction(self.raw_f):
|
|
203
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
204
195
|
raise InvalidError(f"The `@modal.{decorator_name}` decorator can't be applied to an async function.")
|
|
205
196
|
|
|
206
197
|
if require_nullary and callable_has_non_self_params(self.raw_f):
|
|
207
|
-
self.registered = True # Hacky, avoid false-positive warning
|
|
208
198
|
raise InvalidError(f"Functions decorated by `@modal.{decorator_name}` can't have parameters.")
|
|
209
199
|
|
|
210
200
|
def _get_raw_f(self) -> Callable[P, ReturnType]:
|
|
@@ -244,17 +234,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
244
234
|
# to let it be bound to a variable and become a Function later on
|
|
245
235
|
return self # type: ignore # this returns a PartialFunction in a special internal case
|
|
246
236
|
|
|
247
|
-
def __del__(self):
|
|
248
|
-
if self.registered is False:
|
|
249
|
-
if self.raw_f is not None:
|
|
250
|
-
name, object_type, suggestion = self.raw_f.__name__, "function", "@app.function or @app.cls"
|
|
251
|
-
elif self.user_cls is not None:
|
|
252
|
-
name, object_type, suggestion = self.user_cls.__name__, "class", "@app.cls"
|
|
253
|
-
logger.warning(
|
|
254
|
-
f"The `{name}` {object_type} was never registered with the App."
|
|
255
|
-
f" Did you forget an {suggestion} decorator?"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
237
|
|
|
259
238
|
def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[str, _PartialFunction]:
|
|
260
239
|
"""Grabs all method on a user class, and returns partials. Includes legacy methods."""
|
|
@@ -9,11 +9,10 @@ from collections.abc import Hashable
|
|
|
9
9
|
from typing import TYPE_CHECKING, Optional
|
|
10
10
|
|
|
11
11
|
import modal._object
|
|
12
|
-
from modal._traceback import
|
|
12
|
+
from modal._traceback import suppress_tb_frame
|
|
13
13
|
from modal_proto import api_pb2
|
|
14
14
|
|
|
15
15
|
from ._load_context import LoadContext
|
|
16
|
-
from ._utils.async_utils import TaskContext
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from rich.tree import Tree
|
|
@@ -35,7 +34,7 @@ class StatusRow:
|
|
|
35
34
|
|
|
36
35
|
def warning(self, warning: api_pb2.Warning):
|
|
37
36
|
if self._step_node is not None:
|
|
38
|
-
self._step_node.add(f"
|
|
37
|
+
self._step_node.add(f"⚠️ {warning.message}")
|
|
39
38
|
|
|
40
39
|
def finish(self, message):
|
|
41
40
|
if self._step_node is not None and self._spinner is not None:
|
|
@@ -120,37 +119,41 @@ class Resolver:
|
|
|
120
119
|
if not cached_future:
|
|
121
120
|
# don't run any awaits within this if-block to prevent race conditions
|
|
122
121
|
async def loader():
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
122
|
+
with suppress_tb_frame():
|
|
123
|
+
load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
|
|
124
|
+
|
|
125
|
+
# Use asyncio.gather here (not TaskContext.gather) - the shared TaskContext
|
|
126
|
+
# in load_context handles cancellation at the top level, preventing premature
|
|
127
|
+
# cancellation of shared dependencies when sibling tasks fail.
|
|
128
|
+
await asyncio.gather(*[self.load(dep, load_context) for dep in obj.deps()])
|
|
129
|
+
|
|
130
|
+
# Load the object itself
|
|
131
|
+
if not obj._load:
|
|
132
|
+
raise Exception(f"Object {obj} has no loader function")
|
|
133
|
+
|
|
134
|
+
await obj._load(obj, self, load_context, existing_object_id)
|
|
135
|
+
|
|
136
|
+
# Check that the id of functions didn't change
|
|
137
|
+
# Persisted refs are ignored because their life cycle is managed independently.
|
|
138
|
+
if (
|
|
139
|
+
not obj._is_another_app
|
|
140
|
+
and existing_object_id is not None
|
|
141
|
+
and existing_object_id.startswith("fu-")
|
|
142
|
+
and obj.object_id != existing_object_id
|
|
143
|
+
):
|
|
144
|
+
raise Exception(
|
|
145
|
+
f"Tried creating an object using existing id {existing_object_id} "
|
|
146
|
+
f"but it has id {obj.object_id}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return obj
|
|
150
|
+
|
|
151
|
+
# use task_context from load_context to make sure tasks are cleaned up eventually
|
|
152
|
+
cached_future = parent_load_context.task_context.create_task(loader())
|
|
149
153
|
self._local_uuid_to_future[obj.local_uuid] = cached_future
|
|
150
154
|
if deduplication_key is not None:
|
|
151
155
|
self._deduplication_cache[deduplication_key] = cached_future
|
|
152
|
-
with
|
|
153
|
-
# skip current frame + `loader()` closure frame from above
|
|
156
|
+
with suppress_tb_frame():
|
|
154
157
|
return await cached_future
|
|
155
158
|
|
|
156
159
|
def objects(self) -> list["modal._object._Object"]:
|