modal 0.66.14__py3-none-any.whl → 0.66.39__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 +1 -1
- modal/_container_entrypoint.py +27 -358
- modal/_runtime/__init__.py +1 -0
- modal/{_asgi.py → _runtime/asgi.py} +8 -7
- modal/{_container_io_manager.py → _runtime/container_io_manager.py} +18 -27
- modal/{execution_context.py → _runtime/execution_context.py} +2 -1
- modal/{_telemetry.py → _runtime/telemetry.py} +1 -1
- modal/_runtime/user_code_imports.py +361 -0
- modal/_serialization.py +1 -1
- modal/_utils/function_utils.py +28 -8
- modal/app.py +13 -46
- modal/cli/import_refs.py +4 -38
- modal/client.pyi +2 -2
- modal/dict.py +0 -6
- modal/dict.pyi +0 -4
- modal/experimental.py +1 -4
- modal/functions.py +11 -10
- modal/functions.pyi +8 -8
- modal/gpu.py +8 -6
- modal/image.py +93 -6
- modal/image.pyi +20 -2
- modal/io_streams.py +32 -12
- modal/io_streams.pyi +8 -4
- modal/mount.py +3 -2
- modal/network_file_system.py +0 -28
- modal/network_file_system.pyi +0 -14
- modal/parallel_map.py +1 -1
- modal/partial_function.py +11 -1
- modal/queue.py +0 -6
- modal/queue.pyi +0 -4
- modal/runner.py +1 -1
- modal/sandbox.py +1 -1
- modal/secret.py +1 -1
- modal/volume.py +0 -22
- modal/volume.pyi +0 -9
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/METADATA +1 -2
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/RECORD +49 -49
- modal_proto/api.proto +2 -21
- modal_proto/api_grpc.py +0 -16
- modal_proto/api_pb2.py +702 -726
- modal_proto/api_pb2.pyi +6 -60
- modal_proto/api_pb2_grpc.py +0 -33
- modal_proto/api_pb2_grpc.pyi +0 -10
- modal_proto/modal_api_grpc.py +0 -1
- modal_version/_version_generated.py +1 -1
- modal/_container_io_manager.pyi +0 -414
- modal/execution_context.pyi +0 -22
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/LICENSE +0 -0
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/WHEEL +0 -0
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/entry_points.txt +0 -0
- {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,361 @@
|
|
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, Dict, List, 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._runtime.asgi import (
|
13
|
+
LifespanManager,
|
14
|
+
asgi_app_wrapper,
|
15
|
+
get_ip_address,
|
16
|
+
wait_for_web_server,
|
17
|
+
web_server_proxy,
|
18
|
+
webhook_asgi_app,
|
19
|
+
wsgi_app_wrapper,
|
20
|
+
)
|
21
|
+
from modal._utils.async_utils import synchronizer
|
22
|
+
from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
|
23
|
+
from modal.exception import ExecutionError, InvalidError
|
24
|
+
from modal.functions import _Function
|
25
|
+
from modal.partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
|
26
|
+
from modal_proto import api_pb2
|
27
|
+
|
28
|
+
if typing.TYPE_CHECKING:
|
29
|
+
import modal.app
|
30
|
+
import modal.partial_function
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class FinalizedFunction:
|
35
|
+
callable: Callable[..., Any]
|
36
|
+
is_async: bool
|
37
|
+
is_generator: bool
|
38
|
+
data_format: int # api_pb2.DataFormat
|
39
|
+
lifespan_manager: Optional[LifespanManager] = None
|
40
|
+
|
41
|
+
|
42
|
+
class Service(metaclass=ABCMeta):
|
43
|
+
"""Common interface for singular functions and class-based "services"
|
44
|
+
|
45
|
+
There are differences in the importing/finalization logic, and this
|
46
|
+
"protocol"/abc basically defines a common interface for the two types
|
47
|
+
of "Services" after the point of import.
|
48
|
+
"""
|
49
|
+
|
50
|
+
user_cls_instance: Any
|
51
|
+
app: Optional["modal.app._App"]
|
52
|
+
code_deps: Optional[List["modal.object._Object"]]
|
53
|
+
|
54
|
+
@abstractmethod
|
55
|
+
def get_finalized_functions(
|
56
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
57
|
+
) -> Dict[str, "FinalizedFunction"]:
|
58
|
+
...
|
59
|
+
|
60
|
+
|
61
|
+
def construct_webhook_callable(
|
62
|
+
user_defined_callable: Callable,
|
63
|
+
webhook_config: api_pb2.WebhookConfig,
|
64
|
+
container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
|
65
|
+
):
|
66
|
+
# For webhooks, the user function is used to construct an asgi app:
|
67
|
+
if webhook_config.type == api_pb2.WEBHOOK_TYPE_ASGI_APP:
|
68
|
+
# Function returns an asgi_app, which we can use as a callable.
|
69
|
+
return asgi_app_wrapper(user_defined_callable(), container_io_manager)
|
70
|
+
|
71
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WSGI_APP:
|
72
|
+
# Function returns an wsgi_app, which we can use as a callable.
|
73
|
+
return wsgi_app_wrapper(user_defined_callable(), container_io_manager)
|
74
|
+
|
75
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_FUNCTION:
|
76
|
+
# Function is a webhook without an ASGI app. Create one for it.
|
77
|
+
return asgi_app_wrapper(
|
78
|
+
webhook_asgi_app(user_defined_callable, webhook_config.method, webhook_config.web_endpoint_docs),
|
79
|
+
container_io_manager,
|
80
|
+
)
|
81
|
+
|
82
|
+
elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WEB_SERVER:
|
83
|
+
# Function spawns an HTTP web server listening at a port.
|
84
|
+
user_defined_callable()
|
85
|
+
|
86
|
+
# We intentionally try to connect to the external interface instead of the loopback
|
87
|
+
# interface here so users are forced to expose the server. This allows us to potentially
|
88
|
+
# change the implementation to use an external bridge in the future.
|
89
|
+
host = get_ip_address(b"eth0")
|
90
|
+
port = webhook_config.web_server_port
|
91
|
+
startup_timeout = webhook_config.web_server_startup_timeout
|
92
|
+
wait_for_web_server(host, port, timeout=startup_timeout)
|
93
|
+
return asgi_app_wrapper(web_server_proxy(host, port), container_io_manager)
|
94
|
+
else:
|
95
|
+
raise InvalidError(f"Unrecognized web endpoint type {webhook_config.type}")
|
96
|
+
|
97
|
+
|
98
|
+
@dataclass
|
99
|
+
class ImportedFunction(Service):
|
100
|
+
user_cls_instance: Any
|
101
|
+
app: Optional["modal.app._App"]
|
102
|
+
code_deps: Optional[List["modal.object._Object"]]
|
103
|
+
|
104
|
+
_user_defined_callable: Callable[..., Any]
|
105
|
+
|
106
|
+
def get_finalized_functions(
|
107
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
108
|
+
) -> Dict[str, "FinalizedFunction"]:
|
109
|
+
# Check this property before we turn it into a method (overriden by webhooks)
|
110
|
+
is_async = get_is_async(self._user_defined_callable)
|
111
|
+
# Use the function definition for whether this is a generator (overriden by webhooks)
|
112
|
+
is_generator = fun_def.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
|
113
|
+
|
114
|
+
webhook_config = fun_def.webhook_config
|
115
|
+
if not webhook_config.type:
|
116
|
+
# for non-webhooks, the runnable is straight forward:
|
117
|
+
return {
|
118
|
+
"": FinalizedFunction(
|
119
|
+
callable=self._user_defined_callable,
|
120
|
+
is_async=is_async,
|
121
|
+
is_generator=is_generator,
|
122
|
+
data_format=api_pb2.DATA_FORMAT_PICKLE,
|
123
|
+
)
|
124
|
+
}
|
125
|
+
|
126
|
+
web_callable, lifespan_manager = construct_webhook_callable(
|
127
|
+
self._user_defined_callable, fun_def.webhook_config, container_io_manager
|
128
|
+
)
|
129
|
+
|
130
|
+
return {
|
131
|
+
"": FinalizedFunction(
|
132
|
+
callable=web_callable,
|
133
|
+
lifespan_manager=lifespan_manager,
|
134
|
+
is_async=True,
|
135
|
+
is_generator=True,
|
136
|
+
data_format=api_pb2.DATA_FORMAT_ASGI,
|
137
|
+
)
|
138
|
+
}
|
139
|
+
|
140
|
+
|
141
|
+
@dataclass
|
142
|
+
class ImportedClass(Service):
|
143
|
+
user_cls_instance: Any
|
144
|
+
app: Optional["modal.app._App"]
|
145
|
+
code_deps: Optional[List["modal.object._Object"]]
|
146
|
+
|
147
|
+
_partial_functions: Dict[str, "modal.partial_function._PartialFunction"]
|
148
|
+
|
149
|
+
def get_finalized_functions(
|
150
|
+
self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
|
151
|
+
) -> Dict[str, "FinalizedFunction"]:
|
152
|
+
finalized_functions = {}
|
153
|
+
for method_name, partial in self._partial_functions.items():
|
154
|
+
partial = synchronizer._translate_in(partial) # ugly
|
155
|
+
user_func = partial.raw_f
|
156
|
+
# Check this property before we turn it into a method (overriden by webhooks)
|
157
|
+
is_async = get_is_async(user_func)
|
158
|
+
# Use the function definition for whether this is a generator (overriden by webhooks)
|
159
|
+
is_generator = partial.is_generator
|
160
|
+
webhook_config = partial.webhook_config
|
161
|
+
|
162
|
+
bound_func = user_func.__get__(self.user_cls_instance)
|
163
|
+
|
164
|
+
if not webhook_config or webhook_config.type == api_pb2.WEBHOOK_TYPE_UNSPECIFIED:
|
165
|
+
# for non-webhooks, the runnable is straight forward:
|
166
|
+
finalized_function = FinalizedFunction(
|
167
|
+
callable=bound_func,
|
168
|
+
is_async=is_async,
|
169
|
+
is_generator=is_generator,
|
170
|
+
data_format=api_pb2.DATA_FORMAT_PICKLE,
|
171
|
+
)
|
172
|
+
else:
|
173
|
+
web_callable, lifespan_manager = construct_webhook_callable(
|
174
|
+
bound_func, webhook_config, container_io_manager
|
175
|
+
)
|
176
|
+
finalized_function = FinalizedFunction(
|
177
|
+
callable=web_callable,
|
178
|
+
lifespan_manager=lifespan_manager,
|
179
|
+
is_async=True,
|
180
|
+
is_generator=True,
|
181
|
+
data_format=api_pb2.DATA_FORMAT_ASGI,
|
182
|
+
)
|
183
|
+
finalized_functions[method_name] = finalized_function
|
184
|
+
return finalized_functions
|
185
|
+
|
186
|
+
|
187
|
+
def get_user_class_instance(
|
188
|
+
cls: typing.Union[type, modal.cls.Cls], args: typing.Tuple, kwargs: Dict[str, Any]
|
189
|
+
) -> typing.Any:
|
190
|
+
"""Returns instance of the underlying class to be used as the `self`
|
191
|
+
|
192
|
+
The input `cls` can either be the raw Python class the user has declared ("user class"),
|
193
|
+
or an @app.cls-decorated version of it which is a modal.Cls-instance wrapping the user class.
|
194
|
+
"""
|
195
|
+
if isinstance(cls, modal.cls.Cls):
|
196
|
+
# globally @app.cls-decorated class
|
197
|
+
modal_obj: modal.cls.Obj = cls(*args, **kwargs)
|
198
|
+
modal_obj.entered = True # ugly but prevents .local() from triggering additional enter-logic
|
199
|
+
# TODO: unify lifecycle logic between .local() and container_entrypoint
|
200
|
+
user_cls_instance = modal_obj._get_user_cls_instance()
|
201
|
+
else:
|
202
|
+
# undecorated class (non-global decoration or serialized)
|
203
|
+
user_cls_instance = cls(*args, **kwargs)
|
204
|
+
|
205
|
+
return user_cls_instance
|
206
|
+
|
207
|
+
|
208
|
+
def import_single_function_service(
|
209
|
+
function_def: api_pb2.Function,
|
210
|
+
ser_cls, # used only for @build functions
|
211
|
+
ser_fun,
|
212
|
+
cls_args, # used only for @build functions
|
213
|
+
cls_kwargs, # used only for @build functions
|
214
|
+
) -> Service:
|
215
|
+
"""Imports a function dynamically, and locates the app.
|
216
|
+
|
217
|
+
This is somewhat complex because we're dealing with 3 quite different type of functions:
|
218
|
+
1. Functions defined in global scope and decorated in global scope (Function objects)
|
219
|
+
2. Functions defined in global scope but decorated elsewhere (these will be raw callables)
|
220
|
+
3. Serialized functions
|
221
|
+
|
222
|
+
In addition, we also need to handle
|
223
|
+
* Normal functions
|
224
|
+
* Methods on classes (in which case we need to instantiate the object)
|
225
|
+
|
226
|
+
This helper also handles web endpoints, ASGI/WSGI servers, and HTTP servers.
|
227
|
+
|
228
|
+
In order to locate the app, we try two things:
|
229
|
+
* If the function is a Function, we can get the app directly from it
|
230
|
+
* Otherwise, use the app name and look it up from a global list of apps: this
|
231
|
+
typically only happens in case 2 above, or in sometimes for case 3
|
232
|
+
|
233
|
+
Note that `import_function` is *not* synchronized, because we need it to run on the main
|
234
|
+
thread. This is so that any user code running in global scope (which executes as a part of
|
235
|
+
the import) runs on the right thread.
|
236
|
+
"""
|
237
|
+
user_defined_callable: Callable
|
238
|
+
function: Optional[_Function] = None
|
239
|
+
code_deps: Optional[List["modal.object._Object"]] = None
|
240
|
+
active_app: Optional[modal.app._App] = None
|
241
|
+
|
242
|
+
if ser_fun is not None:
|
243
|
+
# This is a serialized function we already fetched from the server
|
244
|
+
cls, user_defined_callable = ser_cls, ser_fun
|
245
|
+
else:
|
246
|
+
# Load the module dynamically
|
247
|
+
module = importlib.import_module(function_def.module_name)
|
248
|
+
qual_name: str = function_def.function_name
|
249
|
+
|
250
|
+
if not is_global_object(qual_name):
|
251
|
+
raise LocalFunctionError("Attempted to load a function defined in a function scope")
|
252
|
+
|
253
|
+
parts = qual_name.split(".")
|
254
|
+
if len(parts) == 1:
|
255
|
+
# This is a function
|
256
|
+
cls = None
|
257
|
+
f = getattr(module, qual_name)
|
258
|
+
if isinstance(f, Function):
|
259
|
+
function = synchronizer._translate_in(f)
|
260
|
+
user_defined_callable = function.get_raw_f()
|
261
|
+
active_app = function._app
|
262
|
+
else:
|
263
|
+
user_defined_callable = f
|
264
|
+
elif len(parts) == 2:
|
265
|
+
# As of v0.63 - this path should only be triggered by @build class builder methods
|
266
|
+
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
267
|
+
assert function_def.is_builder_function
|
268
|
+
cls_name, fun_name = parts
|
269
|
+
cls = getattr(module, cls_name)
|
270
|
+
if isinstance(cls, modal.cls.Cls):
|
271
|
+
# The cls decorator is in global scope
|
272
|
+
_cls = synchronizer._translate_in(cls)
|
273
|
+
user_defined_callable = _cls._callables[fun_name]
|
274
|
+
function = _cls._method_functions.get(fun_name)
|
275
|
+
active_app = _cls._app
|
276
|
+
else:
|
277
|
+
# This is a raw class
|
278
|
+
user_defined_callable = getattr(cls, fun_name)
|
279
|
+
else:
|
280
|
+
raise InvalidError(f"Invalid function qualname {qual_name}")
|
281
|
+
|
282
|
+
# Instantiate the class if it's defined
|
283
|
+
if cls:
|
284
|
+
# This code is only used for @build methods on classes
|
285
|
+
user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
|
286
|
+
# Bind the function to the instance as self (using the descriptor protocol!)
|
287
|
+
user_defined_callable = user_defined_callable.__get__(user_cls_instance)
|
288
|
+
else:
|
289
|
+
user_cls_instance = None
|
290
|
+
|
291
|
+
if function:
|
292
|
+
code_deps = function.deps(only_explicit_mounts=True)
|
293
|
+
|
294
|
+
return ImportedFunction(
|
295
|
+
user_cls_instance,
|
296
|
+
active_app,
|
297
|
+
code_deps,
|
298
|
+
user_defined_callable,
|
299
|
+
)
|
300
|
+
|
301
|
+
|
302
|
+
def import_class_service(
|
303
|
+
function_def: api_pb2.Function,
|
304
|
+
ser_cls,
|
305
|
+
cls_args,
|
306
|
+
cls_kwargs,
|
307
|
+
) -> Service:
|
308
|
+
"""
|
309
|
+
This imports a full class to be able to execute any @method or webhook decorated methods.
|
310
|
+
|
311
|
+
See import_function.
|
312
|
+
"""
|
313
|
+
active_app: Optional["modal.app._App"]
|
314
|
+
code_deps: Optional[List["modal.object._Object"]]
|
315
|
+
cls: typing.Union[type, modal.cls.Cls]
|
316
|
+
|
317
|
+
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
318
|
+
assert ser_cls is not None
|
319
|
+
cls = ser_cls
|
320
|
+
else:
|
321
|
+
# Load the module dynamically
|
322
|
+
module = importlib.import_module(function_def.module_name)
|
323
|
+
qual_name: str = function_def.function_name
|
324
|
+
|
325
|
+
if not is_global_object(qual_name):
|
326
|
+
raise LocalFunctionError("Attempted to load a class defined in a function scope")
|
327
|
+
|
328
|
+
parts = qual_name.split(".")
|
329
|
+
if not (
|
330
|
+
len(parts) == 2 and parts[1] == "*"
|
331
|
+
): # the "function name" of a class service "function placeholder" is expected to be "ClassName.*"
|
332
|
+
raise ExecutionError(
|
333
|
+
f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
|
334
|
+
)
|
335
|
+
|
336
|
+
assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
|
337
|
+
cls_name = parts[0]
|
338
|
+
cls = getattr(module, cls_name)
|
339
|
+
|
340
|
+
if isinstance(cls, modal.cls.Cls):
|
341
|
+
# The cls decorator is in global scope
|
342
|
+
_cls = synchronizer._translate_in(cls)
|
343
|
+
method_partials = _cls._get_partial_functions()
|
344
|
+
service_function: _Function = _cls._class_service_function
|
345
|
+
code_deps = service_function.deps(only_explicit_mounts=True)
|
346
|
+
active_app = service_function.app
|
347
|
+
else:
|
348
|
+
# Undecorated user class - find all methods
|
349
|
+
method_partials = _find_partial_methods_for_user_cls(cls, _PartialFunctionFlags.all())
|
350
|
+
code_deps = None
|
351
|
+
active_app = None
|
352
|
+
|
353
|
+
user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
|
354
|
+
|
355
|
+
return ImportedClass(
|
356
|
+
user_cls_instance,
|
357
|
+
active_app,
|
358
|
+
code_deps,
|
359
|
+
# TODO (elias/deven): instead of using method_partials here we should use a set of api_pb2.MethodDefinition
|
360
|
+
method_partials,
|
361
|
+
)
|
modal/_serialization.py
CHANGED
@@ -90,7 +90,7 @@ def serialize(obj: Any) -> bytes:
|
|
90
90
|
|
91
91
|
def deserialize(s: bytes, client) -> Any:
|
92
92
|
"""Deserializes object and replaces all client placeholders by self."""
|
93
|
-
from .execution_context import is_local # Avoid circular import
|
93
|
+
from ._runtime.execution_context import is_local # Avoid circular import
|
94
94
|
|
95
95
|
env = "local" if is_local() else "remote"
|
96
96
|
try:
|
modal/_utils/function_utils.py
CHANGED
@@ -519,6 +519,16 @@ async def _create_input(
|
|
519
519
|
)
|
520
520
|
|
521
521
|
|
522
|
+
def _get_suffix_from_web_url_info(url_info: api_pb2.WebUrlInfo) -> str:
|
523
|
+
if url_info.truncated:
|
524
|
+
suffix = " [grey70](label truncated)[/grey70]"
|
525
|
+
elif url_info.label_stolen:
|
526
|
+
suffix = " [grey70](label stolen)[/grey70]"
|
527
|
+
else:
|
528
|
+
suffix = ""
|
529
|
+
return suffix
|
530
|
+
|
531
|
+
|
522
532
|
class FunctionCreationStatus:
|
523
533
|
# TODO(michael) this really belongs with other output-related code
|
524
534
|
# but moving it here so we can use it when loading a function with output disabled
|
@@ -547,12 +557,7 @@ class FunctionCreationStatus:
|
|
547
557
|
elif self.response.function.web_url:
|
548
558
|
url_info = self.response.function.web_url_info
|
549
559
|
# Ensure terms used here match terms used in modal.com/docs/guide/webhook-urls doc.
|
550
|
-
|
551
|
-
suffix = " [grey70](label truncated)[/grey70]"
|
552
|
-
elif url_info.label_stolen:
|
553
|
-
suffix = " [grey70](label stolen)[/grey70]"
|
554
|
-
else:
|
555
|
-
suffix = ""
|
560
|
+
suffix = _get_suffix_from_web_url_info(url_info)
|
556
561
|
# TODO: this is only printed when we're showing progress. Maybe move this somewhere else.
|
557
562
|
web_url = self.response.handle_metadata.web_url
|
558
563
|
self.status_row.finish(
|
@@ -563,8 +568,23 @@ class FunctionCreationStatus:
|
|
563
568
|
for custom_domain in self.response.function.custom_domain_info:
|
564
569
|
custom_domain_status_row = self.resolver.add_status_row()
|
565
570
|
custom_domain_status_row.finish(
|
566
|
-
f"Custom domain for {self.tag} => [magenta underline]"
|
567
|
-
f"{custom_domain.url}[/magenta underline]{suffix}"
|
571
|
+
f"Custom domain for {self.tag} => [magenta underline]" f"{custom_domain.url}[/magenta underline]"
|
568
572
|
)
|
569
573
|
else:
|
570
574
|
self.status_row.finish(f"Created function {self.tag}.")
|
575
|
+
if self.response.function.method_definitions_set:
|
576
|
+
for method_definition in self.response.function.method_definitions.values():
|
577
|
+
if method_definition.web_url:
|
578
|
+
url_info = method_definition.web_url_info
|
579
|
+
suffix = _get_suffix_from_web_url_info(url_info)
|
580
|
+
class_web_endpoint_method_status_row = self.resolver.add_status_row()
|
581
|
+
class_web_endpoint_method_status_row.finish(
|
582
|
+
f"Created web endpoint for {method_definition.function_name} => [magenta underline]"
|
583
|
+
f"{method_definition.web_url}[/magenta underline]{suffix}"
|
584
|
+
)
|
585
|
+
for custom_domain in method_definition.custom_domain_info:
|
586
|
+
custom_domain_status_row = self.resolver.add_status_row()
|
587
|
+
custom_domain_status_row.finish(
|
588
|
+
f"Custom domain for {method_definition.function_name} => [magenta underline]"
|
589
|
+
f"{custom_domain.url}[/magenta underline]"
|
590
|
+
)
|
modal/app.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
import inspect
|
3
|
-
import os
|
4
3
|
import typing
|
5
4
|
import warnings
|
6
5
|
from pathlib import PurePosixPath
|
@@ -42,7 +41,6 @@ from .image import _Image
|
|
42
41
|
from .mount import _Mount
|
43
42
|
from .network_file_system import _NetworkFileSystem
|
44
43
|
from .object import _get_environment_name, _Object
|
45
|
-
from .output import _get_output_manager, enable_output
|
46
44
|
from .partial_function import (
|
47
45
|
PartialFunction,
|
48
46
|
_find_partial_methods_for_user_cls,
|
@@ -141,25 +139,9 @@ def f(x, y):
|
|
141
139
|
```
|
142
140
|
"""
|
143
141
|
|
144
|
-
_enable_output_warning = """\
|
145
|
-
Note that output will soon not be be printed with `app.run`.
|
146
|
-
|
147
|
-
If you want to print output, use `modal.enable_output()`:
|
148
|
-
|
149
|
-
```python
|
150
|
-
with modal.enable_output():
|
151
|
-
with app.run():
|
152
|
-
...
|
153
|
-
```
|
154
|
-
|
155
|
-
If you don't want output, and you want to to suppress this warning,
|
156
|
-
use `app.run(..., show_progress=False)`.
|
157
|
-
"""
|
158
|
-
|
159
142
|
|
160
143
|
class _App:
|
161
|
-
"""A Modal
|
162
|
-
deployed together.
|
144
|
+
"""A Modal App is a group of functions and classes that are deployed together.
|
163
145
|
|
164
146
|
The app serves at least three purposes:
|
165
147
|
|
@@ -447,32 +429,16 @@ class _App:
|
|
447
429
|
|
448
430
|
# See Github discussion here: https://github.com/modal-labs/modal-client/pull/2030#issuecomment-2237266186
|
449
431
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
elif show_progress is True:
|
458
|
-
if _get_output_manager() is None:
|
459
|
-
deprecation_warning((2024, 7, 18), _enable_output_warning)
|
460
|
-
auto_enable_output = True
|
461
|
-
else:
|
462
|
-
deprecation_warning((2024, 7, 18), "`show_progress=True` is deprecated and no longer needed.")
|
463
|
-
elif show_progress is False:
|
464
|
-
if _get_output_manager() is not None:
|
465
|
-
deprecation_warning(
|
466
|
-
(2024, 7, 18), "`show_progress=False` will have no effect since output is enabled."
|
467
|
-
)
|
432
|
+
if show_progress is True:
|
433
|
+
deprecation_error(
|
434
|
+
(2024, 11, 20),
|
435
|
+
"`show_progress=True` is no longer supported. Use `with modal.enable_output():` instead.",
|
436
|
+
)
|
437
|
+
elif show_progress is False:
|
438
|
+
deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
|
468
439
|
|
469
|
-
|
470
|
-
|
471
|
-
async with _run_app(self, client=client, detach=detach, interactive=interactive):
|
472
|
-
yield self
|
473
|
-
else:
|
474
|
-
async with _run_app(self, client=client, detach=detach, interactive=interactive):
|
475
|
-
yield self
|
440
|
+
async with _run_app(self, client=client, detach=detach, interactive=interactive):
|
441
|
+
yield self
|
476
442
|
|
477
443
|
def _get_default_image(self):
|
478
444
|
if self._image:
|
@@ -488,7 +454,7 @@ class _App:
|
|
488
454
|
*self._mounts,
|
489
455
|
]
|
490
456
|
for function in self.registered_functions.values():
|
491
|
-
all_mounts.extend(function.
|
457
|
+
all_mounts.extend(function._serve_mounts)
|
492
458
|
|
493
459
|
return [m for m in all_mounts if m.is_local()]
|
494
460
|
|
@@ -1083,7 +1049,8 @@ App = synchronize_api(_App)
|
|
1083
1049
|
|
1084
1050
|
|
1085
1051
|
class _Stub(_App):
|
1086
|
-
"""
|
1052
|
+
"""mdmd:hidden
|
1053
|
+
This enables using a "Stub" class instead of "App".
|
1087
1054
|
|
1088
1055
|
For most of Modal's history, the app class was called "Stub", so this exists for
|
1089
1056
|
backwards compatibility, in order to facilitate moving from "Stub" to "App".
|
modal/cli/import_refs.py
CHANGED
@@ -19,7 +19,7 @@ from rich.console import Console
|
|
19
19
|
from rich.markdown import Markdown
|
20
20
|
|
21
21
|
from modal.app import App, LocalEntrypoint
|
22
|
-
from modal.exception import InvalidError, _CliUserExecutionError
|
22
|
+
from modal.exception import InvalidError, _CliUserExecutionError
|
23
23
|
from modal.functions import Function
|
24
24
|
|
25
25
|
|
@@ -79,7 +79,7 @@ def import_file_or_module(file_or_module: str):
|
|
79
79
|
return module
|
80
80
|
|
81
81
|
|
82
|
-
def get_by_object_path(obj: Any, obj_path:
|
82
|
+
def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
|
83
83
|
# Try to evaluate a `.`-delimited object path in a Modal context
|
84
84
|
# With the caveat that some object names can actually have `.` in their name (lifecycled methods' tags)
|
85
85
|
|
@@ -107,35 +107,6 @@ def get_by_object_path(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
|
107
107
|
return obj
|
108
108
|
|
109
109
|
|
110
|
-
def get_by_object_path_try_possible_app_names(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
111
|
-
"""This just exists as a dumb workaround to support both "stub" and "app" """
|
112
|
-
|
113
|
-
if obj_path:
|
114
|
-
return get_by_object_path(obj, obj_path)
|
115
|
-
else:
|
116
|
-
app = get_by_object_path(obj, DEFAULT_APP_NAME)
|
117
|
-
stub = get_by_object_path(obj, "stub")
|
118
|
-
if isinstance(app, App):
|
119
|
-
return app
|
120
|
-
elif app is not None and isinstance(stub, App):
|
121
|
-
deprecation_warning(
|
122
|
-
(2024, 4, 20),
|
123
|
-
"The symbol `app` is present at the module level but it's not a Modal app."
|
124
|
-
" We will use `stub` instead, but this will not work in future Modal versions."
|
125
|
-
" Suggestion: change the name of `app` to something else.",
|
126
|
-
)
|
127
|
-
return stub
|
128
|
-
elif isinstance(stub, App):
|
129
|
-
deprecation_warning(
|
130
|
-
(2024, 5, 1),
|
131
|
-
"The symbol `app` is not present but `stub` is. This will not work in future"
|
132
|
-
" Modal versions. Suggestion: change the name of `stub` to `app`.",
|
133
|
-
)
|
134
|
-
return stub
|
135
|
-
else:
|
136
|
-
return None
|
137
|
-
|
138
|
-
|
139
110
|
def _infer_function_or_help(
|
140
111
|
app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
|
141
112
|
) -> Union[Function, LocalEntrypoint]:
|
@@ -210,7 +181,7 @@ def import_app(app_ref: str) -> App:
|
|
210
181
|
import_ref = parse_import_ref(app_ref)
|
211
182
|
|
212
183
|
module = import_file_or_module(import_ref.file_or_module)
|
213
|
-
app =
|
184
|
+
app = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
214
185
|
|
215
186
|
if app is None:
|
216
187
|
_show_no_auto_detectable_app(import_ref)
|
@@ -258,7 +229,7 @@ def import_function(
|
|
258
229
|
import_ref = parse_import_ref(func_ref)
|
259
230
|
|
260
231
|
module = import_file_or_module(import_ref.file_or_module)
|
261
|
-
app_or_function =
|
232
|
+
app_or_function = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
262
233
|
|
263
234
|
if app_or_function is None:
|
264
235
|
_show_function_ref_help(import_ref, base_cmd)
|
@@ -279,8 +250,3 @@ def import_function(
|
|
279
250
|
return app_or_function
|
280
251
|
else:
|
281
252
|
raise click.UsageError(f"{app_or_function} is not a Modal entity (should be an App or Function)")
|
282
|
-
|
283
|
-
|
284
|
-
# For backwards compatibility - delete soon
|
285
|
-
# We use it in our internal intergration tests
|
286
|
-
import_stub = import_app
|
modal/client.pyi
CHANGED
@@ -31,7 +31,7 @@ class _Client:
|
|
31
31
|
server_url: str,
|
32
32
|
client_type: int,
|
33
33
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
34
|
-
version: str = "0.66.
|
34
|
+
version: str = "0.66.39",
|
35
35
|
): ...
|
36
36
|
def is_closed(self) -> bool: ...
|
37
37
|
@property
|
@@ -90,7 +90,7 @@ class Client:
|
|
90
90
|
server_url: str,
|
91
91
|
client_type: int,
|
92
92
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
93
|
-
version: str = "0.66.
|
93
|
+
version: str = "0.66.39",
|
94
94
|
): ...
|
95
95
|
def is_closed(self) -> bool: ...
|
96
96
|
@property
|
modal/dict.py
CHANGED
@@ -143,12 +143,6 @@ class _Dict(_Object, type_prefix="di"):
|
|
143
143
|
|
144
144
|
return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
|
145
145
|
|
146
|
-
@staticmethod
|
147
|
-
def persisted(label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None):
|
148
|
-
"""mdmd:hidden"""
|
149
|
-
message = "`Dict.persisted` is deprecated. Please use `Dict.from_name(name, create_if_missing=True)` instead."
|
150
|
-
deprecation_error((2024, 3, 1), message)
|
151
|
-
|
152
146
|
@staticmethod
|
153
147
|
async def lookup(
|
154
148
|
label: str,
|
modal/dict.pyi
CHANGED
@@ -27,8 +27,6 @@ class _Dict(modal.object._Object):
|
|
27
27
|
create_if_missing: bool = False,
|
28
28
|
) -> _Dict: ...
|
29
29
|
@staticmethod
|
30
|
-
def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
|
31
|
-
@staticmethod
|
32
30
|
async def lookup(
|
33
31
|
label: str,
|
34
32
|
data: typing.Optional[dict] = None,
|
@@ -79,8 +77,6 @@ class Dict(modal.object.Object):
|
|
79
77
|
environment_name: typing.Optional[str] = None,
|
80
78
|
create_if_missing: bool = False,
|
81
79
|
) -> Dict: ...
|
82
|
-
@staticmethod
|
83
|
-
def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
|
84
80
|
|
85
81
|
class __lookup_spec(typing_extensions.Protocol):
|
86
82
|
def __call__(
|