modal 0.73.116__py3-none-any.whl → 0.73.128__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 +2 -0
- modal/_functions.py +19 -8
- modal/_partial_function.py +54 -0
- modal/_runtime/container_io_manager.py +13 -9
- modal/_runtime/container_io_manager.pyi +7 -4
- modal/_serialization.py +92 -44
- modal/_utils/async_utils.py +71 -6
- modal/_utils/function_utils.py +33 -13
- modal/_utils/jwt_utils.py +38 -0
- modal/app.py +34 -5
- modal/app.pyi +3 -2
- modal/cli/app.py +15 -0
- modal/client.pyi +2 -2
- modal/cls.py +3 -13
- modal/cls.pyi +0 -2
- modal/functions.pyi +8 -7
- modal/parallel_map.py +393 -44
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +2 -0
- modal/partial_function.pyi +9 -0
- modal/retries.py +11 -9
- modal/sandbox.py +5 -1
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/METADATA +1 -1
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/RECORD +36 -35
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/WHEEL +1 -1
- modal_proto/api.proto +15 -2
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +284 -263
- modal_proto/api_pb2.pyi +49 -6
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/_version_generated.py +1 -1
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/LICENSE +0 -0
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/entry_points.txt +0 -0
- {modal-0.73.116.dist-info → modal-0.73.128.dist-info}/top_level.txt +0 -0
modal/_utils/function_utils.py
CHANGED
@@ -15,7 +15,14 @@ from synchronicity.exceptions import UserCodeException
|
|
15
15
|
import modal_proto
|
16
16
|
from modal_proto import api_pb2
|
17
17
|
|
18
|
-
from .._serialization import
|
18
|
+
from .._serialization import (
|
19
|
+
PROTO_TYPE_INFO,
|
20
|
+
PYTHON_TO_PROTO_TYPE,
|
21
|
+
deserialize,
|
22
|
+
deserialize_data_format,
|
23
|
+
get_proto_parameter_type,
|
24
|
+
serialize,
|
25
|
+
)
|
19
26
|
from .._traceback import append_modal_tb
|
20
27
|
from ..config import config, logger
|
21
28
|
from ..exception import (
|
@@ -99,6 +106,24 @@ def get_function_type(is_generator: Optional[bool]) -> "api_pb2.Function.Functio
|
|
99
106
|
return api_pb2.Function.FUNCTION_TYPE_GENERATOR if is_generator else api_pb2.Function.FUNCTION_TYPE_FUNCTION
|
100
107
|
|
101
108
|
|
109
|
+
def signature_to_protobuf_schema(signature: inspect.Signature) -> list[api_pb2.ClassParameterSpec]:
|
110
|
+
modal_parameters: list[api_pb2.ClassParameterSpec] = []
|
111
|
+
for param in signature.parameters.values():
|
112
|
+
has_default = param.default is not param.empty
|
113
|
+
class_param_spec = api_pb2.ClassParameterSpec(name=param.name, has_default=has_default)
|
114
|
+
if param.annotation not in PYTHON_TO_PROTO_TYPE:
|
115
|
+
class_param_spec.type = api_pb2.PARAM_TYPE_UNKNOWN
|
116
|
+
else:
|
117
|
+
proto_type = PYTHON_TO_PROTO_TYPE[param.annotation]
|
118
|
+
class_param_spec.type = proto_type
|
119
|
+
proto_type_info = PROTO_TYPE_INFO[proto_type]
|
120
|
+
if has_default and proto_type is not api_pb2.PARAM_TYPE_UNKNOWN:
|
121
|
+
setattr(class_param_spec, proto_type_info.default_field, param.default)
|
122
|
+
|
123
|
+
modal_parameters.append(class_param_spec)
|
124
|
+
return modal_parameters
|
125
|
+
|
126
|
+
|
102
127
|
class FunctionInfo:
|
103
128
|
"""Utility that determines serialization/deserialization mechanisms for functions
|
104
129
|
|
@@ -277,28 +302,23 @@ class FunctionInfo:
|
|
277
302
|
return api_pb2.ClassParameterInfo()
|
278
303
|
|
279
304
|
# TODO(elias): Resolve circular dependencies... maybe we'll need some cls_utils module
|
280
|
-
from modal.cls import _get_class_constructor_signature, _use_annotation_parameters
|
305
|
+
from modal.cls import _get_class_constructor_signature, _use_annotation_parameters
|
281
306
|
|
282
307
|
if not _use_annotation_parameters(self.user_cls):
|
283
308
|
return api_pb2.ClassParameterInfo(format=api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PICKLE)
|
284
309
|
|
285
310
|
# annotation parameters trigger strictly typed parametrization
|
286
311
|
# which enables web endpoint for parametrized classes
|
287
|
-
|
288
|
-
modal_parameters: list[api_pb2.ClassParameterSpec] = []
|
289
312
|
signature = _get_class_constructor_signature(self.user_cls)
|
313
|
+
# validate that the schema has no unspecified fields/unsupported class parameter types
|
290
314
|
for param in signature.parameters.values():
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
proto_type_info = PROTO_TYPE_INFO[proto_type]
|
295
|
-
class_param_spec = api_pb2.ClassParameterSpec(name=param.name, has_default=has_default, type=proto_type)
|
296
|
-
if has_default:
|
297
|
-
setattr(class_param_spec, proto_type_info.default_field, param.default)
|
298
|
-
modal_parameters.append(class_param_spec)
|
315
|
+
get_proto_parameter_type(param.annotation)
|
316
|
+
|
317
|
+
protobuf_schema = signature_to_protobuf_schema(signature)
|
299
318
|
|
300
319
|
return api_pb2.ClassParameterInfo(
|
301
|
-
format=api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO,
|
320
|
+
format=api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO,
|
321
|
+
schema=protobuf_schema,
|
302
322
|
)
|
303
323
|
|
304
324
|
def get_entrypoint_mount(self) -> dict[str, _Mount]:
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Copyright Modal Labs 2025
|
2
|
+
import base64
|
3
|
+
import json
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Any, Dict
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class DecodedJwt:
|
10
|
+
header: Dict[str, Any]
|
11
|
+
payload: Dict[str, Any]
|
12
|
+
|
13
|
+
@staticmethod
|
14
|
+
def decode_without_verification(token: str) -> "DecodedJwt":
|
15
|
+
# Split the JWT into its three parts
|
16
|
+
header_b64, payload_b64, _ = token.split(".")
|
17
|
+
|
18
|
+
# Decode Base64 (with padding handling)
|
19
|
+
header_json = base64.urlsafe_b64decode(header_b64 + "==").decode("utf-8")
|
20
|
+
payload_json = base64.urlsafe_b64decode(payload_b64 + "==").decode("utf-8")
|
21
|
+
|
22
|
+
# Convert JSON strings to dictionaries
|
23
|
+
header = json.loads(header_json)
|
24
|
+
payload = json.loads(payload_json)
|
25
|
+
|
26
|
+
return DecodedJwt(header, payload)
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def _base64url_encode(data: str) -> str:
|
30
|
+
"""Encodes data to Base64 URL-safe format without padding."""
|
31
|
+
return base64.urlsafe_b64encode(data.encode()).rstrip(b"=").decode()
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def encode_without_signature(fields: Dict[str, Any]) -> str:
|
35
|
+
"""Encodes an Unsecured JWT (without a signature)."""
|
36
|
+
header_b64 = DecodedJwt._base64url_encode(json.dumps({"alg": "none", "typ": "JWT"}))
|
37
|
+
payload_b64 = DecodedJwt._base64url_encode(json.dumps(fields))
|
38
|
+
return f"{header_b64}.{payload_b64}." # No signature
|
modal/app.py
CHANGED
@@ -678,6 +678,12 @@ class _App:
|
|
678
678
|
is_generator = f.is_generator
|
679
679
|
batch_max_size = f.batch_max_size
|
680
680
|
batch_wait_ms = f.batch_wait_ms
|
681
|
+
if f.max_concurrent_inputs: # Using @modal.concurrent()
|
682
|
+
max_concurrent_inputs = f.max_concurrent_inputs
|
683
|
+
target_concurrent_inputs = f.target_concurrent_inputs
|
684
|
+
else:
|
685
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
686
|
+
target_concurrent_inputs = None
|
681
687
|
else:
|
682
688
|
if not is_global_object(f.__qualname__) and not serialized:
|
683
689
|
raise InvalidError(
|
@@ -709,10 +715,12 @@ class _App:
|
|
709
715
|
)
|
710
716
|
|
711
717
|
info = FunctionInfo(f, serialized=serialized, name_override=name)
|
718
|
+
raw_f = f
|
712
719
|
webhook_config = None
|
713
720
|
batch_max_size = None
|
714
721
|
batch_wait_ms = None
|
715
|
-
|
722
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
723
|
+
target_concurrent_inputs = None
|
716
724
|
|
717
725
|
cluster_size = None # Experimental: Clustered functions
|
718
726
|
i6pn_enabled = i6pn
|
@@ -753,7 +761,8 @@ class _App:
|
|
753
761
|
max_containers=max_containers,
|
754
762
|
buffer_containers=buffer_containers,
|
755
763
|
scaledown_window=scaledown_window,
|
756
|
-
|
764
|
+
max_concurrent_inputs=max_concurrent_inputs,
|
765
|
+
target_concurrent_inputs=target_concurrent_inputs,
|
757
766
|
batch_max_size=batch_max_size,
|
758
767
|
batch_wait_ms=batch_wait_ms,
|
759
768
|
timeout=timeout,
|
@@ -832,7 +841,7 @@ class _App:
|
|
832
841
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
833
842
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
834
843
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
835
|
-
) -> Callable[[CLS_T], CLS_T]:
|
844
|
+
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
836
845
|
"""
|
837
846
|
Decorator to register a new Modal [Cls](/docs/reference/modal.Cls) with this App.
|
838
847
|
"""
|
@@ -845,8 +854,21 @@ class _App:
|
|
845
854
|
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
846
855
|
scheduler_placement = SchedulerPlacement(region=region)
|
847
856
|
|
848
|
-
def wrapper(
|
857
|
+
def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
|
849
858
|
# Check if the decorated object is a class
|
859
|
+
if isinstance(wrapped_cls, _PartialFunction):
|
860
|
+
wrapped_cls.wrapped = True
|
861
|
+
user_cls = wrapped_cls.raw_f
|
862
|
+
if wrapped_cls.max_concurrent_inputs: # Using @modal.concurrent()
|
863
|
+
max_concurrent_inputs = wrapped_cls.max_concurrent_inputs
|
864
|
+
target_concurrent_inputs = wrapped_cls.target_concurrent_inputs
|
865
|
+
else:
|
866
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
867
|
+
target_concurrent_inputs = None
|
868
|
+
else:
|
869
|
+
user_cls = wrapped_cls
|
870
|
+
max_concurrent_inputs = allow_concurrent_inputs
|
871
|
+
target_concurrent_inputs = None
|
850
872
|
if not inspect.isclass(user_cls):
|
851
873
|
raise TypeError("The @app.cls decorator must be used on a class.")
|
852
874
|
|
@@ -871,6 +893,12 @@ class _App:
|
|
871
893
|
):
|
872
894
|
raise InvalidError("A class must have `enable_memory_snapshot=True` to use `snap=True` on its methods.")
|
873
895
|
|
896
|
+
for method in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.FUNCTION).values():
|
897
|
+
if method.max_concurrent_inputs:
|
898
|
+
raise InvalidError(
|
899
|
+
"The `@modal.concurrent` decorator cannot be used on methods; decorate the class instead."
|
900
|
+
)
|
901
|
+
|
874
902
|
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)
|
875
903
|
|
876
904
|
cls_func = _Function.from_local(
|
@@ -892,7 +920,8 @@ class _App:
|
|
892
920
|
scaledown_window=scaledown_window,
|
893
921
|
proxy=proxy,
|
894
922
|
retries=retries,
|
895
|
-
|
923
|
+
max_concurrent_inputs=max_concurrent_inputs,
|
924
|
+
target_concurrent_inputs=target_concurrent_inputs,
|
896
925
|
batch_max_size=batch_max_size,
|
897
926
|
batch_wait_ms=batch_wait_ms,
|
898
927
|
timeout=timeout,
|
modal/app.pyi
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import collections.abc
|
2
2
|
import modal._functions
|
3
3
|
import modal._object
|
4
|
+
import modal._partial_function
|
4
5
|
import modal._utils.function_utils
|
5
6
|
import modal.client
|
6
7
|
import modal.cloud_bucket_mount
|
@@ -247,7 +248,7 @@ class _App:
|
|
247
248
|
concurrency_limit: typing.Optional[int] = None,
|
248
249
|
container_idle_timeout: typing.Optional[int] = None,
|
249
250
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
250
|
-
) -> collections.abc.Callable[[CLS_T], CLS_T]: ...
|
251
|
+
) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]: ...
|
251
252
|
async def spawn_sandbox(
|
252
253
|
self,
|
253
254
|
*entrypoint_args: str,
|
@@ -487,7 +488,7 @@ class App:
|
|
487
488
|
concurrency_limit: typing.Optional[int] = None,
|
488
489
|
container_idle_timeout: typing.Optional[int] = None,
|
489
490
|
_experimental_buffer_containers: typing.Optional[int] = None,
|
490
|
-
) -> collections.abc.Callable[[CLS_T], CLS_T]: ...
|
491
|
+
) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]: ...
|
491
492
|
|
492
493
|
class __spawn_sandbox_spec(typing_extensions.Protocol[SUPERSELF]):
|
493
494
|
def __call__(
|
modal/cli/app.py
CHANGED
@@ -227,6 +227,8 @@ async def history(
|
|
227
227
|
]
|
228
228
|
rows = []
|
229
229
|
deployments_with_tags = False
|
230
|
+
deployments_with_commit_info = False
|
231
|
+
deployments_with_dirty_commit = False
|
230
232
|
for idx, app_stats in enumerate(resp.app_deployment_histories):
|
231
233
|
style = "bold green" if idx == 0 else ""
|
232
234
|
|
@@ -241,10 +243,23 @@ async def history(
|
|
241
243
|
deployments_with_tags = True
|
242
244
|
row.append(Text(app_stats.tag, style=style))
|
243
245
|
|
246
|
+
if app_stats.commit_info.commit_hash:
|
247
|
+
deployments_with_commit_info = True
|
248
|
+
short_hash = app_stats.commit_info.commit_hash[:7]
|
249
|
+
if app_stats.commit_info.dirty:
|
250
|
+
deployments_with_dirty_commit = True
|
251
|
+
short_hash = f"{short_hash}*"
|
252
|
+
row.append(Text(short_hash, style=style))
|
253
|
+
|
244
254
|
rows.append(row)
|
245
255
|
|
246
256
|
if deployments_with_tags:
|
247
257
|
columns.append("Tag")
|
258
|
+
if deployments_with_commit_info:
|
259
|
+
columns.append("Commit")
|
248
260
|
|
249
261
|
rows = sorted(rows, key=lambda x: int(str(x[0])[1:]), reverse=True)
|
250
262
|
display_table(columns, rows, json)
|
263
|
+
|
264
|
+
if deployments_with_dirty_commit and not json:
|
265
|
+
rich.print("* - repo had uncommitted changes")
|
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[tuple[str, str]],
|
34
|
-
version: str = "0.73.
|
34
|
+
version: str = "0.73.128",
|
35
35
|
): ...
|
36
36
|
def is_closed(self) -> bool: ...
|
37
37
|
@property
|
@@ -93,7 +93,7 @@ class Client:
|
|
93
93
|
server_url: str,
|
94
94
|
client_type: int,
|
95
95
|
credentials: typing.Optional[tuple[str, str]],
|
96
|
-
version: str = "0.73.
|
96
|
+
version: str = "0.73.128",
|
97
97
|
): ...
|
98
98
|
def is_closed(self) -> bool: ...
|
99
99
|
@property
|
modal/cls.py
CHANGED
@@ -21,7 +21,7 @@ from ._partial_function import (
|
|
21
21
|
)
|
22
22
|
from ._resolver import Resolver
|
23
23
|
from ._resources import convert_fn_config_to_resources_config
|
24
|
-
from ._serialization import
|
24
|
+
from ._serialization import check_valid_cls_constructor_arg, get_proto_parameter_type
|
25
25
|
from ._traceback import print_server_warnings
|
26
26
|
from ._utils.async_utils import synchronize_api, synchronizer
|
27
27
|
from ._utils.deprecation import deprecation_warning, renamed_parameter, warn_on_renamed_autoscaler_settings
|
@@ -362,15 +362,6 @@ class _Obj:
|
|
362
362
|
Obj = synchronize_api(_Obj)
|
363
363
|
|
364
364
|
|
365
|
-
def _validate_parameter_type(cls_name: str, parameter_name: str, parameter_type: type):
|
366
|
-
if parameter_type not in PYTHON_TO_PROTO_TYPE:
|
367
|
-
type_name = getattr(parameter_type, "__name__", repr(parameter_type))
|
368
|
-
supported = ", ".join(parameter_type.__name__ for parameter_type in PYTHON_TO_PROTO_TYPE.keys())
|
369
|
-
raise InvalidError(
|
370
|
-
f"{cls_name}.{parameter_name}: {type_name} is not a supported parameter type. Use one of: {supported}"
|
371
|
-
)
|
372
|
-
|
373
|
-
|
374
365
|
class _Cls(_Object, type_prefix="cs"):
|
375
366
|
"""
|
376
367
|
Cls adds method pooling and [lifecycle hook](/docs/guide/lifecycle-functions) behavior
|
@@ -467,12 +458,11 @@ class _Cls(_Object, type_prefix="cs"):
|
|
467
458
|
annotations = user_cls.__dict__.get("__annotations__", {}) # compatible with older pythons
|
468
459
|
missing_annotations = params.keys() - annotations.keys()
|
469
460
|
if missing_annotations:
|
470
|
-
raise InvalidError("All modal.parameter() specifications need to be type
|
461
|
+
raise InvalidError("All modal.parameter() specifications need to be type-annotated")
|
471
462
|
|
472
463
|
annotated_params = {k: t for k, t in annotations.items() if k in params}
|
473
464
|
for k, t in annotated_params.items():
|
474
|
-
|
475
|
-
_validate_parameter_type(user_cls.__name__, k, t)
|
465
|
+
get_proto_parameter_type(t)
|
476
466
|
|
477
467
|
@staticmethod
|
478
468
|
def from_local(user_cls, app: "modal.app._App", class_service_function: _Function) -> "_Cls":
|
modal/cls.pyi
CHANGED
@@ -109,8 +109,6 @@ class Obj:
|
|
109
109
|
async def _aenter(self): ...
|
110
110
|
def __getattr__(self, k): ...
|
111
111
|
|
112
|
-
def _validate_parameter_type(cls_name: str, parameter_name: str, parameter_type: type): ...
|
113
|
-
|
114
112
|
class _Cls(modal._object._Object):
|
115
113
|
_class_service_function: typing.Optional[modal._functions._Function]
|
116
114
|
_options: typing.Optional[_ServiceOptions]
|
modal/functions.pyi
CHANGED
@@ -82,7 +82,8 @@ class Function(
|
|
82
82
|
max_containers: typing.Optional[int] = None,
|
83
83
|
buffer_containers: typing.Optional[int] = None,
|
84
84
|
scaledown_window: typing.Optional[int] = None,
|
85
|
-
|
85
|
+
max_concurrent_inputs: typing.Optional[int] = None,
|
86
|
+
target_concurrent_inputs: typing.Optional[int] = None,
|
86
87
|
batch_max_size: typing.Optional[int] = None,
|
87
88
|
batch_wait_ms: typing.Optional[int] = None,
|
88
89
|
cloud: typing.Optional[str] = None,
|
@@ -198,11 +199,11 @@ class Function(
|
|
198
199
|
|
199
200
|
_call_generator_nowait: ___call_generator_nowait_spec[typing_extensions.Self]
|
200
201
|
|
201
|
-
class __remote_spec(typing_extensions.Protocol[
|
202
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
202
203
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
203
204
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
204
205
|
|
205
|
-
remote: __remote_spec[modal._functions.
|
206
|
+
remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
206
207
|
|
207
208
|
class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
|
208
209
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -217,19 +218,19 @@ class Function(
|
|
217
218
|
self, *args: modal._functions.P.args, **kwargs: modal._functions.P.kwargs
|
218
219
|
) -> modal._functions.OriginalReturnType: ...
|
219
220
|
|
220
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
221
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
221
222
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
222
223
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
223
224
|
|
224
225
|
_experimental_spawn: ___experimental_spawn_spec[
|
225
|
-
modal._functions.
|
226
|
+
modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
|
226
227
|
]
|
227
228
|
|
228
|
-
class __spawn_spec(typing_extensions.Protocol[
|
229
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
229
230
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
230
231
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
231
232
|
|
232
|
-
spawn: __spawn_spec[modal._functions.
|
233
|
+
spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
233
234
|
|
234
235
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]: ...
|
235
236
|
|