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/_clustered_functions.pyi
CHANGED
@@ -3,12 +3,22 @@ import typing
|
|
3
3
|
import typing_extensions
|
4
4
|
|
5
5
|
class ClusterInfo:
|
6
|
+
"""ClusterInfo(rank: int, container_ips: list[str])"""
|
7
|
+
|
6
8
|
rank: int
|
7
9
|
container_ips: list[str]
|
8
10
|
|
9
|
-
def __init__(self, rank: int, container_ips: list[str]) -> None:
|
10
|
-
|
11
|
-
|
11
|
+
def __init__(self, rank: int, container_ips: list[str]) -> None:
|
12
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
13
|
+
...
|
14
|
+
|
15
|
+
def __repr__(self):
|
16
|
+
"""Return repr(self)."""
|
17
|
+
...
|
18
|
+
|
19
|
+
def __eq__(self, other):
|
20
|
+
"""Return self==value."""
|
21
|
+
...
|
12
22
|
|
13
23
|
def get_cluster_info() -> ClusterInfo: ...
|
14
24
|
async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
|
modal/_functions.py
CHANGED
@@ -15,7 +15,6 @@ import typing_extensions
|
|
15
15
|
from google.protobuf.message import Message
|
16
16
|
from grpclib import GRPCError, Status
|
17
17
|
from synchronicity.combined_types import MethodWithAio
|
18
|
-
from synchronicity.exceptions import UserCodeException
|
19
18
|
|
20
19
|
from modal_proto import api_pb2
|
21
20
|
from modal_proto.modal_api_grpc import ModalClientModal
|
@@ -63,8 +62,6 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
63
62
|
from .config import config
|
64
63
|
from .exception import (
|
65
64
|
ExecutionError,
|
66
|
-
FunctionTimeoutError,
|
67
|
-
InternalFailure,
|
68
65
|
InvalidError,
|
69
66
|
NotFoundError,
|
70
67
|
OutputExpiredError,
|
@@ -144,7 +141,13 @@ class _Invocation:
|
|
144
141
|
stub = client.stub
|
145
142
|
|
146
143
|
function_id = function.object_id
|
147
|
-
item = await _create_input(
|
144
|
+
item = await _create_input(
|
145
|
+
args,
|
146
|
+
kwargs,
|
147
|
+
stub,
|
148
|
+
method_name=function._use_method_name,
|
149
|
+
function_call_invocation_type=function_call_invocation_type,
|
150
|
+
)
|
148
151
|
|
149
152
|
request = api_pb2.FunctionMapRequest(
|
150
153
|
function_id=function_id,
|
@@ -257,7 +260,7 @@ class _Invocation:
|
|
257
260
|
request,
|
258
261
|
)
|
259
262
|
|
260
|
-
async def _get_single_output(self, expected_jwt: Optional[str] = None) ->
|
263
|
+
async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
|
261
264
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
262
265
|
item: api_pb2.FunctionGetOutputsItem = (
|
263
266
|
await self.pop_function_call_outputs(
|
@@ -266,7 +269,7 @@ class _Invocation:
|
|
266
269
|
input_jwts=[expected_jwt] if expected_jwt else None,
|
267
270
|
)
|
268
271
|
).outputs[0]
|
269
|
-
return
|
272
|
+
return item
|
270
273
|
|
271
274
|
async def run_function(self) -> Any:
|
272
275
|
# Use retry logic only if retry policy is specified and
|
@@ -278,23 +281,30 @@ class _Invocation:
|
|
278
281
|
or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
279
282
|
or not ctx.sync_client_retries_enabled
|
280
283
|
):
|
281
|
-
|
284
|
+
item = await self._get_single_output()
|
285
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
282
286
|
|
283
287
|
# User errors including timeouts are managed by the user specified retry policy.
|
284
288
|
user_retry_manager = RetryManager(ctx.retry_policy)
|
285
289
|
|
286
290
|
while True:
|
287
|
-
|
288
|
-
|
289
|
-
|
291
|
+
item = await self._get_single_output(ctx.input_jwt)
|
292
|
+
if item.result.status in (
|
293
|
+
api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
|
294
|
+
api_pb2.GenericResult.GENERIC_STATUS_TERMINATED,
|
295
|
+
):
|
296
|
+
# success or cancellations are "final" results
|
297
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
298
|
+
|
299
|
+
if item.result.status != api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
|
300
|
+
# non-internal failures get a delay before retrying
|
290
301
|
delay_ms = user_retry_manager.get_delay_ms()
|
291
302
|
if delay_ms is None:
|
292
|
-
raise
|
303
|
+
# no more retries, this should raise an error when the non-success status is converted
|
304
|
+
# to an exception:
|
305
|
+
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
293
306
|
await asyncio.sleep(delay_ms / 1000)
|
294
|
-
|
295
|
-
# For system failures on the server, we retry immediately,
|
296
|
-
# and the failure does not count towards the retry policy.
|
297
|
-
pass
|
307
|
+
|
298
308
|
await self._retry_input()
|
299
309
|
|
300
310
|
async def poll_function(self, timeout: Optional[float] = None):
|
@@ -352,12 +362,14 @@ class _InputPlaneInvocation:
|
|
352
362
|
client: _Client,
|
353
363
|
input_item: api_pb2.FunctionPutInputsItem,
|
354
364
|
function_id: str,
|
365
|
+
input_plane_region: str,
|
355
366
|
):
|
356
367
|
self.stub = stub
|
357
368
|
self.client = client # Used by the deserializer.
|
358
369
|
self.attempt_token = attempt_token
|
359
370
|
self.input_item = input_item
|
360
371
|
self.function_id = function_id
|
372
|
+
self.input_plane_region = input_plane_region
|
361
373
|
|
362
374
|
@staticmethod
|
363
375
|
async def create(
|
@@ -367,21 +379,27 @@ class _InputPlaneInvocation:
|
|
367
379
|
*,
|
368
380
|
client: _Client,
|
369
381
|
input_plane_url: str,
|
382
|
+
input_plane_region: str,
|
370
383
|
) -> "_InputPlaneInvocation":
|
371
384
|
stub = await client.get_stub(input_plane_url)
|
372
385
|
|
373
386
|
function_id = function.object_id
|
374
|
-
|
387
|
+
control_plane_stub = client.stub
|
388
|
+
# Note: Blob upload is done on the control plane stub, not the input plane stub!
|
389
|
+
input_item = await _create_input(args, kwargs, control_plane_stub, method_name=function._use_method_name)
|
375
390
|
|
376
391
|
request = api_pb2.AttemptStartRequest(
|
377
392
|
function_id=function_id,
|
378
393
|
parent_input_id=current_input_id() or "",
|
379
394
|
input=input_item,
|
380
395
|
)
|
381
|
-
|
396
|
+
metadata: list[tuple[str, str]] = []
|
397
|
+
if input_plane_region and input_plane_region != "":
|
398
|
+
metadata.append(("x-modal-input-plane-region", input_plane_region))
|
399
|
+
response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
|
382
400
|
attempt_token = response.attempt_token
|
383
401
|
|
384
|
-
return _InputPlaneInvocation(stub, attempt_token, client, input_item, function_id)
|
402
|
+
return _InputPlaneInvocation(stub, attempt_token, client, input_item, function_id, input_plane_region)
|
385
403
|
|
386
404
|
async def run_function(self) -> Any:
|
387
405
|
# This will retry when the server returns GENERIC_STATUS_INTERNAL_FAILURE, i.e. lost inputs or worker preemption
|
@@ -393,33 +411,41 @@ class _InputPlaneInvocation:
|
|
393
411
|
timeout_secs=OUTPUTS_TIMEOUT,
|
394
412
|
requested_at=time.time(),
|
395
413
|
)
|
414
|
+
metadata: list[tuple[str, str]] = []
|
415
|
+
if self.input_plane_region and self.input_plane_region != "":
|
416
|
+
metadata.append(("x-modal-input-plane-region", self.input_plane_region))
|
396
417
|
await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
|
397
418
|
self.stub.AttemptAwait,
|
398
419
|
await_request,
|
399
420
|
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
421
|
+
metadata=metadata,
|
400
422
|
)
|
401
423
|
|
402
|
-
|
403
|
-
if await_response.
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
424
|
+
if await_response.HasField("output"):
|
425
|
+
if await_response.output.result.status == api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
|
426
|
+
internal_failure_count += 1
|
427
|
+
# Limit the number of times we retry
|
428
|
+
if internal_failure_count < MAX_INTERNAL_FAILURE_COUNT:
|
429
|
+
# For system failures on the server, we retry immediately,
|
430
|
+
# and the failure does not count towards the retry policy.
|
431
|
+
retry_request = api_pb2.AttemptRetryRequest(
|
432
|
+
function_id=self.function_id,
|
433
|
+
parent_input_id=current_input_id() or "",
|
434
|
+
input=self.input_item,
|
435
|
+
attempt_token=self.attempt_token,
|
436
|
+
)
|
437
|
+
# TODO(ryan): Add exponential backoff?
|
438
|
+
retry_response = await retry_transient_errors(
|
439
|
+
self.stub.AttemptRetry,
|
440
|
+
retry_request,
|
441
|
+
metadata=metadata,
|
442
|
+
)
|
443
|
+
self.attempt_token = retry_response.attempt_token
|
444
|
+
continue
|
445
|
+
|
446
|
+
return await _process_result(
|
447
|
+
await_response.output.result, await_response.output.data_format, self.stub, self.client
|
419
448
|
)
|
420
|
-
# TODO(ryan): Add exponential backoff?
|
421
|
-
retry_response = await retry_transient_errors(self.stub.AttemptRetry, retry_request)
|
422
|
-
self.attempt_token = retry_response.attempt_token
|
423
449
|
|
424
450
|
|
425
451
|
# Wrapper type for api_pb2.FunctionStats
|
@@ -773,6 +799,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
773
799
|
req.method_definitions[method_name].CopyFrom(method_definition)
|
774
800
|
elif webhook_config:
|
775
801
|
req.webhook_config.CopyFrom(webhook_config)
|
802
|
+
|
776
803
|
response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
|
777
804
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
778
805
|
|
@@ -827,6 +854,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
827
854
|
mount_path=path,
|
828
855
|
volume_id=volume.object_id,
|
829
856
|
allow_background_commits=True,
|
857
|
+
read_only=volume._read_only,
|
830
858
|
)
|
831
859
|
for path, volume in validated_volumes_no_cloud_buckets
|
832
860
|
]
|
@@ -1080,6 +1108,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1080
1108
|
mount_path=path,
|
1081
1109
|
volume_id=volume.object_id,
|
1082
1110
|
allow_background_commits=True,
|
1111
|
+
read_only=volume._read_only,
|
1083
1112
|
)
|
1084
1113
|
for path, volume in options.validated_volumes
|
1085
1114
|
]
|
@@ -1252,9 +1281,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1252
1281
|
) -> "_Function":
|
1253
1282
|
"""Reference a Function from a deployed App by its name.
|
1254
1283
|
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1284
|
+
This is a lazy method that defers hydrating the local
|
1285
|
+
object with metadata from Modal servers until the first
|
1286
|
+
time it is actually used.
|
1258
1287
|
|
1259
1288
|
```python
|
1260
1289
|
f = modal.Function.from_name("other-app", "function")
|
@@ -1377,6 +1406,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1377
1406
|
self._method_handle_metadata = dict(metadata.method_handle_metadata)
|
1378
1407
|
self._definition_id = metadata.definition_id
|
1379
1408
|
self._input_plane_url = metadata.input_plane_url
|
1409
|
+
self._input_plane_region = metadata.input_plane_region
|
1380
1410
|
|
1381
1411
|
def _get_metadata(self):
|
1382
1412
|
# Overridden concrete implementation of base class method
|
@@ -1392,6 +1422,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1392
1422
|
method_handle_metadata=self._method_handle_metadata,
|
1393
1423
|
function_schema=self._metadata.function_schema if self._metadata else None,
|
1394
1424
|
input_plane_url=self._input_plane_url,
|
1425
|
+
input_plane_region=self._input_plane_region,
|
1395
1426
|
)
|
1396
1427
|
|
1397
1428
|
def _check_no_web_url(self, fn_name: str):
|
@@ -1438,7 +1469,11 @@ Use the `Function.get_web_url()` method instead.
|
|
1438
1469
|
|
1439
1470
|
@live_method_gen
|
1440
1471
|
async def _map(
|
1441
|
-
self,
|
1472
|
+
self,
|
1473
|
+
input_queue: _SynchronizedQueue,
|
1474
|
+
order_outputs: bool,
|
1475
|
+
return_exceptions: bool,
|
1476
|
+
wrap_returned_exceptions: bool,
|
1442
1477
|
) -> AsyncGenerator[Any, None]:
|
1443
1478
|
"""mdmd:hidden
|
1444
1479
|
|
@@ -1466,6 +1501,7 @@ Use the `Function.get_web_url()` method instead.
|
|
1466
1501
|
self.client,
|
1467
1502
|
order_outputs,
|
1468
1503
|
return_exceptions,
|
1504
|
+
wrap_returned_exceptions,
|
1469
1505
|
count_update_callback,
|
1470
1506
|
api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
|
1471
1507
|
)
|
@@ -1482,6 +1518,7 @@ Use the `Function.get_web_url()` method instead.
|
|
1482
1518
|
kwargs,
|
1483
1519
|
client=self.client,
|
1484
1520
|
input_plane_url=self._input_plane_url,
|
1521
|
+
input_plane_region=self._input_plane_region,
|
1485
1522
|
)
|
1486
1523
|
else:
|
1487
1524
|
invocation = await _Invocation.create(
|
@@ -1662,8 +1699,9 @@ Use the `Function.get_web_url()` method instead.
|
|
1662
1699
|
async def spawn(self, *args: P.args, **kwargs: P.kwargs) -> "_FunctionCall[ReturnType]":
|
1663
1700
|
"""Calls the function with the given arguments, without waiting for the results.
|
1664
1701
|
|
1665
|
-
Returns a [`modal.FunctionCall`](/docs/reference/modal.FunctionCall) object
|
1666
|
-
waited for using
|
1702
|
+
Returns a [`modal.FunctionCall`](https://modal.com/docs/reference/modal.FunctionCall) object
|
1703
|
+
that can later be polled or waited for using
|
1704
|
+
[`.get(timeout=...)`](https://modal.com/docs/reference/modal.FunctionCall#get).
|
1667
1705
|
Conceptually similar to `multiprocessing.pool.apply_async`, or a Future/Promise in other contexts.
|
1668
1706
|
"""
|
1669
1707
|
self._check_no_web_url("spawn")
|
@@ -1739,7 +1777,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1739
1777
|
"""Returns a structure representing the call graph from a given root
|
1740
1778
|
call ID, along with the status of execution for each node.
|
1741
1779
|
|
1742
|
-
See [`modal.call_graph`](/docs/reference/modal.call_graph) reference page
|
1780
|
+
See [`modal.call_graph`](https://modal.com/docs/reference/modal.call_graph) reference page
|
1743
1781
|
for documentation on the structure of the returned `InputInfo` items.
|
1744
1782
|
"""
|
1745
1783
|
assert self._client and self._client.stub
|
@@ -1753,7 +1791,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1753
1791
|
terminate_containers: bool = False,
|
1754
1792
|
):
|
1755
1793
|
"""Cancels the function call, which will stop its execution and mark its inputs as
|
1756
|
-
[`TERMINATED`](/docs/reference/modal.call_graph#modalcall_graphinputstatus).
|
1794
|
+
[`TERMINATED`](https://modal.com/docs/reference/modal.call_graph#modalcall_graphinputstatus).
|
1757
1795
|
|
1758
1796
|
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
1759
1797
|
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
modal/_partial_function.py
CHANGED
@@ -538,7 +538,7 @@ def _wsgi_app(
|
|
538
538
|
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
539
539
|
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
540
540
|
which is compatible with ASGI and supports additional functionality such as web sockets.
|
541
|
-
Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
|
541
|
+
Modal supports ASGI via [`asgi_app`](https://modal.com/docs/reference/modal.asgi_app).
|
542
542
|
|
543
543
|
**Usage:**
|
544
544
|
|