modal 1.1.5.dev83__py3-none-any.whl → 1.3.1.dev8__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.

Files changed (139) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +146 -121
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +26 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/package_utils.py +0 -1
  31. modal/_utils/rand_pb_testing.py +8 -1
  32. modal/_utils/task_command_router_client.py +524 -0
  33. modal/_vendor/cloudpickle.py +144 -48
  34. modal/app.py +215 -96
  35. modal/app.pyi +78 -37
  36. modal/billing.py +5 -0
  37. modal/builder/2025.06.txt +6 -3
  38. modal/builder/PREVIEW.txt +2 -1
  39. modal/builder/base-images.json +4 -2
  40. modal/cli/_download.py +19 -3
  41. modal/cli/cluster.py +4 -2
  42. modal/cli/config.py +3 -1
  43. modal/cli/container.py +5 -4
  44. modal/cli/dict.py +5 -2
  45. modal/cli/entry_point.py +26 -2
  46. modal/cli/environment.py +2 -16
  47. modal/cli/launch.py +1 -76
  48. modal/cli/network_file_system.py +5 -20
  49. modal/cli/queues.py +5 -4
  50. modal/cli/run.py +24 -204
  51. modal/cli/secret.py +1 -2
  52. modal/cli/shell.py +375 -0
  53. modal/cli/utils.py +1 -13
  54. modal/cli/volume.py +11 -17
  55. modal/client.py +16 -125
  56. modal/client.pyi +94 -144
  57. modal/cloud_bucket_mount.py +3 -1
  58. modal/cloud_bucket_mount.pyi +4 -0
  59. modal/cls.py +101 -64
  60. modal/cls.pyi +9 -8
  61. modal/config.py +21 -1
  62. modal/container_process.py +288 -12
  63. modal/container_process.pyi +99 -38
  64. modal/dict.py +72 -33
  65. modal/dict.pyi +88 -57
  66. modal/environments.py +16 -8
  67. modal/environments.pyi +6 -2
  68. modal/exception.py +154 -16
  69. modal/experimental/__init__.py +23 -5
  70. modal/experimental/flash.py +161 -74
  71. modal/experimental/flash.pyi +97 -49
  72. modal/file_io.py +50 -92
  73. modal/file_io.pyi +117 -89
  74. modal/functions.pyi +70 -87
  75. modal/image.py +73 -47
  76. modal/image.pyi +33 -30
  77. modal/io_streams.py +500 -149
  78. modal/io_streams.pyi +279 -189
  79. modal/mount.py +60 -45
  80. modal/mount.pyi +41 -17
  81. modal/network_file_system.py +19 -11
  82. modal/network_file_system.pyi +72 -39
  83. modal/object.pyi +114 -22
  84. modal/parallel_map.py +42 -44
  85. modal/parallel_map.pyi +9 -17
  86. modal/partial_function.pyi +4 -2
  87. modal/proxy.py +14 -6
  88. modal/proxy.pyi +10 -2
  89. modal/queue.py +45 -38
  90. modal/queue.pyi +88 -52
  91. modal/runner.py +96 -96
  92. modal/runner.pyi +44 -27
  93. modal/sandbox.py +225 -108
  94. modal/sandbox.pyi +226 -63
  95. modal/secret.py +58 -56
  96. modal/secret.pyi +28 -13
  97. modal/serving.py +7 -11
  98. modal/serving.pyi +7 -8
  99. modal/snapshot.py +29 -15
  100. modal/snapshot.pyi +18 -10
  101. modal/token_flow.py +1 -1
  102. modal/token_flow.pyi +4 -6
  103. modal/volume.py +102 -55
  104. modal/volume.pyi +125 -66
  105. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  106. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  107. modal_proto/api.proto +86 -30
  108. modal_proto/api_grpc.py +10 -25
  109. modal_proto/api_pb2.py +1080 -1047
  110. modal_proto/api_pb2.pyi +253 -79
  111. modal_proto/api_pb2_grpc.py +14 -48
  112. modal_proto/api_pb2_grpc.pyi +6 -18
  113. modal_proto/modal_api_grpc.py +175 -176
  114. modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
  115. modal_proto/task_command_router_grpc.py +138 -0
  116. modal_proto/task_command_router_pb2.py +180 -0
  117. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
  118. modal_proto/task_command_router_pb2_grpc.py +272 -0
  119. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  120. modal_version/__init__.py +1 -1
  121. modal_version/__main__.py +1 -1
  122. modal/cli/programs/launch_instance_ssh.py +0 -94
  123. modal/cli/programs/run_marimo.py +0 -95
  124. modal-1.1.5.dev83.dist-info/RECORD +0 -191
  125. modal_proto/modal_options_grpc.py +0 -3
  126. modal_proto/options.proto +0 -19
  127. modal_proto/options_grpc.py +0 -3
  128. modal_proto/options_pb2.py +0 -35
  129. modal_proto/options_pb2.pyi +0 -20
  130. modal_proto/options_pb2_grpc.py +0 -4
  131. modal_proto/options_pb2_grpc.pyi +0 -7
  132. modal_proto/sandbox_router_grpc.py +0 -105
  133. modal_proto/sandbox_router_pb2.py +0 -148
  134. modal_proto/sandbox_router_pb2_grpc.py +0 -203
  135. modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
  136. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  137. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  138. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  139. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/volume.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import asyncio
3
+ import builtins
3
4
  import concurrent.futures
4
5
  import enum
5
6
  import functools
@@ -7,6 +8,7 @@ import multiprocessing
7
8
  import os
8
9
  import platform
9
10
  import re
11
+ import sys
10
12
  import time
11
13
  import typing
12
14
  from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
@@ -24,20 +26,21 @@ from typing import (
24
26
  )
25
27
 
26
28
  from google.protobuf.message import Message
27
- from grpclib import GRPCError, Status
28
29
  from synchronicity import classproperty
29
30
  from synchronicity.async_wrap import asynccontextmanager
30
31
 
31
32
  import modal.exception
32
33
  import modal_proto.api_pb2
33
- from modal.exception import AlreadyExistsError, InvalidError, NotFoundError, VolumeUploadTimeoutError
34
+ from modal.exception import AlreadyExistsError, ConflictError, InvalidError, NotFoundError, VolumeUploadTimeoutError
34
35
  from modal_proto import api_pb2
35
36
 
37
+ from ._load_context import LoadContext
36
38
  from ._object import (
37
39
  EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
38
40
  _get_environment_name,
39
41
  _Object,
40
42
  live_method,
43
+ live_method_contextmanager,
41
44
  live_method_gen,
42
45
  )
43
46
  from ._resolver import Resolver
@@ -59,7 +62,7 @@ from ._utils.blob_utils import (
59
62
  get_file_upload_spec_from_path,
60
63
  )
61
64
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
62
- from ._utils.grpc_utils import retry_transient_errors
65
+ from ._utils.grpc_utils import Retry
63
66
  from ._utils.http_utils import ClientSessionRegistry
64
67
  from ._utils.name_utils import check_object_name
65
68
  from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
@@ -170,11 +173,9 @@ class _VolumeManager:
170
173
  version=version,
171
174
  )
172
175
  try:
173
- await retry_transient_errors(client.stub.VolumeGetOrCreate, req)
174
- except GRPCError as exc:
175
- if exc.status == Status.ALREADY_EXISTS and not allow_existing:
176
- raise AlreadyExistsError(exc.message)
177
- else:
176
+ await client.stub.VolumeGetOrCreate(req)
177
+ except AlreadyExistsError:
178
+ if not allow_existing:
178
179
  raise
179
180
 
180
181
  @staticmethod
@@ -184,7 +185,7 @@ class _VolumeManager:
184
185
  created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
185
186
  environment_name: str = "", # Uses active environment if not specified
186
187
  client: Optional[_Client] = None, # Optional client with Modal credentials
187
- ) -> list["_Volume"]:
188
+ ) -> builtins.list["_Volume"]:
188
189
  """Return a list of hydrated Volume objects.
189
190
 
190
191
  **Examples:**
@@ -222,7 +223,7 @@ class _VolumeManager:
222
223
  req = api_pb2.VolumeListRequest(
223
224
  environment_name=_get_environment_name(environment_name), pagination=pagination
224
225
  )
225
- resp = await retry_transient_errors(client.stub.VolumeList, req)
226
+ resp = await client.stub.VolumeList(req)
226
227
  items.extend(resp.items)
227
228
  finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
228
229
  return finished
@@ -280,7 +281,7 @@ class _VolumeManager:
280
281
  raise
281
282
  else:
282
283
  req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
283
- await retry_transient_errors(obj._client.stub.VolumeDelete, req)
284
+ await obj._client.stub.VolumeDelete(req)
284
285
 
285
286
 
286
287
  VolumeManager = synchronize_api(_VolumeManager)
@@ -362,11 +363,19 @@ class _Volume(_Object, type_prefix="vo"):
362
363
  Added in v1.0.5.
363
364
  """
364
365
 
365
- async def _load(new_volume: _Volume, resolver: Resolver, existing_object_id: Optional[str]):
366
+ async def _load(
367
+ new_volume: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
368
+ ):
366
369
  new_volume._initialize_from_other(self)
367
370
  new_volume._read_only = True
368
371
 
369
- obj = _Volume._from_loader(_load, "Volume()", hydrate_lazily=True, deps=lambda: [self])
372
+ obj = _Volume._from_loader(
373
+ _load,
374
+ "Volume()",
375
+ hydrate_lazily=True,
376
+ deps=lambda: [self],
377
+ load_context_overrides=self._load_context_overrides,
378
+ )
370
379
  return obj
371
380
 
372
381
  def _hydrate_metadata(self, metadata: Optional[Message]):
@@ -408,6 +417,7 @@ class _Volume(_Object, type_prefix="vo"):
408
417
  environment_name: Optional[str] = None,
409
418
  create_if_missing: bool = False,
410
419
  version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
420
+ client: Optional[_Client] = None,
411
421
  ) -> "_Volume":
412
422
  """Reference a Volume by name, creating if necessary.
413
423
 
@@ -429,18 +439,26 @@ class _Volume(_Object, type_prefix="vo"):
429
439
  check_object_name(name, "Volume")
430
440
  warn_if_passing_namespace(namespace, "modal.Volume.from_name")
431
441
 
432
- async def _load(self: _Volume, resolver: Resolver, existing_object_id: Optional[str]):
442
+ async def _load(
443
+ self: _Volume, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
444
+ ):
433
445
  req = api_pb2.VolumeGetOrCreateRequest(
434
446
  deployment_name=name,
435
- environment_name=_get_environment_name(environment_name, resolver),
447
+ environment_name=load_context.environment_name,
436
448
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
437
449
  version=version,
438
450
  )
439
- response = await resolver.client.stub.VolumeGetOrCreate(req)
440
- self._hydrate(response.volume_id, resolver.client, response.metadata)
451
+ response = await load_context.client.stub.VolumeGetOrCreate(req)
452
+ self._hydrate(response.volume_id, load_context.client, response.metadata)
441
453
 
442
454
  rep = _Volume._repr(name, environment_name)
443
- return _Volume._from_loader(_load, rep, hydrate_lazily=True, name=name)
455
+ return _Volume._from_loader(
456
+ _load,
457
+ rep,
458
+ hydrate_lazily=True,
459
+ name=name,
460
+ load_context_overrides=LoadContext(client=client, environment_name=environment_name),
461
+ )
444
462
 
445
463
  @classmethod
446
464
  @asynccontextmanager
@@ -519,7 +537,7 @@ class _Volume(_Object, type_prefix="vo"):
519
537
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
520
538
  version=version,
521
539
  )
522
- resp = await retry_transient_errors(client.stub.VolumeGetOrCreate, request)
540
+ resp = await client.stub.VolumeGetOrCreate(request)
523
541
  return resp.volume_id
524
542
 
525
543
  @live_method
@@ -539,7 +557,7 @@ class _Volume(_Object, type_prefix="vo"):
539
557
  async def _do_reload(self, lock=True):
540
558
  async with (await self._get_lock()) if lock else asyncnullcontext():
541
559
  req = api_pb2.VolumeReloadRequest(volume_id=self.object_id)
542
- _ = await retry_transient_errors(self._client.stub.VolumeReload, req)
560
+ _ = await self._client.stub.VolumeReload(req)
543
561
 
544
562
  @live_method
545
563
  async def commit(self):
@@ -552,12 +570,12 @@ class _Volume(_Object, type_prefix="vo"):
552
570
  req = api_pb2.VolumeCommitRequest(volume_id=self.object_id)
553
571
  try:
554
572
  # TODO(gongy): only apply indefinite retries on 504 status.
555
- resp = await retry_transient_errors(self._client.stub.VolumeCommit, req, max_retries=90)
573
+ resp = await self._client.stub.VolumeCommit(req, retry=Retry(max_retries=90))
556
574
  if not resp.skip_reload:
557
575
  # Reload changes on successful commit.
558
576
  await self._do_reload(lock=False)
559
- except GRPCError as exc:
560
- raise RuntimeError(exc.message) if exc.status in (Status.FAILED_PRECONDITION, Status.NOT_FOUND) else exc
577
+ except (ConflictError, NotFoundError) as exc:
578
+ raise RuntimeError(str(exc))
561
579
 
562
580
  @live_method
563
581
  async def reload(self):
@@ -570,11 +588,12 @@ class _Volume(_Object, type_prefix="vo"):
570
588
  """
571
589
  try:
572
590
  await self._do_reload()
573
- except GRPCError as exc:
591
+ except (NotFoundError, ConflictError) as exc:
574
592
  # TODO(staffan): This is brittle and janky, as it relies on specific paths and error messages which can
575
593
  # change server-side at any time. Consider returning the open files directly in the error emitted from the
576
594
  # server.
577
- if exc.message == "there are open files preventing the operation":
595
+ message = str(exc)
596
+ if "there are open files preventing the operation" in message:
578
597
  # Attempt to identify what open files are problematic and include information about the first (to avoid
579
598
  # really verbose errors) open file in the error message to help troubleshooting.
580
599
  # This is best-effort and not necessarily bulletproof, as the view of open files inside the container
@@ -582,9 +601,8 @@ class _Volume(_Object, type_prefix="vo"):
582
601
  vol_path = f"/__modal/volumes/{self.object_id}"
583
602
  annotation = _open_files_error_annotation(vol_path)
584
603
  if annotation:
585
- raise RuntimeError(f"{exc.message}: {annotation}")
586
-
587
- raise RuntimeError(exc.message) if exc.status in (Status.FAILED_PRECONDITION, Status.NOT_FOUND) else exc
604
+ raise RuntimeError(f"{message}: {annotation}")
605
+ raise RuntimeError(message)
588
606
 
589
607
  @live_method_gen
590
608
  async def iterdir(self, path: str, *, recursive: bool = True) -> AsyncIterator[FileEntry]:
@@ -627,7 +645,7 @@ class _Volume(_Object, type_prefix="vo"):
627
645
  return [entry async for entry in self.iterdir(path, recursive=recursive)]
628
646
 
629
647
  @live_method_gen
630
- async def read_file(self, path: str) -> AsyncIterator[bytes]:
648
+ async def read_file(self, path: str) -> AsyncGenerator[bytes, None]:
631
649
  """
632
650
  Read a file from the modal.Volume.
633
651
 
@@ -648,13 +666,14 @@ class _Volume(_Object, type_prefix="vo"):
648
666
  req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
649
667
 
650
668
  try:
651
- response = await retry_transient_errors(self._client.stub.VolumeGetFile2, req)
669
+ response = await self._client.stub.VolumeGetFile2(req)
652
670
  except modal.exception.NotFoundError as exc:
653
671
  raise FileNotFoundError(exc.args[0])
654
672
 
655
673
  @retry(n_attempts=5, base_delay=0.1, timeout=None)
656
674
  async def read_block(block_url: str) -> bytes:
657
675
  async with ClientSessionRegistry.get_session().get(block_url) as get_response:
676
+ get_response.raise_for_status()
658
677
  return await get_response.content.read()
659
678
 
660
679
  async def iter_urls() -> AsyncGenerator[str]:
@@ -670,25 +689,43 @@ class _Volume(_Object, type_prefix="vo"):
670
689
 
671
690
  @live_method
672
691
  async def read_file_into_fileobj(
673
- self, path: str, fileobj: typing.IO[bytes], progress_cb: Optional[Callable[..., Any]] = None
692
+ self,
693
+ path: str,
694
+ fileobj: typing.IO[bytes],
695
+ progress_cb: Optional[Callable[..., Any]] = None,
674
696
  ) -> int:
675
697
  """mdmd:hidden
676
698
  Read volume file into file-like IO object.
677
699
  """
700
+ return await self._read_file_into_fileobj(path, fileobj, progress_cb=progress_cb)
701
+
702
+ @live_method
703
+ async def _read_file_into_fileobj(
704
+ self,
705
+ path: str,
706
+ fileobj: typing.IO[bytes],
707
+ concurrency: Optional[int] = None,
708
+ download_semaphore: Optional[asyncio.Semaphore] = None,
709
+ progress_cb: Optional[Callable[..., Any]] = None,
710
+ ) -> int:
678
711
  if progress_cb is None:
679
712
 
680
713
  def progress_cb(*_, **__):
681
714
  pass
682
715
 
716
+ if concurrency is None:
717
+ concurrency = multiprocessing.cpu_count()
718
+
683
719
  req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
684
720
 
685
721
  try:
686
- response = await retry_transient_errors(self._client.stub.VolumeGetFile2, req)
722
+ response = await self._client.stub.VolumeGetFile2(req)
687
723
  except modal.exception.NotFoundError as exc:
688
724
  raise FileNotFoundError(exc.args[0])
689
725
 
690
- # TODO(dflemstr): Sane default limit? Make configurable?
691
- download_semaphore = asyncio.Semaphore(multiprocessing.cpu_count())
726
+ if download_semaphore is None:
727
+ download_semaphore = asyncio.Semaphore(concurrency)
728
+
692
729
  write_lock = asyncio.Lock()
693
730
  start_pos = fileobj.tell()
694
731
 
@@ -698,6 +735,7 @@ class _Volume(_Object, type_prefix="vo"):
698
735
  num_bytes_written = 0
699
736
 
700
737
  async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
738
+ get_response.raise_for_status()
701
739
  async for chunk in get_response.content.iter_any():
702
740
  num_chunk_bytes_written = 0
703
741
 
@@ -731,10 +769,10 @@ class _Volume(_Object, type_prefix="vo"):
731
769
  try:
732
770
  if self._is_v1:
733
771
  req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
734
- await retry_transient_errors(self._client.stub.VolumeRemoveFile, req)
772
+ await self._client.stub.VolumeRemoveFile(req)
735
773
  else:
736
774
  req = api_pb2.VolumeRemoveFile2Request(volume_id=self.object_id, path=path, recursive=recursive)
737
- await retry_transient_errors(self._client.stub.VolumeRemoveFile2, req)
775
+ await self._client.stub.VolumeRemoveFile2(req)
738
776
  except modal.exception.NotFoundError as exc:
739
777
  raise FileNotFoundError(exc.args[0])
740
778
 
@@ -773,15 +811,16 @@ class _Volume(_Object, type_prefix="vo"):
773
811
  request = api_pb2.VolumeCopyFilesRequest(
774
812
  volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
775
813
  )
776
- await retry_transient_errors(self._client.stub.VolumeCopyFiles, request, base_delay=1)
814
+ await self._client.stub.VolumeCopyFiles(request, retry=Retry(base_delay=1))
777
815
  else:
778
816
  request = api_pb2.VolumeCopyFiles2Request(
779
817
  volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path, recursive=recursive
780
818
  )
781
- await retry_transient_errors(self._client.stub.VolumeCopyFiles2, request, base_delay=1)
819
+ await self._client.stub.VolumeCopyFiles2(request, retry=Retry(base_delay=1))
782
820
 
783
- @live_method
784
- async def batch_upload(self, force: bool = False) -> "_AbstractVolumeUploadContextManager":
821
+ @live_method_contextmanager
822
+ @asynccontextmanager
823
+ async def batch_upload(self, force: bool = False) -> AsyncGenerator["_AbstractVolumeUploadContextManager", None]:
785
824
  """
786
825
  Initiate a batched upload to a volume.
787
826
 
@@ -802,15 +841,19 @@ class _Volume(_Object, type_prefix="vo"):
802
841
  if self._read_only:
803
842
  raise InvalidError("Read-only Volume can not be written to")
804
843
 
805
- return _AbstractVolumeUploadContextManager.resolve(
844
+ version_context_manager = _AbstractVolumeUploadContextManager.resolve(
806
845
  self._metadata.version, self.object_id, self._client, force=force
807
846
  )
847
+ await version_context_manager.__aenter__()
848
+ try:
849
+ yield version_context_manager
850
+ finally:
851
+ exc_type, exc_value, traceback = sys.exc_info()
852
+ await version_context_manager.__aexit__(exc_type, exc_value, traceback)
808
853
 
809
854
  @live_method
810
855
  async def _instance_delete(self):
811
- await retry_transient_errors(
812
- self._client.stub.VolumeDelete, api_pb2.VolumeDeleteRequest(volume_id=self.object_id)
813
- )
856
+ await self._client.stub.VolumeDelete(api_pb2.VolumeDeleteRequest(volume_id=self.object_id))
814
857
 
815
858
  @staticmethod
816
859
  async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
@@ -839,7 +882,7 @@ class _Volume(_Object, type_prefix="vo"):
839
882
  ):
840
883
  obj = await _Volume.from_name(old_name, environment_name=environment_name).hydrate(client)
841
884
  req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
842
- await retry_transient_errors(obj._client.stub.VolumeRename, req)
885
+ await obj._client.stub.VolumeRename(req)
843
886
 
844
887
 
845
888
  Volume = synchronize_api(_Volume)
@@ -940,9 +983,9 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
940
983
  disallow_overwrite_existing_files=not self._force,
941
984
  )
942
985
  try:
943
- await retry_transient_errors(self._client.stub.VolumePutFiles, request, base_delay=1)
944
- except GRPCError as exc:
945
- raise FileExistsError(exc.message) if exc.status == Status.ALREADY_EXISTS else exc
986
+ await self._client.stub.VolumePutFiles(request, retry=Retry(base_delay=1))
987
+ except AlreadyExistsError as exc:
988
+ raise FileExistsError(str(exc))
946
989
 
947
990
  def put_file(
948
991
  self,
@@ -1000,7 +1043,7 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
1000
1043
  remote_filename = file_spec.mount_filename
1001
1044
  progress_task_id = self._progress_cb(name=remote_filename, size=file_spec.size)
1002
1045
  request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
1003
- response = await retry_transient_errors(self._client.stub.MountPutFile, request, base_delay=1)
1046
+ response = await self._client.stub.MountPutFile(request, retry=Retry(base_delay=1))
1004
1047
 
1005
1048
  start_time = time.monotonic()
1006
1049
  if not response.exists:
@@ -1020,11 +1063,15 @@ class _VolumeUploadContextManager(_AbstractVolumeUploadContextManager):
1020
1063
  logger.debug(
1021
1064
  f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
1022
1065
  )
1023
- request2 = api_pb2.MountPutFileRequest(data=file_spec.content, sha256_hex=file_spec.sha256_hex)
1066
+ if file_spec.content is None:
1067
+ content = await asyncio.to_thread(file_spec.read_content)
1068
+ else:
1069
+ content = file_spec.content
1070
+ request2 = api_pb2.MountPutFileRequest(data=content, sha256_hex=file_spec.sha256_hex)
1024
1071
  self._progress_cb(task_id=progress_task_id, complete=True)
1025
1072
 
1026
1073
  while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
1027
- response = await retry_transient_errors(self._client.stub.MountPutFile, request2, base_delay=1)
1074
+ response = await self._client.stub.MountPutFile(request2, retry=Retry(base_delay=1))
1028
1075
  if response.exists:
1029
1076
  break
1030
1077
 
@@ -1184,9 +1231,9 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
1184
1231
  )
1185
1232
 
1186
1233
  try:
1187
- response = await retry_transient_errors(self._client.stub.VolumePutFiles2, request, base_delay=1)
1188
- except GRPCError as exc:
1189
- raise FileExistsError(exc.message) if exc.status == Status.ALREADY_EXISTS else exc
1234
+ response = await self._client.stub.VolumePutFiles2(request, retry=Retry(base_delay=1))
1235
+ except AlreadyExistsError as exc:
1236
+ raise FileExistsError(str(exc))
1190
1237
 
1191
1238
  if not response.missing_blocks:
1192
1239
  break
@@ -1245,7 +1292,7 @@ async def _put_missing_blocks(
1245
1292
  file_progress.pending_blocks.add(missing_block.block_index)
1246
1293
  task_progress_cb = functools.partial(progress_cb, task_id=file_progress.task_id)
1247
1294
 
1248
- @retry(n_attempts=5, base_delay=0.5, timeout=None)
1295
+ @retry(n_attempts=11, base_delay=0.5, timeout=None)
1249
1296
  async def put_missing_block_attempt(payload: BytesIOSegmentPayload) -> bytes:
1250
1297
  with payload.reset_on_error(subtract_progress=True):
1251
1298
  async with ClientSessionRegistry.get_session().put(