modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl
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/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
@@ -2,10 +2,11 @@
|
|
2
2
|
from contextvars import ContextVar
|
3
3
|
from typing import Callable, Optional
|
4
4
|
|
5
|
-
from modal._container_io_manager import _ContainerIOManager
|
6
5
|
from modal._utils.async_utils import synchronize_api
|
7
6
|
from modal.exception import InvalidError
|
8
7
|
|
8
|
+
from .container_io_manager import _ContainerIOManager
|
9
|
+
|
9
10
|
|
10
11
|
def is_local() -> bool:
|
11
12
|
"""Returns if we are currently on the machine launching/deploying a Modal app
|
@@ -17,6 +18,11 @@ def is_local() -> bool:
|
|
17
18
|
|
18
19
|
|
19
20
|
async def _interact() -> None:
|
21
|
+
"""Enable interactivity with user input inside a Modal container.
|
22
|
+
|
23
|
+
See the [interactivity guide](https://modal.com/docs/guide/developing-debugging#interactivity)
|
24
|
+
for more information on how to use this function.
|
25
|
+
"""
|
20
26
|
container_io_manager = _ContainerIOManager._singleton
|
21
27
|
if not container_io_manager:
|
22
28
|
raise InvalidError("Interactivity only works inside a Modal container.")
|
@@ -65,7 +71,10 @@ def current_function_call_id() -> Optional[str]:
|
|
65
71
|
return None
|
66
72
|
|
67
73
|
|
68
|
-
def _set_current_context_ids(
|
74
|
+
def _set_current_context_ids(input_ids: list[str], function_call_ids: list[str]) -> Callable[[], None]:
|
75
|
+
assert len(input_ids) == len(function_call_ids) and len(input_ids) > 0
|
76
|
+
input_id = input_ids[0]
|
77
|
+
function_call_id = function_call_ids[0]
|
69
78
|
input_token = _current_input_id.set(input_id)
|
70
79
|
function_call_token = _current_function_call_id.set(function_call_id)
|
71
80
|
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
|
3
|
+
import importlib.abc
|
4
|
+
import json
|
5
|
+
import queue
|
6
|
+
import socket
|
7
|
+
import sys
|
8
|
+
import threading
|
9
|
+
import time
|
10
|
+
import uuid
|
11
|
+
from importlib.util import find_spec, module_from_spec
|
12
|
+
from struct import pack
|
13
|
+
|
14
|
+
from modal.config import logger
|
15
|
+
|
16
|
+
MODULE_LOAD_START = "module_load_start"
|
17
|
+
MODULE_LOAD_END = "module_load_end"
|
18
|
+
|
19
|
+
MESSAGE_HEADER_FORMAT = "<I"
|
20
|
+
MESSAGE_HEADER_LEN = 4
|
21
|
+
|
22
|
+
|
23
|
+
class InterceptedModuleLoader(importlib.abc.Loader):
|
24
|
+
def __init__(self, name, loader, interceptor):
|
25
|
+
self.name = name
|
26
|
+
self.loader = loader
|
27
|
+
self.interceptor = interceptor
|
28
|
+
|
29
|
+
def exec_module(self, module):
|
30
|
+
if self.loader is None:
|
31
|
+
return
|
32
|
+
try:
|
33
|
+
self.loader.exec_module(module)
|
34
|
+
finally:
|
35
|
+
self.interceptor.load_end(self.name)
|
36
|
+
|
37
|
+
def create_module(self, spec):
|
38
|
+
spec.loader = self.loader
|
39
|
+
module = module_from_spec(spec)
|
40
|
+
spec.loader = self
|
41
|
+
return module
|
42
|
+
|
43
|
+
def get_data(self, path: str) -> bytes:
|
44
|
+
"""
|
45
|
+
Implementation is required to support pkgutil.get_data.
|
46
|
+
|
47
|
+
> If the package cannot be located or loaded, or it uses a loader which does
|
48
|
+
> not support get_data, then None is returned.
|
49
|
+
|
50
|
+
ref: https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
|
51
|
+
"""
|
52
|
+
return self.loader.get_data(path)
|
53
|
+
|
54
|
+
def get_resource_reader(self, fullname: str):
|
55
|
+
"""
|
56
|
+
Support reading a binary artifact that is shipped within a package.
|
57
|
+
|
58
|
+
> Loaders that wish to support resource reading are expected to provide a method called
|
59
|
+
> get_resource_reader(fullname) which returns an object implementing this ABC’s interface.
|
60
|
+
|
61
|
+
ref: docs.python.org/3.10/library/importlib.html?highlight=traversableresources#importlib.abc.ResourceReader
|
62
|
+
"""
|
63
|
+
return self.loader.get_resource_reader(fullname)
|
64
|
+
|
65
|
+
|
66
|
+
class ImportInterceptor(importlib.abc.MetaPathFinder):
|
67
|
+
loading: dict[str, tuple[str, float]]
|
68
|
+
tracing_socket: socket.socket
|
69
|
+
events: queue.Queue
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def connect(cls, socket_filename: str) -> "ImportInterceptor":
|
73
|
+
tracing_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
74
|
+
tracing_socket.connect(socket_filename)
|
75
|
+
return cls(tracing_socket)
|
76
|
+
|
77
|
+
def __init__(self, tracing_socket: socket.socket):
|
78
|
+
self.loading = {}
|
79
|
+
self.tracing_socket = tracing_socket
|
80
|
+
self.events = queue.Queue(maxsize=16 * 1024)
|
81
|
+
sender = threading.Thread(target=self._send, daemon=True)
|
82
|
+
sender.start()
|
83
|
+
|
84
|
+
def find_spec(self, fullname, path, target=None):
|
85
|
+
if fullname in self.loading:
|
86
|
+
return None
|
87
|
+
self.load_start(fullname)
|
88
|
+
spec = find_spec(fullname)
|
89
|
+
if spec is None:
|
90
|
+
self.load_end(fullname)
|
91
|
+
return None
|
92
|
+
spec.loader = InterceptedModuleLoader(fullname, spec.loader, self)
|
93
|
+
return spec
|
94
|
+
|
95
|
+
def load_start(self, name):
|
96
|
+
t0 = time.monotonic()
|
97
|
+
span_id = str(uuid.uuid4())
|
98
|
+
self.emit(
|
99
|
+
{"span_id": span_id, "timestamp": time.time(), "event": MODULE_LOAD_START, "attributes": {"name": name}}
|
100
|
+
)
|
101
|
+
self.loading[name] = (span_id, t0)
|
102
|
+
|
103
|
+
def load_end(self, name):
|
104
|
+
span_id, t0 = self.loading.pop(name, (None, None))
|
105
|
+
if t0 is None:
|
106
|
+
return
|
107
|
+
latency = time.monotonic() - t0
|
108
|
+
self.emit(
|
109
|
+
{
|
110
|
+
"span_id": span_id,
|
111
|
+
"timestamp": time.time(),
|
112
|
+
"event": MODULE_LOAD_END,
|
113
|
+
"attributes": {
|
114
|
+
"name": name,
|
115
|
+
"latency": latency,
|
116
|
+
},
|
117
|
+
}
|
118
|
+
)
|
119
|
+
|
120
|
+
def emit(self, event):
|
121
|
+
try:
|
122
|
+
self.events.put_nowait(event)
|
123
|
+
except queue.Full:
|
124
|
+
logger.debug("failed to emit event: queue full")
|
125
|
+
|
126
|
+
def _send(self):
|
127
|
+
while True:
|
128
|
+
event = self.events.get()
|
129
|
+
try:
|
130
|
+
msg = json.dumps(event).encode("utf-8")
|
131
|
+
except BaseException as e:
|
132
|
+
logger.debug(f"failed to serialize event: {e}")
|
133
|
+
continue
|
134
|
+
try:
|
135
|
+
encoded_len = pack(MESSAGE_HEADER_FORMAT, len(msg))
|
136
|
+
self.tracing_socket.send(encoded_len + msg)
|
137
|
+
except OSError as e:
|
138
|
+
logger.debug(f"failed to send event: {e}")
|
139
|
+
|
140
|
+
def install(self):
|
141
|
+
sys.meta_path = [self] + sys.meta_path # type: ignore
|
142
|
+
|
143
|
+
def remove(self):
|
144
|
+
sys.meta_path.remove(self) # type: ignore
|
145
|
+
|
146
|
+
def __enter__(self):
|
147
|
+
self.install()
|
148
|
+
|
149
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
150
|
+
self.remove()
|
151
|
+
|
152
|
+
|
153
|
+
def _instrument_imports(socket_filename: str):
|
154
|
+
if not supported_platform():
|
155
|
+
logger.debug("unsupported platform, not instrumenting imports")
|
156
|
+
return
|
157
|
+
interceptor = ImportInterceptor.connect(socket_filename)
|
158
|
+
interceptor.install()
|
159
|
+
|
160
|
+
|
161
|
+
def instrument_imports(socket_filename: str):
|
162
|
+
try:
|
163
|
+
_instrument_imports(socket_filename)
|
164
|
+
except BaseException as e:
|
165
|
+
logger.warning(f"failed to instrument imports: {e}")
|
166
|
+
|
167
|
+
|
168
|
+
def supported_platform():
|
169
|
+
return sys.platform in ("linux", "darwin")
|
@@ -0,0 +1,356 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
import importlib
|
3
|
+
import typing
|
4
|
+
from abc import ABCMeta, abstractmethod
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Any, Callable, Optional
|
7
|
+
|
8
|
+
import modal._runtime.container_io_manager
|
9
|
+
import modal.cls
|
10
|
+
import modal.object
|
11
|
+
from modal import Function
|
12
|
+
from modal._utils.async_utils import synchronizer
|
13
|
+
from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
|
14
|
+
from modal.exception import ExecutionError, InvalidError
|
15
|
+
from modal.functions import _Function
|
16
|
+
from modal.partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
|
17
|
+
from modal_proto import api_pb2
|
18
|
+
|
19
|
+
if typing.TYPE_CHECKING:
|
20
|
+
import modal.app
|
21
|
+
import modal.partial_function
|
22
|
+
from modal._runtime.asgi import LifespanManager
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class FinalizedFunction:
|
27
|
+
callable: Callable[..., Any]
|
28
|
+
is_async: bool
|
29
|
+
is_generator: bool
|
30
|
+
data_format: int # api_pb2.DataFormat
|
31
|
+
lifespan_manager: Optional["LifespanManager"] = None
|
32
|
+
|
33
|
+
|
34
|
+
class Service(metaclass=ABCMeta):
|
35
|
+
"""Common interface for singular functions and class-based "services"
|
36
|
+
|
37
|
+
There are differences in the importing/finalization logic, and this
|
38
|
+
"protocol"/abc basically defines a common interface for the two types
|
39
|
+
of "Services" after the point of import.
|
40
|
+
"""
|
41
|
+
|
42
|
+
user_cls_instance: Any
|
43
|
+
app: Optional["modal.app._App"]
|
44
|
+
code_deps: Optional[list["modal.object._Object"]]
|
45
|
+
|
46
|
+
@abstractmethod
|
47
|
+
def get_finalized_functions(
|
48
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
49
|
+
) -> dict[str, "FinalizedFunction"]:
|
50
|
+
...
|
51
|
+
|
52
|
+
|
53
|
+
def construct_webhook_callable(
|
54
|
+
user_defined_callable: Callable,
|
55
|
+
webhook_config: api_pb2.WebhookConfig,
|
56
|
+
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
57
|
+
):
|
58
|
+
# Note: aiohttp is a significant dependency of the `asgi` module, so we import it locally
|
59
|
+
from modal._runtime import asgi
|
60
|
+
|
61
|
+
# For webhooks, the user function is used to construct an asgi app:
|
62
|
+
if webhook_config.type == api_pb2.WEBHOOK_TYPE_ASGI_APP:
|
63
|
+
# Function returns an asgi_app, which we can use as a callable.
|
64
|
+
return asgi.asgi_app_wrapper(user_defined_callable(), container_io_manager)
|
65
|
+
|
66
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WSGI_APP:
|
67
|
+
# Function returns an wsgi_app, which we can use as a callable
|
68
|
+
return asgi.wsgi_app_wrapper(user_defined_callable(), container_io_manager)
|
69
|
+
|
70
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_FUNCTION:
|
71
|
+
# Function is a webhook without an ASGI app. Create one for it.
|
72
|
+
return asgi.asgi_app_wrapper(
|
73
|
+
asgi.webhook_asgi_app(user_defined_callable, webhook_config.method, webhook_config.web_endpoint_docs),
|
74
|
+
container_io_manager,
|
75
|
+
)
|
76
|
+
|
77
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WEB_SERVER:
|
78
|
+
# Function spawns an HTTP web server listening at a port.
|
79
|
+
user_defined_callable()
|
80
|
+
|
81
|
+
# We intentionally try to connect to the external interface instead of the loopback
|
82
|
+
# interface here so users are forced to expose the server. This allows us to potentially
|
83
|
+
# change the implementation to use an external bridge in the future.
|
84
|
+
host = asgi.get_ip_address(b"eth0")
|
85
|
+
port = webhook_config.web_server_port
|
86
|
+
startup_timeout = webhook_config.web_server_startup_timeout
|
87
|
+
asgi.wait_for_web_server(host, port, timeout=startup_timeout)
|
88
|
+
return asgi.asgi_app_wrapper(asgi.web_server_proxy(host, port), container_io_manager)
|
89
|
+
else:
|
90
|
+
raise InvalidError(f"Unrecognized web endpoint type {webhook_config.type}")
|
91
|
+
|
92
|
+
|
93
|
+
@dataclass
|
94
|
+
class ImportedFunction(Service):
|
95
|
+
user_cls_instance: Any
|
96
|
+
app: Optional["modal.app._App"]
|
97
|
+
code_deps: Optional[list["modal.object._Object"]]
|
98
|
+
|
99
|
+
_user_defined_callable: Callable[..., Any]
|
100
|
+
|
101
|
+
def get_finalized_functions(
|
102
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
103
|
+
) -> dict[str, "FinalizedFunction"]:
|
104
|
+
# Check this property before we turn it into a method (overriden by webhooks)
|
105
|
+
is_async = get_is_async(self._user_defined_callable)
|
106
|
+
# Use the function definition for whether this is a generator (overriden by webhooks)
|
107
|
+
is_generator = fun_def.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
|
108
|
+
|
109
|
+
webhook_config = fun_def.webhook_config
|
110
|
+
if not webhook_config.type:
|
111
|
+
# for non-webhooks, the runnable is straight forward:
|
112
|
+
return {
|
113
|
+
"": FinalizedFunction(
|
114
|
+
callable=self._user_defined_callable,
|
115
|
+
is_async=is_async,
|
116
|
+
is_generator=is_generator,
|
117
|
+
data_format=api_pb2.DATA_FORMAT_PICKLE,
|
118
|
+
)
|
119
|
+
}
|
120
|
+
|
121
|
+
web_callable, lifespan_manager = construct_webhook_callable(
|
122
|
+
self._user_defined_callable, fun_def.webhook_config, container_io_manager
|
123
|
+
)
|
124
|
+
|
125
|
+
return {
|
126
|
+
"": FinalizedFunction(
|
127
|
+
callable=web_callable,
|
128
|
+
lifespan_manager=lifespan_manager,
|
129
|
+
is_async=True,
|
130
|
+
is_generator=True,
|
131
|
+
data_format=api_pb2.DATA_FORMAT_ASGI,
|
132
|
+
)
|
133
|
+
}
|
134
|
+
|
135
|
+
|
136
|
+
@dataclass
|
137
|
+
class ImportedClass(Service):
|
138
|
+
user_cls_instance: Any
|
139
|
+
app: Optional["modal.app._App"]
|
140
|
+
code_deps: Optional[list["modal.object._Object"]]
|
141
|
+
|
142
|
+
_partial_functions: dict[str, "modal.partial_function._PartialFunction"]
|
143
|
+
|
144
|
+
def get_finalized_functions(
|
145
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
146
|
+
) -> dict[str, "FinalizedFunction"]:
|
147
|
+
finalized_functions = {}
|
148
|
+
for method_name, partial in self._partial_functions.items():
|
149
|
+
partial = synchronizer._translate_in(partial) # ugly
|
150
|
+
user_func = partial.raw_f
|
151
|
+
# Check this property before we turn it into a method (overriden by webhooks)
|
152
|
+
is_async = get_is_async(user_func)
|
153
|
+
# Use the function definition for whether this is a generator (overriden by webhooks)
|
154
|
+
is_generator = partial.is_generator
|
155
|
+
webhook_config = partial.webhook_config
|
156
|
+
|
157
|
+
bound_func = user_func.__get__(self.user_cls_instance)
|
158
|
+
|
159
|
+
if not webhook_config or webhook_config.type == api_pb2.WEBHOOK_TYPE_UNSPECIFIED:
|
160
|
+
# for non-webhooks, the runnable is straight forward:
|
161
|
+
finalized_function = FinalizedFunction(
|
162
|
+
callable=bound_func,
|
163
|
+
is_async=is_async,
|
164
|
+
is_generator=is_generator,
|
165
|
+
data_format=api_pb2.DATA_FORMAT_PICKLE,
|
166
|
+
)
|
167
|
+
else:
|
168
|
+
web_callable, lifespan_manager = construct_webhook_callable(
|
169
|
+
bound_func, webhook_config, container_io_manager
|
170
|
+
)
|
171
|
+
finalized_function = FinalizedFunction(
|
172
|
+
callable=web_callable,
|
173
|
+
lifespan_manager=lifespan_manager,
|
174
|
+
is_async=True,
|
175
|
+
is_generator=True,
|
176
|
+
data_format=api_pb2.DATA_FORMAT_ASGI,
|
177
|
+
)
|
178
|
+
finalized_functions[method_name] = finalized_function
|
179
|
+
return finalized_functions
|
180
|
+
|
181
|
+
|
182
|
+
def get_user_class_instance(cls: typing.Union[type, modal.cls.Cls], args: tuple, kwargs: dict[str, Any]) -> typing.Any:
|
183
|
+
"""Returns instance of the underlying class to be used as the `self`
|
184
|
+
|
185
|
+
The input `cls` can either be the raw Python class the user has declared ("user class"),
|
186
|
+
or an @app.cls-decorated version of it which is a modal.Cls-instance wrapping the user class.
|
187
|
+
"""
|
188
|
+
if isinstance(cls, modal.cls.Cls):
|
189
|
+
# globally @app.cls-decorated class
|
190
|
+
modal_obj: modal.cls.Obj = cls(*args, **kwargs)
|
191
|
+
modal_obj._entered = True # ugly but prevents .local() from triggering additional enter-logic
|
192
|
+
# TODO: unify lifecycle logic between .local() and container_entrypoint
|
193
|
+
user_cls_instance = modal_obj._cached_user_cls_instance()
|
194
|
+
else:
|
195
|
+
# undecorated class (non-global decoration or serialized)
|
196
|
+
user_cls_instance = cls(*args, **kwargs)
|
197
|
+
|
198
|
+
return user_cls_instance
|
199
|
+
|
200
|
+
|
201
|
+
def import_single_function_service(
|
202
|
+
function_def: api_pb2.Function,
|
203
|
+
ser_cls, # used only for @build functions
|
204
|
+
ser_fun,
|
205
|
+
cls_args, # used only for @build functions
|
206
|
+
cls_kwargs, # used only for @build functions
|
207
|
+
) -> Service:
|
208
|
+
"""Imports a function dynamically, and locates the app.
|
209
|
+
|
210
|
+
This is somewhat complex because we're dealing with 3 quite different type of functions:
|
211
|
+
1. Functions defined in global scope and decorated in global scope (Function objects)
|
212
|
+
2. Functions defined in global scope but decorated elsewhere (these will be raw callables)
|
213
|
+
3. Serialized functions
|
214
|
+
|
215
|
+
In addition, we also need to handle
|
216
|
+
* Normal functions
|
217
|
+
* Methods on classes (in which case we need to instantiate the object)
|
218
|
+
|
219
|
+
This helper also handles web endpoints, ASGI/WSGI servers, and HTTP servers.
|
220
|
+
|
221
|
+
In order to locate the app, we try two things:
|
222
|
+
* If the function is a Function, we can get the app directly from it
|
223
|
+
* Otherwise, use the app name and look it up from a global list of apps: this
|
224
|
+
typically only happens in case 2 above, or in sometimes for case 3
|
225
|
+
|
226
|
+
Note that `import_function` is *not* synchronized, because we need it to run on the main
|
227
|
+
thread. This is so that any user code running in global scope (which executes as a part of
|
228
|
+
the import) runs on the right thread.
|
229
|
+
"""
|
230
|
+
user_defined_callable: Callable
|
231
|
+
function: Optional[_Function] = None
|
232
|
+
code_deps: Optional[list["modal.object._Object"]] = None
|
233
|
+
active_app: Optional[modal.app._App] = None
|
234
|
+
|
235
|
+
if ser_fun is not None:
|
236
|
+
# This is a serialized function we already fetched from the server
|
237
|
+
cls, user_defined_callable = ser_cls, ser_fun
|
238
|
+
else:
|
239
|
+
# Load the module dynamically
|
240
|
+
module = importlib.import_module(function_def.module_name)
|
241
|
+
qual_name: str = function_def.function_name
|
242
|
+
|
243
|
+
if not is_global_object(qual_name):
|
244
|
+
raise LocalFunctionError("Attempted to load a function defined in a function scope")
|
245
|
+
|
246
|
+
parts = qual_name.split(".")
|
247
|
+
if len(parts) == 1:
|
248
|
+
# This is a function
|
249
|
+
cls = None
|
250
|
+
f = getattr(module, qual_name)
|
251
|
+
if isinstance(f, Function):
|
252
|
+
function = synchronizer._translate_in(f)
|
253
|
+
user_defined_callable = function.get_raw_f()
|
254
|
+
active_app = function._app
|
255
|
+
else:
|
256
|
+
user_defined_callable = f
|
257
|
+
elif len(parts) == 2:
|
258
|
+
# As of v0.63 - this path should only be triggered by @build class builder methods
|
259
|
+
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
260
|
+
assert function_def.is_builder_function
|
261
|
+
cls_name, fun_name = parts
|
262
|
+
cls = getattr(module, cls_name)
|
263
|
+
if isinstance(cls, modal.cls.Cls):
|
264
|
+
# The cls decorator is in global scope
|
265
|
+
_cls = synchronizer._translate_in(cls)
|
266
|
+
user_defined_callable = _cls._callables[fun_name]
|
267
|
+
function = _cls._method_functions.get(
|
268
|
+
fun_name
|
269
|
+
) # bound to the class service function - there is no instance
|
270
|
+
active_app = _cls._app
|
271
|
+
else:
|
272
|
+
# This is non-decorated class
|
273
|
+
user_defined_callable = getattr(cls, fun_name)
|
274
|
+
else:
|
275
|
+
raise InvalidError(f"Invalid function qualname {qual_name}")
|
276
|
+
|
277
|
+
# Instantiate the class if it's defined
|
278
|
+
if cls:
|
279
|
+
# This code is only used for @build methods on classes
|
280
|
+
user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
|
281
|
+
# Bind the function to the instance as self (using the descriptor protocol!)
|
282
|
+
user_defined_callable = user_defined_callable.__get__(user_cls_instance)
|
283
|
+
else:
|
284
|
+
user_cls_instance = None
|
285
|
+
|
286
|
+
if function:
|
287
|
+
code_deps = function.deps(only_explicit_mounts=True)
|
288
|
+
|
289
|
+
return ImportedFunction(
|
290
|
+
user_cls_instance,
|
291
|
+
active_app,
|
292
|
+
code_deps,
|
293
|
+
user_defined_callable,
|
294
|
+
)
|
295
|
+
|
296
|
+
|
297
|
+
def import_class_service(
|
298
|
+
function_def: api_pb2.Function,
|
299
|
+
ser_cls,
|
300
|
+
cls_args,
|
301
|
+
cls_kwargs,
|
302
|
+
) -> Service:
|
303
|
+
"""
|
304
|
+
This imports a full class to be able to execute any @method or webhook decorated methods.
|
305
|
+
|
306
|
+
See import_function.
|
307
|
+
"""
|
308
|
+
active_app: Optional["modal.app._App"]
|
309
|
+
code_deps: Optional[list["modal.object._Object"]]
|
310
|
+
cls: typing.Union[type, modal.cls.Cls]
|
311
|
+
|
312
|
+
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
313
|
+
assert ser_cls is not None
|
314
|
+
cls = ser_cls
|
315
|
+
else:
|
316
|
+
# Load the module dynamically
|
317
|
+
module = importlib.import_module(function_def.module_name)
|
318
|
+
qual_name: str = function_def.function_name
|
319
|
+
|
320
|
+
if not is_global_object(qual_name):
|
321
|
+
raise LocalFunctionError("Attempted to load a class defined in a function scope")
|
322
|
+
|
323
|
+
parts = qual_name.split(".")
|
324
|
+
if not (
|
325
|
+
len(parts) == 2 and parts[1] == "*"
|
326
|
+
): # the "function name" of a class service "function placeholder" is expected to be "ClassName.*"
|
327
|
+
raise ExecutionError(
|
328
|
+
f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
|
329
|
+
)
|
330
|
+
|
331
|
+
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
332
|
+
cls_name = parts[0]
|
333
|
+
cls = getattr(module, cls_name)
|
334
|
+
|
335
|
+
if isinstance(cls, modal.cls.Cls):
|
336
|
+
# The cls decorator is in global scope
|
337
|
+
_cls = synchronizer._translate_in(cls)
|
338
|
+
method_partials = _cls._get_partial_functions()
|
339
|
+
service_function: _Function = _cls._class_service_function
|
340
|
+
code_deps = service_function.deps(only_explicit_mounts=True)
|
341
|
+
active_app = service_function.app
|
342
|
+
else:
|
343
|
+
# Undecorated user class - find all methods
|
344
|
+
method_partials = _find_partial_methods_for_user_cls(cls, _PartialFunctionFlags.all())
|
345
|
+
code_deps = None
|
346
|
+
active_app = None
|
347
|
+
|
348
|
+
user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
|
349
|
+
|
350
|
+
return ImportedClass(
|
351
|
+
user_cls_instance,
|
352
|
+
active_app,
|
353
|
+
code_deps,
|
354
|
+
# TODO (elias/deven): instead of using method_partials here we should use a set of api_pb2.MethodDefinition
|
355
|
+
method_partials,
|
356
|
+
)
|