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.
Files changed (67) hide show
  1. modal/_clustered_functions.pyi +13 -3
  2. modal/_functions.py +84 -46
  3. modal/_partial_function.py +1 -1
  4. modal/_runtime/container_io_manager.pyi +222 -40
  5. modal/_runtime/execution_context.pyi +60 -6
  6. modal/_serialization.py +25 -2
  7. modal/_tunnel.pyi +380 -12
  8. modal/_utils/async_utils.py +1 -1
  9. modal/_utils/blob_utils.py +56 -19
  10. modal/_utils/function_utils.py +33 -7
  11. modal/_utils/grpc_utils.py +11 -4
  12. modal/app.py +5 -5
  13. modal/app.pyi +658 -48
  14. modal/cli/run.py +2 -1
  15. modal/client.pyi +224 -36
  16. modal/cloud_bucket_mount.pyi +192 -4
  17. modal/cls.py +7 -7
  18. modal/cls.pyi +442 -35
  19. modal/container_process.pyi +103 -14
  20. modal/dict.py +4 -4
  21. modal/dict.pyi +453 -51
  22. modal/environments.pyi +41 -9
  23. modal/exception.py +6 -2
  24. modal/experimental/__init__.py +90 -0
  25. modal/experimental/ipython.py +11 -7
  26. modal/file_io.pyi +236 -45
  27. modal/functions.pyi +573 -65
  28. modal/gpu.py +1 -1
  29. modal/image.py +1 -1
  30. modal/image.pyi +1256 -74
  31. modal/io_streams.py +8 -4
  32. modal/io_streams.pyi +348 -38
  33. modal/mount.pyi +261 -31
  34. modal/network_file_system.py +3 -3
  35. modal/network_file_system.pyi +307 -26
  36. modal/object.pyi +48 -9
  37. modal/parallel_map.py +93 -19
  38. modal/parallel_map.pyi +160 -15
  39. modal/partial_function.pyi +255 -14
  40. modal/proxy.py +1 -1
  41. modal/proxy.pyi +28 -3
  42. modal/queue.py +4 -4
  43. modal/queue.pyi +447 -30
  44. modal/runner.pyi +160 -22
  45. modal/sandbox.py +8 -7
  46. modal/sandbox.pyi +310 -50
  47. modal/schedule.py +1 -1
  48. modal/secret.py +2 -2
  49. modal/secret.pyi +164 -15
  50. modal/snapshot.pyi +25 -4
  51. modal/token_flow.pyi +28 -8
  52. modal/volume.py +41 -4
  53. modal/volume.pyi +693 -59
  54. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/METADATA +3 -3
  55. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/RECORD +67 -67
  56. modal_proto/api.proto +56 -0
  57. modal_proto/api_grpc.py +48 -0
  58. modal_proto/api_pb2.py +874 -780
  59. modal_proto/api_pb2.pyi +194 -8
  60. modal_proto/api_pb2_grpc.py +100 -0
  61. modal_proto/api_pb2_grpc.pyi +32 -0
  62. modal_proto/modal_api_grpc.py +3 -0
  63. modal_version/__init__.py +1 -1
  64. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/WHEEL +0 -0
  65. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/entry_points.txt +0 -0
  66. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/licenses/LICENSE +0 -0
  67. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/top_level.txt +0 -0
@@ -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
- def __repr__(self): ...
11
- def __eq__(self, other): ...
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(args, kwargs, stub, method_name=function._use_method_name)
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) -> Any:
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 await _process_result(item.result, item.data_format, self.stub, self.client)
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
- return await self._get_single_output()
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
- try:
288
- return await self._get_single_output(ctx.input_jwt)
289
- except (UserCodeException, FunctionTimeoutError) as exc:
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 exc
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
- except InternalFailure:
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
- input_item = await _create_input(args, kwargs, stub, method_name=function._use_method_name)
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
- response = await retry_transient_errors(stub.AttemptStart, request)
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
- try:
403
- if await_response.HasField("output"):
404
- return await _process_result(
405
- await_response.output.result, await_response.output.data_format, self.stub, self.client
406
- )
407
- except InternalFailure as e:
408
- internal_failure_count += 1
409
- # Limit the number of times we retry
410
- if internal_failure_count >= MAX_INTERNAL_FAILURE_COUNT:
411
- raise e
412
- # For system failures on the server, we retry immediately,
413
- # and the failure does not count towards the retry policy.
414
- retry_request = api_pb2.AttemptRetryRequest(
415
- function_id=self.function_id,
416
- parent_input_id=current_input_id() or "",
417
- input=self.input_item,
418
- attempt_token=self.attempt_token,
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
- In contrast to `modal.Function.lookup`, this is a lazy method
1256
- that defers hydrating the local object with metadata from
1257
- Modal servers until the first time it is actually used.
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, input_queue: _SynchronizedQueue, order_outputs: bool, return_exceptions: bool
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, that can later be polled or
1666
- waited for using [`.get(timeout=...)`](/docs/reference/modal.FunctionCall#get).
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.
@@ -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