modal 0.62.16__py3-none-any.whl → 0.72.11__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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +420 -937
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +5 -7
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,89 @@
1
+ # Copyright Modal Labs 2024
2
+ from contextvars import ContextVar
3
+ from typing import Callable, Optional
4
+
5
+ from modal._utils.async_utils import synchronize_api
6
+ from modal.exception import InvalidError
7
+
8
+ from .container_io_manager import _ContainerIOManager
9
+
10
+
11
+ def is_local() -> bool:
12
+ """Returns if we are currently on the machine launching/deploying a Modal app
13
+
14
+ Returns `True` when executed locally on the user's machine.
15
+ Returns `False` when executed from a Modal container in the cloud.
16
+ """
17
+ return not _ContainerIOManager._singleton
18
+
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
+ """
26
+ container_io_manager = _ContainerIOManager._singleton
27
+ if not container_io_manager:
28
+ raise InvalidError("Interactivity only works inside a Modal container.")
29
+ else:
30
+ await container_io_manager.interact()
31
+
32
+
33
+ interact = synchronize_api(_interact)
34
+
35
+
36
+ def current_input_id() -> Optional[str]:
37
+ """Returns the input ID for the current input.
38
+
39
+ Can only be called from Modal function (i.e. in a container context).
40
+
41
+ ```python
42
+ from modal import current_input_id
43
+
44
+ @app.function()
45
+ def process_stuff():
46
+ print(f"Starting to process {current_input_id()}")
47
+ ```
48
+ """
49
+ try:
50
+ return _current_input_id.get()
51
+ except LookupError:
52
+ return None
53
+
54
+
55
+ def current_function_call_id() -> Optional[str]:
56
+ """Returns the function call ID for the current input.
57
+
58
+ Can only be called from Modal function (i.e. in a container context).
59
+
60
+ ```python
61
+ from modal import current_function_call_id
62
+
63
+ @app.function()
64
+ def process_stuff():
65
+ print(f"Starting to process input from {current_function_call_id()}")
66
+ ```
67
+ """
68
+ try:
69
+ return _current_function_call_id.get()
70
+ except LookupError:
71
+ return None
72
+
73
+
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]
78
+ input_token = _current_input_id.set(input_id)
79
+ function_call_token = _current_function_call_id.set(function_call_id)
80
+
81
+ def _reset_current_context_ids():
82
+ _current_input_id.reset(input_token)
83
+ _current_function_call_id.reset(function_call_token)
84
+
85
+ return _reset_current_context_ids
86
+
87
+
88
+ _current_input_id: ContextVar = ContextVar("_current_input_id")
89
+ _current_function_call_id: ContextVar = ContextVar("_current_function_call_id")
@@ -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
+ )