modal 1.0.4.dev12__py3-none-any.whl → 1.0.5__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/_clustered_functions.pyi +13 -3
- modal/_functions.py +84 -46
- modal/_partial_function.py +1 -1
- modal/_runtime/container_io_manager.pyi +222 -40
- modal/_runtime/execution_context.pyi +60 -6
- modal/_serialization.py +25 -2
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +1 -1
- modal/_utils/blob_utils.py +56 -19
- modal/_utils/function_utils.py +33 -7
- modal/_utils/grpc_utils.py +11 -4
- modal/app.py +5 -5
- modal/app.pyi +658 -48
- modal/cli/run.py +2 -1
- modal/client.pyi +224 -36
- modal/cloud_bucket_mount.pyi +192 -4
- modal/cls.py +7 -7
- modal/cls.pyi +442 -35
- modal/container_process.pyi +103 -14
- modal/dict.py +4 -4
- modal/dict.pyi +453 -51
- modal/environments.pyi +41 -9
- modal/exception.py +6 -2
- modal/experimental/__init__.py +90 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.pyi +236 -45
- modal/functions.pyi +573 -65
- modal/gpu.py +1 -1
- modal/image.py +1 -1
- modal/image.pyi +1256 -74
- modal/io_streams.py +8 -4
- modal/io_streams.pyi +348 -38
- modal/mount.pyi +261 -31
- modal/network_file_system.py +3 -3
- modal/network_file_system.pyi +307 -26
- modal/object.pyi +48 -9
- modal/parallel_map.py +93 -19
- modal/parallel_map.pyi +160 -15
- modal/partial_function.pyi +255 -14
- modal/proxy.py +1 -1
- modal/proxy.pyi +28 -3
- modal/queue.py +4 -4
- modal/queue.pyi +447 -30
- modal/runner.pyi +160 -22
- modal/sandbox.py +8 -7
- modal/sandbox.pyi +310 -50
- modal/schedule.py +1 -1
- modal/secret.py +2 -2
- modal/secret.pyi +164 -15
- modal/snapshot.pyi +25 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +41 -4
- modal/volume.pyi +693 -59
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/METADATA +3 -3
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/RECORD +67 -67
- modal_proto/api.proto +56 -0
- modal_proto/api_grpc.py +48 -0
- modal_proto/api_pb2.py +874 -780
- modal_proto/api_pb2.pyi +194 -8
- modal_proto/api_pb2_grpc.py +100 -0
- modal_proto/api_pb2_grpc.pyi +32 -0
- modal_proto/modal_api_grpc.py +3 -0
- modal_version/__init__.py +1 -1
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/WHEEL +0 -0
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/entry_points.txt +0 -0
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/top_level.txt +0 -0
modal/parallel_map.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
2
|
import asyncio
|
3
3
|
import enum
|
4
|
+
import inspect
|
4
5
|
import time
|
5
6
|
import typing
|
6
7
|
from asyncio import FIRST_COMPLETED
|
@@ -9,6 +10,7 @@ from typing import Any, Callable, Optional
|
|
9
10
|
|
10
11
|
from grpclib import Status
|
11
12
|
|
13
|
+
import modal.exception
|
12
14
|
from modal._runtime.execution_context import current_input_id
|
13
15
|
from modal._utils.async_utils import (
|
14
16
|
AsyncOrSyncIterable,
|
@@ -26,6 +28,7 @@ from modal._utils.async_utils import (
|
|
26
28
|
warn_if_generator_is_not_consumed,
|
27
29
|
)
|
28
30
|
from modal._utils.blob_utils import BLOB_MAX_PARALLELISM
|
31
|
+
from modal._utils.deprecation import deprecation_warning
|
29
32
|
from modal._utils.function_utils import (
|
30
33
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
31
34
|
OUTPUTS_TIMEOUT,
|
@@ -89,6 +92,7 @@ async def _map_invocation(
|
|
89
92
|
client: "modal.client._Client",
|
90
93
|
order_outputs: bool,
|
91
94
|
return_exceptions: bool,
|
95
|
+
wrap_returned_exceptions: bool,
|
92
96
|
count_update_callback: Optional[Callable[[int, int], None]],
|
93
97
|
function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType",
|
94
98
|
):
|
@@ -278,17 +282,19 @@ async def _map_invocation(
|
|
278
282
|
)
|
279
283
|
)
|
280
284
|
map_done_task = asyncio.create_task(map_done_event.wait())
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
285
|
+
try:
|
286
|
+
done, pending = await asyncio.wait([get_response_task, map_done_task], return_when=FIRST_COMPLETED)
|
287
|
+
if get_response_task in done:
|
288
|
+
map_done_task.cancel()
|
289
|
+
response = get_response_task.result()
|
290
|
+
else:
|
291
|
+
assert map_done_event.is_set()
|
292
|
+
# map is done - no more outputs, so return early
|
293
|
+
return
|
294
|
+
finally:
|
295
|
+
# clean up tasks, in case of cancellations etc.
|
288
296
|
get_response_task.cancel()
|
289
|
-
|
290
|
-
await asyncio.gather(get_response_task, return_exceptions=True)
|
291
|
-
return
|
297
|
+
map_done_task.cancel()
|
292
298
|
|
293
299
|
last_entry_id = response.last_entry_id
|
294
300
|
now_seconds = int(time.time())
|
@@ -339,7 +345,13 @@ async def _map_invocation(
|
|
339
345
|
output = await _process_result(item.result, item.data_format, client.stub, client)
|
340
346
|
except Exception as e:
|
341
347
|
if return_exceptions:
|
342
|
-
|
348
|
+
if wrap_returned_exceptions:
|
349
|
+
# Prior to client 1.0.4 there was a bug where return_exceptions would wrap
|
350
|
+
# any returned exceptions in a synchronicity.UserCodeException. This adds
|
351
|
+
# deprecated non-breaking compatibility bandaid for migrating away from that:
|
352
|
+
output = modal.exception.UserCodeException(e)
|
353
|
+
else:
|
354
|
+
output = e
|
343
355
|
else:
|
344
356
|
raise e
|
345
357
|
return (item.idx, output)
|
@@ -399,7 +411,7 @@ async def _map_invocation(
|
|
399
411
|
async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())
|
400
412
|
) as streamer:
|
401
413
|
async for response in streamer:
|
402
|
-
if response is not None:
|
414
|
+
if response is not None: # type: ignore[unreachable]
|
403
415
|
yield response.value
|
404
416
|
log_debug_stats_task.cancel()
|
405
417
|
await log_debug_stats_task
|
@@ -411,6 +423,7 @@ async def _map_helper(
|
|
411
423
|
kwargs={}, # any extra keyword arguments for the function
|
412
424
|
order_outputs: bool = True, # return outputs in order
|
413
425
|
return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
|
426
|
+
wrap_returned_exceptions: bool = True,
|
414
427
|
) -> typing.AsyncGenerator[Any, None]:
|
415
428
|
"""Core implementation that supports `_map_async()`, `_starmap_async()` and `_for_each_async()`.
|
416
429
|
|
@@ -423,7 +436,6 @@ async def _map_helper(
|
|
423
436
|
We could make this explicit as an improvement or even let users decide what they
|
424
437
|
prefer: throughput (prioritize queueing inputs) or latency (prioritize yielding results)
|
425
438
|
"""
|
426
|
-
|
427
439
|
raw_input_queue: Any = SynchronizedQueue() # type: ignore
|
428
440
|
await raw_input_queue.init.aio()
|
429
441
|
|
@@ -441,12 +453,41 @@ async def _map_helper(
|
|
441
453
|
# synchronicity-wrapped, since they accept executable code in the form of iterators that we don't want to run inside
|
442
454
|
# the synchronicity thread. Instead, we delegate to `._map()` with a safer Queue as input.
|
443
455
|
async with aclosing(
|
444
|
-
async_merge(
|
456
|
+
async_merge(
|
457
|
+
self._map.aio(raw_input_queue, order_outputs, return_exceptions, wrap_returned_exceptions), feed_queue()
|
458
|
+
)
|
445
459
|
) as map_output_stream:
|
446
460
|
async for output in map_output_stream:
|
447
461
|
yield output
|
448
462
|
|
449
463
|
|
464
|
+
def _maybe_warn_about_exceptions(func_name: str, return_exceptions: bool, wrap_returned_exceptions: bool):
|
465
|
+
if return_exceptions and wrap_returned_exceptions:
|
466
|
+
deprecation_warning(
|
467
|
+
(2025, 6, 27),
|
468
|
+
(
|
469
|
+
f"Function.{func_name} currently leaks an internal exception wrapping type "
|
470
|
+
"(modal.exceptions.UserCodeException) when `return_exceptions=True` is set. "
|
471
|
+
"In the future, this will change, and the underlying exception will be returned directly.\n"
|
472
|
+
"To opt into the future behavior and silence this warning, add `wrap_returned_exceptions=False`:\n\n"
|
473
|
+
f" f.{func_name}(..., return_exceptions=True, wrap_returned_exceptions=False)"
|
474
|
+
),
|
475
|
+
)
|
476
|
+
|
477
|
+
|
478
|
+
def _invoked_from_sync_wrapper() -> bool:
|
479
|
+
"""Check whether the calling function was called from a sync wrapper."""
|
480
|
+
# This is temporary: we only need it to avoind double-firing the wrap_returned_exceptions warning.
|
481
|
+
# (We don't want to push the warning lower in the stack beacuse then we can't attribute to the user's code.)
|
482
|
+
try:
|
483
|
+
frame = inspect.currentframe()
|
484
|
+
caller_function_name = frame.f_back.f_back.f_code.co_name
|
485
|
+
# Embeds some assumptions about how the current calling stack works, but this is just temporary.
|
486
|
+
return caller_function_name == "asend"
|
487
|
+
except Exception:
|
488
|
+
return False
|
489
|
+
|
490
|
+
|
450
491
|
@warn_if_generator_is_not_consumed(function_name="Function.map.aio")
|
451
492
|
async def _map_async(
|
452
493
|
self: "modal.functions.Function",
|
@@ -456,10 +497,18 @@ async def _map_async(
|
|
456
497
|
kwargs={}, # any extra keyword arguments for the function
|
457
498
|
order_outputs: bool = True, # return outputs in order
|
458
499
|
return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
|
500
|
+
wrap_returned_exceptions: bool = True, # wrap returned exceptions in modal.exception.UserCodeException
|
459
501
|
) -> typing.AsyncGenerator[Any, None]:
|
502
|
+
if not _invoked_from_sync_wrapper():
|
503
|
+
_maybe_warn_about_exceptions("map.aio", return_exceptions, wrap_returned_exceptions)
|
460
504
|
async_input_gen = async_zip(*[sync_or_async_iter(it) for it in input_iterators])
|
461
505
|
async for output in _map_helper(
|
462
|
-
self,
|
506
|
+
self,
|
507
|
+
async_input_gen,
|
508
|
+
kwargs=kwargs,
|
509
|
+
order_outputs=order_outputs,
|
510
|
+
return_exceptions=return_exceptions,
|
511
|
+
wrap_returned_exceptions=wrap_returned_exceptions,
|
463
512
|
):
|
464
513
|
yield output
|
465
514
|
|
@@ -472,13 +521,17 @@ async def _starmap_async(
|
|
472
521
|
kwargs={},
|
473
522
|
order_outputs: bool = True,
|
474
523
|
return_exceptions: bool = False,
|
524
|
+
wrap_returned_exceptions: bool = True,
|
475
525
|
) -> typing.AsyncIterable[Any]:
|
526
|
+
if not _invoked_from_sync_wrapper():
|
527
|
+
_maybe_warn_about_exceptions("starmap.aio", return_exceptions, wrap_returned_exceptions)
|
476
528
|
async for output in _map_helper(
|
477
529
|
self,
|
478
530
|
sync_or_async_iter(input_iterator),
|
479
531
|
kwargs=kwargs,
|
480
532
|
order_outputs=order_outputs,
|
481
533
|
return_exceptions=return_exceptions,
|
534
|
+
wrap_returned_exceptions=wrap_returned_exceptions,
|
482
535
|
):
|
483
536
|
yield output
|
484
537
|
|
@@ -488,7 +541,12 @@ async def _for_each_async(self, *input_iterators, kwargs={}, ignore_exceptions:
|
|
488
541
|
# rather than iterating over the result
|
489
542
|
async_input_gen = async_zip(*[sync_or_async_iter(it) for it in input_iterators])
|
490
543
|
async for _ in _map_helper(
|
491
|
-
self,
|
544
|
+
self,
|
545
|
+
async_input_gen,
|
546
|
+
kwargs=kwargs,
|
547
|
+
order_outputs=False,
|
548
|
+
return_exceptions=ignore_exceptions,
|
549
|
+
wrap_returned_exceptions=False,
|
492
550
|
):
|
493
551
|
pass
|
494
552
|
|
@@ -500,6 +558,7 @@ def _map_sync(
|
|
500
558
|
kwargs={}, # any extra keyword arguments for the function
|
501
559
|
order_outputs: bool = True, # return outputs in order
|
502
560
|
return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
|
561
|
+
wrap_returned_exceptions: bool = True,
|
503
562
|
) -> AsyncOrSyncIterable:
|
504
563
|
"""Parallel map over a set of inputs.
|
505
564
|
|
@@ -537,10 +596,16 @@ def _map_sync(
|
|
537
596
|
print(list(my_func.map(range(3), return_exceptions=True)))
|
538
597
|
```
|
539
598
|
"""
|
599
|
+
_maybe_warn_about_exceptions("map", return_exceptions, wrap_returned_exceptions)
|
540
600
|
|
541
601
|
return AsyncOrSyncIterable(
|
542
602
|
_map_async(
|
543
|
-
self,
|
603
|
+
self,
|
604
|
+
*input_iterators,
|
605
|
+
kwargs=kwargs,
|
606
|
+
order_outputs=order_outputs,
|
607
|
+
return_exceptions=return_exceptions,
|
608
|
+
wrap_returned_exceptions=wrap_returned_exceptions,
|
544
609
|
),
|
545
610
|
nested_async_message=(
|
546
611
|
"You can't iter(Function.map()) from an async function. Use async for ... in Function.map.aio() instead."
|
@@ -620,6 +685,7 @@ def _starmap_sync(
|
|
620
685
|
kwargs={},
|
621
686
|
order_outputs: bool = True,
|
622
687
|
return_exceptions: bool = False,
|
688
|
+
wrap_returned_exceptions: bool = True,
|
623
689
|
) -> AsyncOrSyncIterable:
|
624
690
|
"""Like `map`, but spreads arguments over multiple function arguments.
|
625
691
|
|
@@ -637,9 +703,15 @@ def _starmap_sync(
|
|
637
703
|
assert list(my_func.starmap([(1, 2), (3, 4)])) == [3, 7]
|
638
704
|
```
|
639
705
|
"""
|
706
|
+
_maybe_warn_about_exceptions("starmap", return_exceptions, wrap_returned_exceptions)
|
640
707
|
return AsyncOrSyncIterable(
|
641
708
|
_starmap_async(
|
642
|
-
self,
|
709
|
+
self,
|
710
|
+
input_iterator,
|
711
|
+
kwargs=kwargs,
|
712
|
+
order_outputs=order_outputs,
|
713
|
+
return_exceptions=return_exceptions,
|
714
|
+
wrap_returned_exceptions=wrap_returned_exceptions,
|
643
715
|
),
|
644
716
|
nested_async_message=(
|
645
717
|
"You can't `iter(Function.starmap())` from an async function. "
|
@@ -716,6 +788,7 @@ class _MapItemContext:
|
|
716
788
|
Return True if input state was changed to COMPLETE, otherwise False.
|
717
789
|
"""
|
718
790
|
# If the item is already complete, this is a duplicate output and can be ignored.
|
791
|
+
|
719
792
|
if self.state == _MapItemState.COMPLETE:
|
720
793
|
logger.debug(
|
721
794
|
f"Received output for input marked as complete. Must be duplicate, so ignoring. "
|
@@ -761,11 +834,12 @@ class _MapItemContext:
|
|
761
834
|
delay_ms = 0
|
762
835
|
|
763
836
|
# None means the maximum number of retries has been reached, so output the error
|
764
|
-
if delay_ms is None:
|
837
|
+
if delay_ms is None or item.result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
|
765
838
|
self.state = _MapItemState.COMPLETE
|
766
839
|
return _OutputType.FAILED_COMPLETION
|
767
840
|
|
768
841
|
self.state = _MapItemState.WAITING_TO_RETRY
|
842
|
+
|
769
843
|
await retry_queue.put(now_seconds + (delay_ms / 1000), item.idx)
|
770
844
|
|
771
845
|
return _OutputType.RETRYING
|
modal/parallel_map.pyi
CHANGED
@@ -12,6 +12,7 @@ import typing
|
|
12
12
|
import typing_extensions
|
13
13
|
|
14
14
|
class _SynchronizedQueue:
|
15
|
+
"""mdmd:hidden"""
|
15
16
|
async def init(self): ...
|
16
17
|
async def put(self, item): ...
|
17
18
|
async def get(self): ...
|
@@ -19,7 +20,10 @@ class _SynchronizedQueue:
|
|
19
20
|
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
20
21
|
|
21
22
|
class SynchronizedQueue:
|
22
|
-
|
23
|
+
"""mdmd:hidden"""
|
24
|
+
def __init__(self, /, *args, **kwargs):
|
25
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
26
|
+
...
|
23
27
|
|
24
28
|
class __init_spec(typing_extensions.Protocol[SUPERSELF]):
|
25
29
|
def __call__(self, /): ...
|
@@ -40,11 +44,21 @@ class SynchronizedQueue:
|
|
40
44
|
get: __get_spec[typing_extensions.Self]
|
41
45
|
|
42
46
|
class _OutputValue:
|
47
|
+
"""_OutputValue(value: Any)"""
|
48
|
+
|
43
49
|
value: typing.Any
|
44
50
|
|
45
|
-
def __init__(self, value: typing.Any) -> None:
|
46
|
-
|
47
|
-
|
51
|
+
def __init__(self, value: typing.Any) -> None:
|
52
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
53
|
+
...
|
54
|
+
|
55
|
+
def __repr__(self):
|
56
|
+
"""Return repr(self)."""
|
57
|
+
...
|
58
|
+
|
59
|
+
def __eq__(self, other):
|
60
|
+
"""Return self==value."""
|
61
|
+
...
|
48
62
|
|
49
63
|
def _map_invocation(
|
50
64
|
function: modal._functions._Function,
|
@@ -52,6 +66,7 @@ def _map_invocation(
|
|
52
66
|
client: modal.client._Client,
|
53
67
|
order_outputs: bool,
|
54
68
|
return_exceptions: bool,
|
69
|
+
wrap_returned_exceptions: bool,
|
55
70
|
count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
|
56
71
|
function_call_invocation_type: int,
|
57
72
|
): ...
|
@@ -61,13 +76,33 @@ def _map_helper(
|
|
61
76
|
kwargs={},
|
62
77
|
order_outputs: bool = True,
|
63
78
|
return_exceptions: bool = False,
|
64
|
-
|
79
|
+
wrap_returned_exceptions: bool = True,
|
80
|
+
) -> typing.AsyncGenerator[typing.Any, None]:
|
81
|
+
"""Core implementation that supports `_map_async()`, `_starmap_async()` and `_for_each_async()`.
|
82
|
+
|
83
|
+
Runs in an event loop on the main thread. Concurrently feeds new input to the input queue and yields available
|
84
|
+
outputs to the caller.
|
85
|
+
|
86
|
+
Note that since the iterator(s) can block, it's a bit opaque how often the event
|
87
|
+
loop decides to get a new input vs how often it will emit a new output.
|
88
|
+
|
89
|
+
We could make this explicit as an improvement or even let users decide what they
|
90
|
+
prefer: throughput (prioritize queueing inputs) or latency (prioritize yielding results)
|
91
|
+
"""
|
92
|
+
...
|
93
|
+
|
94
|
+
def _maybe_warn_about_exceptions(func_name: str, return_exceptions: bool, wrap_returned_exceptions: bool): ...
|
95
|
+
def _invoked_from_sync_wrapper() -> bool:
|
96
|
+
"""Check whether the calling function was called from a sync wrapper."""
|
97
|
+
...
|
98
|
+
|
65
99
|
def _map_async(
|
66
100
|
self: modal.functions.Function,
|
67
101
|
*input_iterators: typing.Union[typing.Iterable[typing.Any], typing.AsyncIterable[typing.Any]],
|
68
102
|
kwargs={},
|
69
103
|
order_outputs: bool = True,
|
70
104
|
return_exceptions: bool = False,
|
105
|
+
wrap_returned_exceptions: bool = True,
|
71
106
|
) -> typing.AsyncGenerator[typing.Any, None]: ...
|
72
107
|
def _starmap_async(
|
73
108
|
self,
|
@@ -78,14 +113,91 @@ def _starmap_async(
|
|
78
113
|
kwargs={},
|
79
114
|
order_outputs: bool = True,
|
80
115
|
return_exceptions: bool = False,
|
116
|
+
wrap_returned_exceptions: bool = True,
|
81
117
|
) -> typing.AsyncIterable[typing.Any]: ...
|
82
118
|
async def _for_each_async(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False) -> None: ...
|
83
119
|
def _map_sync(
|
84
|
-
self,
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
120
|
+
self,
|
121
|
+
*input_iterators,
|
122
|
+
kwargs={},
|
123
|
+
order_outputs: bool = True,
|
124
|
+
return_exceptions: bool = False,
|
125
|
+
wrap_returned_exceptions: bool = True,
|
126
|
+
) -> modal._utils.async_utils.AsyncOrSyncIterable:
|
127
|
+
"""Parallel map over a set of inputs.
|
128
|
+
|
129
|
+
Takes one iterator argument per argument in the function being mapped over.
|
130
|
+
|
131
|
+
Example:
|
132
|
+
```python
|
133
|
+
@app.function()
|
134
|
+
def my_func(a):
|
135
|
+
return a ** 2
|
136
|
+
|
137
|
+
|
138
|
+
@app.local_entrypoint()
|
139
|
+
def main():
|
140
|
+
assert list(my_func.map([1, 2, 3, 4])) == [1, 4, 9, 16]
|
141
|
+
```
|
142
|
+
|
143
|
+
If applied to a `app.function`, `map()` returns one result per input and the output order
|
144
|
+
is guaranteed to be the same as the input order. Set `order_outputs=False` to return results
|
145
|
+
in the order that they are completed instead.
|
146
|
+
|
147
|
+
`return_exceptions` can be used to treat exceptions as successful results:
|
148
|
+
|
149
|
+
```python
|
150
|
+
@app.function()
|
151
|
+
def my_func(a):
|
152
|
+
if a == 2:
|
153
|
+
raise Exception("ohno")
|
154
|
+
return a ** 2
|
155
|
+
|
156
|
+
|
157
|
+
@app.local_entrypoint()
|
158
|
+
def main():
|
159
|
+
# [0, 1, UserCodeException(Exception('ohno'))]
|
160
|
+
print(list(my_func.map(range(3), return_exceptions=True)))
|
161
|
+
```
|
162
|
+
"""
|
163
|
+
...
|
164
|
+
|
165
|
+
async def _spawn_map_async(self, *input_iterators, kwargs={}) -> None:
|
166
|
+
"""This runs in an event loop on the main thread. It consumes inputs from the input iterators and creates async
|
167
|
+
function calls for each.
|
168
|
+
"""
|
169
|
+
...
|
170
|
+
|
171
|
+
def _spawn_map_sync(self, *input_iterators, kwargs={}) -> None:
|
172
|
+
"""Spawn parallel execution over a set of inputs, exiting as soon as the inputs are created (without waiting
|
173
|
+
for the map to complete).
|
174
|
+
|
175
|
+
Takes one iterator argument per argument in the function being mapped over.
|
176
|
+
|
177
|
+
Example:
|
178
|
+
```python
|
179
|
+
@app.function()
|
180
|
+
def my_func(a):
|
181
|
+
return a ** 2
|
182
|
+
|
183
|
+
|
184
|
+
@app.local_entrypoint()
|
185
|
+
def main():
|
186
|
+
my_func.spawn_map([1, 2, 3, 4])
|
187
|
+
```
|
188
|
+
|
189
|
+
Programmatic retrieval of results will be supported in a future update.
|
190
|
+
"""
|
191
|
+
...
|
192
|
+
|
193
|
+
def _for_each_sync(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False):
|
194
|
+
"""Execute function for all inputs, ignoring outputs. Waits for completion of the inputs.
|
195
|
+
|
196
|
+
Convenient alias for `.map()` in cases where the function just needs to be called.
|
197
|
+
as the caller doesn't have to consume the generator to process the inputs.
|
198
|
+
"""
|
199
|
+
...
|
200
|
+
|
89
201
|
def _starmap_sync(
|
90
202
|
self,
|
91
203
|
input_iterator: typing.Iterable[typing.Sequence[typing.Any]],
|
@@ -93,7 +205,25 @@ def _starmap_sync(
|
|
93
205
|
kwargs={},
|
94
206
|
order_outputs: bool = True,
|
95
207
|
return_exceptions: bool = False,
|
96
|
-
|
208
|
+
wrap_returned_exceptions: bool = True,
|
209
|
+
) -> modal._utils.async_utils.AsyncOrSyncIterable:
|
210
|
+
"""Like `map`, but spreads arguments over multiple function arguments.
|
211
|
+
|
212
|
+
Assumes every input is a sequence (e.g. a tuple).
|
213
|
+
|
214
|
+
Example:
|
215
|
+
```python
|
216
|
+
@app.function()
|
217
|
+
def my_func(a, b):
|
218
|
+
return a + b
|
219
|
+
|
220
|
+
|
221
|
+
@app.local_entrypoint()
|
222
|
+
def main():
|
223
|
+
assert list(my_func.starmap([(1, 2), (3, 4)])) == [3, 7]
|
224
|
+
```
|
225
|
+
"""
|
226
|
+
...
|
97
227
|
|
98
228
|
class _MapItemState(enum.Enum):
|
99
229
|
# The input is being sent the server with a PutInputs request, but the response has not been received yet.
|
@@ -130,7 +260,10 @@ class _MapItemContext:
|
|
130
260
|
input: modal_proto.api_pb2.FunctionInput,
|
131
261
|
retry_manager: modal.retries.RetryManager,
|
132
262
|
sync_client_retries_enabled: bool,
|
133
|
-
):
|
263
|
+
):
|
264
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
265
|
+
...
|
266
|
+
|
134
267
|
def handle_put_inputs_response(self, item: modal_proto.api_pb2.FunctionPutInputsResponseItem): ...
|
135
268
|
async def handle_get_outputs_response(
|
136
269
|
self,
|
@@ -138,7 +271,13 @@ class _MapItemContext:
|
|
138
271
|
now_seconds: int,
|
139
272
|
function_call_invocation_type: int,
|
140
273
|
retry_queue: modal._utils.async_utils.TimestampPriorityQueue,
|
141
|
-
) -> _OutputType:
|
274
|
+
) -> _OutputType:
|
275
|
+
"""Processes the output, and determines if it is complete or needs to be retried.
|
276
|
+
|
277
|
+
Return True if input state was changed to COMPLETE, otherwise False.
|
278
|
+
"""
|
279
|
+
...
|
280
|
+
|
142
281
|
async def prepare_item_for_retry(self) -> modal_proto.api_pb2.FunctionRetryInputsItem: ...
|
143
282
|
def handle_retry_response(self, input_jwt: str): ...
|
144
283
|
|
@@ -150,12 +289,18 @@ class _MapItemsManager:
|
|
150
289
|
retry_queue: modal._utils.async_utils.TimestampPriorityQueue,
|
151
290
|
sync_client_retries_enabled: bool,
|
152
291
|
max_inputs_outstanding: int,
|
153
|
-
):
|
292
|
+
):
|
293
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
294
|
+
...
|
295
|
+
|
154
296
|
async def add_items(self, items: list[modal_proto.api_pb2.FunctionPutInputsItem]): ...
|
155
297
|
async def prepare_items_for_retry(
|
156
298
|
self, retriable_idxs: list[int]
|
157
299
|
) -> list[modal_proto.api_pb2.FunctionRetryInputsItem]: ...
|
158
|
-
def get_input_jwts_waiting_for_output(self) -> list[str]:
|
300
|
+
def get_input_jwts_waiting_for_output(self) -> list[str]:
|
301
|
+
"""Returns a list of input_jwts for inputs that are waiting for output."""
|
302
|
+
...
|
303
|
+
|
159
304
|
def _remove_item(self, item_idx: int): ...
|
160
305
|
def get_item_context(self, item_idx: int) -> _MapItemContext: ...
|
161
306
|
def handle_put_inputs_response(self, items: list[modal_proto.api_pb2.FunctionPutInputsResponseItem]): ...
|