modal 1.1.5.dev43__py3-none-any.whl → 1.1.5.dev45__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/_container_entrypoint.py +19 -41
- modal/_functions.py +21 -14
- modal/_runtime/container_io_manager.py +252 -150
- modal/_runtime/container_io_manager.pyi +32 -48
- modal/_runtime/user_code_imports.py +15 -5
- modal/_serialization.py +57 -1
- modal/_utils/blob_utils.py +4 -0
- modal/_utils/function_utils.py +22 -8
- modal/app.py +4 -0
- modal/app.pyi +4 -0
- modal/client.pyi +2 -2
- modal/config.py +5 -0
- modal/dict.py +15 -2
- modal/dict.pyi +22 -6
- modal/functions.pyi +7 -6
- modal/image.py +10 -3
- modal/parallel_map.py +2 -4
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/METADATA +2 -1
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/RECORD +24 -24
- modal_version/__init__.py +1 -1
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev43.dist-info → modal-1.1.5.dev45.dist-info}/top_level.txt +0 -0
|
@@ -29,7 +29,7 @@ class FinalizedFunction:
|
|
|
29
29
|
callable: Callable[..., Any]
|
|
30
30
|
is_async: bool
|
|
31
31
|
is_generator: bool
|
|
32
|
-
|
|
32
|
+
supported_output_formats: Sequence["api_pb2.DataFormat.ValueType"]
|
|
33
33
|
lifespan_manager: Optional["LifespanManager"] = None
|
|
34
34
|
|
|
35
35
|
|
|
@@ -108,6 +108,7 @@ class ImportedFunction(Service):
|
|
|
108
108
|
is_generator = fun_def.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
|
|
109
109
|
|
|
110
110
|
webhook_config = fun_def.webhook_config
|
|
111
|
+
|
|
111
112
|
if not webhook_config.type:
|
|
112
113
|
# for non-webhooks, the runnable is straight forward:
|
|
113
114
|
return {
|
|
@@ -115,7 +116,10 @@ class ImportedFunction(Service):
|
|
|
115
116
|
callable=self._user_defined_callable,
|
|
116
117
|
is_async=is_async,
|
|
117
118
|
is_generator=is_generator,
|
|
118
|
-
|
|
119
|
+
supported_output_formats=fun_def.supported_output_formats
|
|
120
|
+
# FIXME (elias): the following `or [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]` is only
|
|
121
|
+
# needed for tests
|
|
122
|
+
or [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR],
|
|
119
123
|
)
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -129,7 +133,8 @@ class ImportedFunction(Service):
|
|
|
129
133
|
lifespan_manager=lifespan_manager,
|
|
130
134
|
is_async=True,
|
|
131
135
|
is_generator=True,
|
|
132
|
-
|
|
136
|
+
# FIXME (elias): the following `or [api_pb2.DATA_FORMAT_ASGI]` is only needed for tests
|
|
137
|
+
supported_output_formats=fun_def.supported_output_formats or [api_pb2.DATA_FORMAT_ASGI],
|
|
133
138
|
)
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -154,6 +159,7 @@ class ImportedClass(Service):
|
|
|
154
159
|
# Use the function definition for whether this is a generator (overriden by webhooks)
|
|
155
160
|
is_generator = _partial.params.is_generator
|
|
156
161
|
webhook_config = _partial.params.webhook_config
|
|
162
|
+
method_def = fun_def.method_definitions[method_name]
|
|
157
163
|
|
|
158
164
|
bound_func = user_func.__get__(self.user_cls_instance)
|
|
159
165
|
|
|
@@ -163,7 +169,10 @@ class ImportedClass(Service):
|
|
|
163
169
|
callable=bound_func,
|
|
164
170
|
is_async=is_async,
|
|
165
171
|
is_generator=bool(is_generator),
|
|
166
|
-
|
|
172
|
+
# FIXME (elias): the following `or [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]` is only
|
|
173
|
+
# needed for tests
|
|
174
|
+
supported_output_formats=method_def.supported_output_formats
|
|
175
|
+
or [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR],
|
|
167
176
|
)
|
|
168
177
|
else:
|
|
169
178
|
web_callable, lifespan_manager = construct_webhook_callable(
|
|
@@ -174,7 +183,8 @@ class ImportedClass(Service):
|
|
|
174
183
|
lifespan_manager=lifespan_manager,
|
|
175
184
|
is_async=True,
|
|
176
185
|
is_generator=True,
|
|
177
|
-
|
|
186
|
+
# FIXME (elias): the following `or [api_pb2.DATA_FORMAT_ASGI]` is only needed for tests
|
|
187
|
+
supported_output_formats=method_def.supported_output_formats or [api_pb2.DATA_FORMAT_ASGI],
|
|
178
188
|
)
|
|
179
189
|
finalized_functions[method_name] = finalized_function
|
|
180
190
|
return finalized_functions
|
modal/_serialization.py
CHANGED
|
@@ -6,6 +6,14 @@ import typing
|
|
|
6
6
|
from inspect import Parameter
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from modal._traceback import extract_traceback
|
|
10
|
+
from modal.config import config
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import cbor2 # type: ignore
|
|
14
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
15
|
+
cbor2 = None
|
|
16
|
+
|
|
9
17
|
import google.protobuf.message
|
|
10
18
|
|
|
11
19
|
from modal._utils.async_utils import synchronizer
|
|
@@ -15,7 +23,7 @@ from ._object import _Object
|
|
|
15
23
|
from ._type_manager import parameter_serde_registry, schema_registry
|
|
16
24
|
from ._vendor import cloudpickle
|
|
17
25
|
from .config import logger
|
|
18
|
-
from .exception import DeserializationError, ExecutionError, InvalidError
|
|
26
|
+
from .exception import DeserializationError, ExecutionError, InvalidError, SerializationError
|
|
19
27
|
from .object import Object
|
|
20
28
|
|
|
21
29
|
if typing.TYPE_CHECKING:
|
|
@@ -346,6 +354,12 @@ def _deserialize_asgi(asgi: api_pb2.Asgi) -> Any:
|
|
|
346
354
|
return None
|
|
347
355
|
|
|
348
356
|
|
|
357
|
+
def get_preferred_payload_format() -> "api_pb2.DataFormat.ValueType":
|
|
358
|
+
payload_format = (config.get("payload_format") or "pickle").lower()
|
|
359
|
+
data_format = api_pb2.DATA_FORMAT_CBOR if payload_format == "cbor" else api_pb2.DATA_FORMAT_PICKLE
|
|
360
|
+
return data_format
|
|
361
|
+
|
|
362
|
+
|
|
349
363
|
def serialize_data_format(obj: Any, data_format: int) -> bytes:
|
|
350
364
|
"""Similar to serialize(), but supports other data formats."""
|
|
351
365
|
if data_format == api_pb2.DATA_FORMAT_PICKLE:
|
|
@@ -355,6 +369,21 @@ def serialize_data_format(obj: Any, data_format: int) -> bytes:
|
|
|
355
369
|
elif data_format == api_pb2.DATA_FORMAT_GENERATOR_DONE:
|
|
356
370
|
assert isinstance(obj, api_pb2.GeneratorDone)
|
|
357
371
|
return obj.SerializeToString(deterministic=True)
|
|
372
|
+
elif data_format == api_pb2.DATA_FORMAT_CBOR:
|
|
373
|
+
if cbor2 is None:
|
|
374
|
+
raise InvalidError("CBOR support requires the 'cbor2' package to be installed.")
|
|
375
|
+
try:
|
|
376
|
+
return cbor2.dumps(obj)
|
|
377
|
+
except cbor2.CBOREncodeTypeError:
|
|
378
|
+
try:
|
|
379
|
+
typename = f"{type(obj).__module__}.{type(obj).__name__}"
|
|
380
|
+
except Exception:
|
|
381
|
+
typename = str(type(obj))
|
|
382
|
+
raise SerializationError(
|
|
383
|
+
# TODO (elias): add documentation link for more information on this
|
|
384
|
+
f"Can not serialize type {typename} as cbor. If you need to use a custom data type, "
|
|
385
|
+
"try to serialize it yourself e.g. by using pickle.dumps(my_data)"
|
|
386
|
+
)
|
|
358
387
|
else:
|
|
359
388
|
raise InvalidError(f"Unknown data format {data_format!r}")
|
|
360
389
|
|
|
@@ -366,6 +395,10 @@ def deserialize_data_format(s: bytes, data_format: int, client) -> Any:
|
|
|
366
395
|
return _deserialize_asgi(api_pb2.Asgi.FromString(s))
|
|
367
396
|
elif data_format == api_pb2.DATA_FORMAT_GENERATOR_DONE:
|
|
368
397
|
return api_pb2.GeneratorDone.FromString(s)
|
|
398
|
+
elif data_format == api_pb2.DATA_FORMAT_CBOR:
|
|
399
|
+
if cbor2 is None:
|
|
400
|
+
raise InvalidError("CBOR support requires the 'cbor2' package to be installed.")
|
|
401
|
+
return cbor2.loads(s)
|
|
369
402
|
else:
|
|
370
403
|
raise InvalidError(f"Unknown data format {data_format!r}")
|
|
371
404
|
|
|
@@ -579,3 +612,26 @@ def get_callable_schema(
|
|
|
579
612
|
arguments=arguments,
|
|
580
613
|
return_type=return_type_proto,
|
|
581
614
|
)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def pickle_exception(exc: BaseException) -> bytes:
|
|
618
|
+
try:
|
|
619
|
+
return serialize(exc)
|
|
620
|
+
except Exception as serialization_exc:
|
|
621
|
+
# We can't always serialize exceptions.
|
|
622
|
+
err = f"Failed to serialize exception {exc} of type {type(exc)}: {serialization_exc}"
|
|
623
|
+
logger.info(err)
|
|
624
|
+
return serialize(SerializationError(err))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def pickle_traceback(exc: BaseException, task_id: str) -> tuple[bytes, bytes]:
|
|
628
|
+
serialized_tb, tb_line_cache = b"", b""
|
|
629
|
+
|
|
630
|
+
try:
|
|
631
|
+
tb_dict, line_cache = extract_traceback(exc, task_id)
|
|
632
|
+
serialized_tb = serialize(tb_dict)
|
|
633
|
+
tb_line_cache = serialize(line_cache)
|
|
634
|
+
except Exception:
|
|
635
|
+
logger.info("Failed to serialize exception traceback.")
|
|
636
|
+
|
|
637
|
+
return serialized_tb, tb_line_cache
|
modal/_utils/blob_utils.py
CHANGED
|
@@ -300,6 +300,10 @@ async def blob_upload(payload: bytes, stub: ModalClientModal) -> str:
|
|
|
300
300
|
return blob_id
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
async def format_blob_data(data: bytes, api_stub: ModalClientModal) -> dict[str, Any]:
|
|
304
|
+
return {"data_blob_id": await blob_upload(data, api_stub)} if len(data) > MAX_OBJECT_SIZE_BYTES else {"data": data}
|
|
305
|
+
|
|
306
|
+
|
|
303
307
|
async def blob_upload_file(
|
|
304
308
|
file_obj: BinaryIO,
|
|
305
309
|
stub: ModalClientModal,
|
modal/_utils/function_utils.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
|
+
import typing
|
|
5
6
|
from collections.abc import AsyncGenerator
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from pathlib import Path, PurePosixPath
|
|
@@ -17,7 +18,9 @@ from modal_proto.modal_api_grpc import ModalClientModal
|
|
|
17
18
|
from .._serialization import (
|
|
18
19
|
deserialize,
|
|
19
20
|
deserialize_data_format,
|
|
21
|
+
get_preferred_payload_format,
|
|
20
22
|
serialize,
|
|
23
|
+
serialize_data_format as _serialize_data_format,
|
|
21
24
|
signature_to_parameter_specs,
|
|
22
25
|
)
|
|
23
26
|
from .._traceback import append_modal_tb
|
|
@@ -38,6 +41,9 @@ from .blob_utils import (
|
|
|
38
41
|
)
|
|
39
42
|
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
40
43
|
|
|
44
|
+
if typing.TYPE_CHECKING:
|
|
45
|
+
import modal._functions
|
|
46
|
+
|
|
41
47
|
|
|
42
48
|
class FunctionInfoType(Enum):
|
|
43
49
|
PACKAGE = "package"
|
|
@@ -486,7 +492,7 @@ async def _process_result(result: api_pb2.GenericResult, data_format: int, stub,
|
|
|
486
492
|
elif result.status == api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
|
|
487
493
|
raise InternalFailure(result.exception)
|
|
488
494
|
elif result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
489
|
-
if data:
|
|
495
|
+
if data and data_format == api_pb2.DATA_FORMAT_PICKLE:
|
|
490
496
|
try:
|
|
491
497
|
exc = deserialize(data, client)
|
|
492
498
|
except DeserializationError as deser_exc:
|
|
@@ -549,27 +555,35 @@ async def _create_input(
|
|
|
549
555
|
kwargs,
|
|
550
556
|
stub: ModalClientModal,
|
|
551
557
|
*,
|
|
552
|
-
|
|
558
|
+
function: "modal._functions._Function",
|
|
553
559
|
idx: Optional[int] = None,
|
|
554
|
-
method_name: Optional[str] = None,
|
|
555
560
|
function_call_invocation_type: Optional["api_pb2.FunctionCallInvocationType.ValueType"] = None,
|
|
556
561
|
) -> api_pb2.FunctionPutInputsItem:
|
|
557
562
|
"""Serialize function arguments and create a FunctionInput protobuf,
|
|
558
563
|
uploading to blob storage if needed.
|
|
559
564
|
"""
|
|
565
|
+
method_name = function._use_method_name
|
|
566
|
+
max_object_size_bytes = function._max_object_size_bytes
|
|
567
|
+
|
|
560
568
|
if idx is None:
|
|
561
569
|
idx = 0
|
|
562
|
-
if method_name is None:
|
|
563
|
-
method_name = "" # proto compatible
|
|
564
570
|
|
|
565
|
-
|
|
571
|
+
data_format = get_preferred_payload_format()
|
|
572
|
+
if not function._metadata:
|
|
573
|
+
raise ExecutionError("Attempted to call function that has not been hydrated with metadata")
|
|
574
|
+
|
|
575
|
+
supported_input_formats = function._metadata.supported_input_formats or [api_pb2.DATA_FORMAT_PICKLE]
|
|
576
|
+
if data_format not in supported_input_formats:
|
|
577
|
+
data_format = supported_input_formats[0]
|
|
578
|
+
|
|
579
|
+
args_serialized = _serialize_data_format((args, kwargs), data_format)
|
|
566
580
|
|
|
567
581
|
if should_upload(len(args_serialized), max_object_size_bytes, function_call_invocation_type):
|
|
568
582
|
args_blob_id, r2_failed, r2_throughput_bytes_s = await blob_upload_with_r2_failure_info(args_serialized, stub)
|
|
569
583
|
return api_pb2.FunctionPutInputsItem(
|
|
570
584
|
input=api_pb2.FunctionInput(
|
|
571
585
|
args_blob_id=args_blob_id,
|
|
572
|
-
data_format=
|
|
586
|
+
data_format=data_format,
|
|
573
587
|
method_name=method_name,
|
|
574
588
|
),
|
|
575
589
|
idx=idx,
|
|
@@ -580,7 +594,7 @@ async def _create_input(
|
|
|
580
594
|
return api_pb2.FunctionPutInputsItem(
|
|
581
595
|
input=api_pb2.FunctionInput(
|
|
582
596
|
args=args_serialized,
|
|
583
|
-
data_format=
|
|
597
|
+
data_format=data_format,
|
|
584
598
|
method_name=method_name,
|
|
585
599
|
),
|
|
586
600
|
idx=idx,
|
modal/app.py
CHANGED
|
@@ -667,6 +667,7 @@ class _App:
|
|
|
667
667
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
668
668
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
669
669
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
670
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
670
671
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
671
672
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
672
673
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
@@ -835,6 +836,7 @@ class _App:
|
|
|
835
836
|
include_source=include_source if include_source is not None else self._include_source_default,
|
|
836
837
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
837
838
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
839
|
+
restrict_output=_experimental_restrict_output,
|
|
838
840
|
)
|
|
839
841
|
|
|
840
842
|
self._add_function(function, webhook_config is not None)
|
|
@@ -895,6 +897,7 @@ class _App:
|
|
|
895
897
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
896
898
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
897
899
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
900
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
898
901
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
899
902
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
900
903
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
@@ -1028,6 +1031,7 @@ class _App:
|
|
|
1028
1031
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
1029
1032
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1030
1033
|
_experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
|
|
1034
|
+
restrict_output=_experimental_restrict_output,
|
|
1031
1035
|
)
|
|
1032
1036
|
|
|
1033
1037
|
self._add_function(cls_func, is_web_endpoint=False)
|
modal/app.pyi
CHANGED
|
@@ -427,6 +427,7 @@ class _App:
|
|
|
427
427
|
_experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
|
428
428
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
429
429
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
|
430
|
+
_experimental_restrict_output: bool = False,
|
|
430
431
|
keep_warm: typing.Optional[int] = None,
|
|
431
432
|
concurrency_limit: typing.Optional[int] = None,
|
|
432
433
|
container_idle_timeout: typing.Optional[int] = None,
|
|
@@ -480,6 +481,7 @@ class _App:
|
|
|
480
481
|
_experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
|
481
482
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
482
483
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
|
484
|
+
_experimental_restrict_output: bool = False,
|
|
483
485
|
keep_warm: typing.Optional[int] = None,
|
|
484
486
|
concurrency_limit: typing.Optional[int] = None,
|
|
485
487
|
container_idle_timeout: typing.Optional[int] = None,
|
|
@@ -1034,6 +1036,7 @@ class App:
|
|
|
1034
1036
|
_experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
|
1035
1037
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1036
1038
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
|
1039
|
+
_experimental_restrict_output: bool = False,
|
|
1037
1040
|
keep_warm: typing.Optional[int] = None,
|
|
1038
1041
|
concurrency_limit: typing.Optional[int] = None,
|
|
1039
1042
|
container_idle_timeout: typing.Optional[int] = None,
|
|
@@ -1087,6 +1090,7 @@ class App:
|
|
|
1087
1090
|
_experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
|
1088
1091
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1089
1092
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
|
1093
|
+
_experimental_restrict_output: bool = False,
|
|
1090
1094
|
keep_warm: typing.Optional[int] = None,
|
|
1091
1095
|
concurrency_limit: typing.Optional[int] = None,
|
|
1092
1096
|
container_idle_timeout: typing.Optional[int] = None,
|
modal/client.pyi
CHANGED
|
@@ -33,7 +33,7 @@ class _Client:
|
|
|
33
33
|
server_url: str,
|
|
34
34
|
client_type: int,
|
|
35
35
|
credentials: typing.Optional[tuple[str, str]],
|
|
36
|
-
version: str = "1.1.5.
|
|
36
|
+
version: str = "1.1.5.dev45",
|
|
37
37
|
):
|
|
38
38
|
"""mdmd:hidden
|
|
39
39
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -164,7 +164,7 @@ class Client:
|
|
|
164
164
|
server_url: str,
|
|
165
165
|
client_type: int,
|
|
166
166
|
credentials: typing.Optional[tuple[str, str]],
|
|
167
|
-
version: str = "1.1.5.
|
|
167
|
+
version: str = "1.1.5.dev45",
|
|
168
168
|
):
|
|
169
169
|
"""mdmd:hidden
|
|
170
170
|
The Modal client object is not intended to be instantiated directly by users.
|
modal/config.py
CHANGED
|
@@ -239,6 +239,11 @@ _SETTINGS = {
|
|
|
239
239
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
|
240
240
|
"cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
|
|
241
241
|
"build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),
|
|
242
|
+
# Payload format for function inputs/outputs: 'pickle' (default) or 'cbor'
|
|
243
|
+
"payload_format": _Setting(
|
|
244
|
+
"pickle",
|
|
245
|
+
transform=lambda s: _check_value(["pickle", "cbor"])(s.lower()),
|
|
246
|
+
),
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
|
modal/dict.py
CHANGED
|
@@ -30,6 +30,14 @@ from .config import logger
|
|
|
30
30
|
from .exception import AlreadyExistsError, InvalidError, NotFoundError, RequestSizeError
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
class _NoDefaultSentinel:
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return "..."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_NO_DEFAULT = _NoDefaultSentinel()
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
def _serialize_dict(data):
|
|
34
42
|
return [api_pb2.DictEntry(key=serialize(k), value=serialize(v)) for k, v in data.items()]
|
|
35
43
|
|
|
@@ -543,11 +551,16 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
543
551
|
return await self.put(key, value)
|
|
544
552
|
|
|
545
553
|
@live_method
|
|
546
|
-
async def pop(self, key: Any) -> Any:
|
|
547
|
-
"""Remove a key from the Dict, returning the value if it exists.
|
|
554
|
+
async def pop(self, key: Any, default: Any = _NO_DEFAULT) -> Any:
|
|
555
|
+
"""Remove a key from the Dict, returning the value if it exists.
|
|
556
|
+
|
|
557
|
+
If key is not found, return default if provided, otherwise raise KeyError.
|
|
558
|
+
"""
|
|
548
559
|
req = api_pb2.DictPopRequest(dict_id=self.object_id, key=serialize(key))
|
|
549
560
|
resp = await retry_transient_errors(self._client.stub.DictPop, req)
|
|
550
561
|
if not resp.found:
|
|
562
|
+
if default is not _NO_DEFAULT:
|
|
563
|
+
return default
|
|
551
564
|
raise KeyError(f"{key} not in dict {self.object_id}")
|
|
552
565
|
return deserialize(resp.value, self._client)
|
|
553
566
|
|
modal/dict.pyi
CHANGED
|
@@ -10,6 +10,13 @@ import synchronicity.combined_types
|
|
|
10
10
|
import typing
|
|
11
11
|
import typing_extensions
|
|
12
12
|
|
|
13
|
+
class _NoDefaultSentinel:
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
"""Return repr(self)."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
_NO_DEFAULT: _NoDefaultSentinel
|
|
19
|
+
|
|
13
20
|
def _serialize_dict(data): ...
|
|
14
21
|
|
|
15
22
|
class DictInfo:
|
|
@@ -548,8 +555,11 @@ class _Dict(modal._object._Object):
|
|
|
548
555
|
"""
|
|
549
556
|
...
|
|
550
557
|
|
|
551
|
-
async def pop(self, key: typing.Any) -> typing.Any:
|
|
552
|
-
"""Remove a key from the Dict, returning the value if it exists.
|
|
558
|
+
async def pop(self, key: typing.Any, default: typing.Any = ...) -> typing.Any:
|
|
559
|
+
"""Remove a key from the Dict, returning the value if it exists.
|
|
560
|
+
|
|
561
|
+
If key is not found, return default if provided, otherwise raise KeyError.
|
|
562
|
+
"""
|
|
553
563
|
...
|
|
554
564
|
|
|
555
565
|
async def __delitem__(self, key: typing.Any) -> typing.Any:
|
|
@@ -918,12 +928,18 @@ class Dict(modal.object.Object):
|
|
|
918
928
|
__setitem__: ____setitem___spec[typing_extensions.Self]
|
|
919
929
|
|
|
920
930
|
class __pop_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
921
|
-
def __call__(self, /, key: typing.Any) -> typing.Any:
|
|
922
|
-
"""Remove a key from the Dict, returning the value if it exists.
|
|
931
|
+
def __call__(self, /, key: typing.Any, default: typing.Any = ...) -> typing.Any:
|
|
932
|
+
"""Remove a key from the Dict, returning the value if it exists.
|
|
933
|
+
|
|
934
|
+
If key is not found, return default if provided, otherwise raise KeyError.
|
|
935
|
+
"""
|
|
923
936
|
...
|
|
924
937
|
|
|
925
|
-
async def aio(self, /, key: typing.Any) -> typing.Any:
|
|
926
|
-
"""Remove a key from the Dict, returning the value if it exists.
|
|
938
|
+
async def aio(self, /, key: typing.Any, default: typing.Any = ...) -> typing.Any:
|
|
939
|
+
"""Remove a key from the Dict, returning the value if it exists.
|
|
940
|
+
|
|
941
|
+
If key is not found, return default if provided, otherwise raise KeyError.
|
|
942
|
+
"""
|
|
927
943
|
...
|
|
928
944
|
|
|
929
945
|
pop: __pop_spec[typing_extensions.Self]
|
modal/functions.pyi
CHANGED
|
@@ -111,6 +111,7 @@ class Function(
|
|
|
111
111
|
experimental_options: typing.Optional[dict[str, str]] = None,
|
|
112
112
|
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
113
113
|
_experimental_custom_scaling_factor: typing.Optional[float] = None,
|
|
114
|
+
restrict_output: bool = False,
|
|
114
115
|
) -> Function:
|
|
115
116
|
"""mdmd:hidden
|
|
116
117
|
|
|
@@ -449,7 +450,7 @@ class Function(
|
|
|
449
450
|
|
|
450
451
|
_call_generator: ___call_generator_spec[typing_extensions.Self]
|
|
451
452
|
|
|
452
|
-
class __remote_spec(typing_extensions.Protocol[
|
|
453
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
|
453
454
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
|
|
454
455
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
455
456
|
...
|
|
@@ -458,7 +459,7 @@ class Function(
|
|
|
458
459
|
"""Calls the function remotely, executing it with the given arguments and returning the execution's result."""
|
|
459
460
|
...
|
|
460
461
|
|
|
461
|
-
remote: __remote_spec[modal._functions.
|
|
462
|
+
remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
|
462
463
|
|
|
463
464
|
class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
464
465
|
def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
|
|
@@ -485,7 +486,7 @@ class Function(
|
|
|
485
486
|
"""
|
|
486
487
|
...
|
|
487
488
|
|
|
488
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
|
489
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
|
489
490
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
490
491
|
"""[Experimental] Calls the function with the given arguments, without waiting for the results.
|
|
491
492
|
|
|
@@ -509,7 +510,7 @@ class Function(
|
|
|
509
510
|
...
|
|
510
511
|
|
|
511
512
|
_experimental_spawn: ___experimental_spawn_spec[
|
|
512
|
-
modal._functions.
|
|
513
|
+
modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
|
|
513
514
|
]
|
|
514
515
|
|
|
515
516
|
class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
|
|
@@ -518,7 +519,7 @@ class Function(
|
|
|
518
519
|
|
|
519
520
|
_spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
|
|
520
521
|
|
|
521
|
-
class __spawn_spec(typing_extensions.Protocol[
|
|
522
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
|
|
522
523
|
def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
|
|
523
524
|
"""Calls the function with the given arguments, without waiting for the results.
|
|
524
525
|
|
|
@@ -539,7 +540,7 @@ class Function(
|
|
|
539
540
|
"""
|
|
540
541
|
...
|
|
541
542
|
|
|
542
|
-
spawn: __spawn_spec[modal._functions.
|
|
543
|
+
spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
|
|
543
544
|
|
|
544
545
|
def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
|
|
545
546
|
"""Return the inner Python object wrapped by this Modal Function."""
|
modal/image.py
CHANGED
|
@@ -25,11 +25,12 @@ from google.protobuf.message import Message
|
|
|
25
25
|
from grpclib.exceptions import GRPCError, StreamTerminatedError
|
|
26
26
|
from typing_extensions import Self
|
|
27
27
|
|
|
28
|
+
from modal._serialization import serialize_data_format
|
|
28
29
|
from modal_proto import api_pb2
|
|
29
30
|
|
|
30
31
|
from ._object import _Object, live_method_gen
|
|
31
32
|
from ._resolver import Resolver
|
|
32
|
-
from ._serialization import serialize
|
|
33
|
+
from ._serialization import get_preferred_payload_format, serialize
|
|
33
34
|
from ._utils.async_utils import synchronize_api
|
|
34
35
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
35
36
|
from ._utils.deprecation import deprecation_warning
|
|
@@ -2318,13 +2319,19 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2318
2319
|
include_source=include_source,
|
|
2319
2320
|
)
|
|
2320
2321
|
if len(args) + len(kwargs) > 0:
|
|
2321
|
-
|
|
2322
|
+
data_format = get_preferred_payload_format()
|
|
2323
|
+
args_serialized = serialize_data_format((args, kwargs), data_format)
|
|
2324
|
+
|
|
2322
2325
|
if len(args_serialized) > MAX_OBJECT_SIZE_BYTES:
|
|
2323
2326
|
raise InvalidError(
|
|
2324
2327
|
f"Arguments to `run_function` are too large ({len(args_serialized)} bytes). "
|
|
2325
2328
|
f"Maximum size is {MAX_OBJECT_SIZE_BYTES} bytes."
|
|
2326
2329
|
)
|
|
2327
|
-
|
|
2330
|
+
|
|
2331
|
+
build_function_input = api_pb2.FunctionInput(
|
|
2332
|
+
args=args_serialized,
|
|
2333
|
+
data_format=data_format,
|
|
2334
|
+
)
|
|
2328
2335
|
else:
|
|
2329
2336
|
build_function_input = None
|
|
2330
2337
|
return _Image._from_args(
|
modal/parallel_map.py
CHANGED
|
@@ -128,9 +128,8 @@ class InputPreprocessor:
|
|
|
128
128
|
args,
|
|
129
129
|
kwargs,
|
|
130
130
|
self.client.stub,
|
|
131
|
-
max_object_size_bytes=self.function._max_object_size_bytes,
|
|
132
131
|
idx=idx,
|
|
133
|
-
|
|
132
|
+
function=self.function,
|
|
134
133
|
)
|
|
135
134
|
|
|
136
135
|
return create_input
|
|
@@ -728,9 +727,8 @@ async def _map_invocation_inputplane(
|
|
|
728
727
|
args,
|
|
729
728
|
kwargs,
|
|
730
729
|
client.stub,
|
|
731
|
-
max_object_size_bytes=function._max_object_size_bytes,
|
|
732
730
|
idx=idx,
|
|
733
|
-
|
|
731
|
+
function=function,
|
|
734
732
|
)
|
|
735
733
|
return api_pb2.MapStartOrContinueItem(input=put_item)
|
|
736
734
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.1.5.
|
|
3
|
+
Version: 1.1.5.dev45
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,6 +17,7 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: aiohttp
|
|
20
|
+
Requires-Dist: cbor2
|
|
20
21
|
Requires-Dist: certifi
|
|
21
22
|
Requires-Dist: click~=8.1
|
|
22
23
|
Requires-Dist: grpclib<0.4.9,>=0.4.7
|