Source code for multistorageclient.client.client

  1# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  2# SPDX-License-Identifier: Apache-2.0
  3#
  4# Licensed under the Apache License, Version 2.0 (the "License");
  5# you may not use this file except in compliance with the License.
  6# You may obtain a copy of the License at
  7#
  8# http://www.apache.org/licenses/LICENSE-2.0
  9#
 10# Unless required by applicable law or agreed to in writing, software
 11# distributed under the License is distributed on an "AS IS" BASIS,
 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13# See the License for the specific language governing permissions and
 14# limitations under the License.
 15
 16import logging
 17from collections.abc import Iterator, Sequence
 18from typing import IO, Any, Optional, Union
 19
 20from ..config import StorageClientConfig
 21from ..constants import MEMORY_LOAD_LIMIT
 22from ..file import ObjectFile, PosixFile
 23from ..types import (
 24    ExecutionMode,
 25    MetadataProvider,
 26    ObjectMetadata,
 27    PatternList,
 28    Range,
 29    SignerType,
 30    SourceVersionCheckMode,
 31    StorageProvider,
 32    SyncResult,
 33)
 34from .composite import CompositeStorageClient
 35from .single import SingleStorageClient
 36from .types import AbstractStorageClient
 37
 38logger = logging.getLogger(__name__)
 39
 40
[docs] 41class StorageClient(AbstractStorageClient): 42 """ 43 Unified storage client facade. 44 45 Automatically delegates to: 46 - SingleStorageClient: For single-backend configurations (full read/write) 47 - CompositeStorageClient: For multi-backend configurations (read-only) 48 """ 49 50 _delegate: Union[SingleStorageClient, CompositeStorageClient] 51 52 def __init__(self, config: StorageClientConfig): 53 if config.storage_provider_profiles: 54 self._delegate = CompositeStorageClient(config) 55 logger.debug(f"StorageClient '{config.profile}' using CompositeStorageClient (read-only)") 56 else: 57 self._delegate = SingleStorageClient(config) 58 logger.debug(f"StorageClient '{config.profile}' using SingleStorageClient") 59 60 @property 61 def delegate(self) -> Union[SingleStorageClient, CompositeStorageClient]: 62 """ 63 Access to underlying delegate storage client. 64 65 :return: SingleStorageClient or CompositeStorageClient. 66 """ 67 return self._delegate 68 69 @property 70 def _config(self) -> StorageClientConfig: 71 """ 72 :return: The configuration for the underlying storage client. 73 """ 74 return self._delegate._config 75 76 @property 77 def _storage_provider(self) -> Optional[StorageProvider]: 78 """ 79 :return: The storage provider for the underlying storage client. None for CompositeStorageClient. 80 """ 81 return self._delegate._storage_provider 82 83 @_storage_provider.setter 84 def _storage_provider(self, value: StorageProvider) -> None: 85 """Allow mutation of storage provider for testing purposes.""" 86 if isinstance(self._delegate, SingleStorageClient): 87 self._delegate._storage_provider = value 88 89 @property 90 def _metadata_provider(self) -> Optional[MetadataProvider]: 91 """ 92 :return: The metadata provider for the underlying storage client. 93 """ 94 return self._delegate._metadata_provider 95 96 @_metadata_provider.setter 97 def _metadata_provider(self, value: Optional[MetadataProvider]) -> None: 98 """Allow mutation of metadata provider for DSS compatibility.""" 99 if isinstance(self._delegate, CompositeStorageClient) and value is None: 100 raise ValueError("CompositeStorageClient requires a metadata_provider for routing decisions.") 101 self._delegate._metadata_provider = value # type: ignore[assignment] 102 103 @property 104 def _metadata_provider_lock(self): 105 """ 106 Access to metadata provider lock for DSS compatibility. 107 108 :return: The lock for the metadata provider. 109 """ 110 return self._delegate._metadata_provider_lock 111 112 @_metadata_provider_lock.setter 113 def _metadata_provider_lock(self, value): 114 """Allow mutation of metadata provider lock for DSS compatibility.""" 115 self._delegate._metadata_provider_lock = value 116 117 @property 118 def _credentials_provider(self): 119 """ 120 :return: The credentials provider for the underlying storage client. 121 """ 122 return self._delegate._credentials_provider 123 124 @property 125 def _retry_config(self): 126 """ 127 :return: The retry configuration for the underlying storage client. 128 """ 129 return self._delegate._retry_config 130 131 @property 132 def _cache_manager(self): 133 """ 134 :return: The cache manager for the underlying storage client. 135 """ 136 return self._delegate._cache_manager 137 138 @property 139 def _replica_manager(self): 140 """ 141 :return: The replica manager for the underlying storage client. 142 """ 143 return self._delegate._replica_manager 144 145 @_replica_manager.setter 146 def _replica_manager(self, value): 147 """ 148 Allow mutation of replica manager for testing purposes. 149 150 :param value: The new replica manager. 151 """ 152 if isinstance(self._delegate, SingleStorageClient): 153 self._delegate._replica_manager = value 154 155 @property 156 def profile(self) -> str: 157 """ 158 :return: The profile name of the storage client. 159 """ 160 return self._delegate.profile 161 162 @property 163 def replicas(self) -> list[AbstractStorageClient]: 164 """ 165 :return: List of replica storage clients, sorted by read priority. 166 """ 167 return self._delegate.replicas 168
[docs] 169 def is_default_profile(self) -> bool: 170 """ 171 :return: ``True`` if the storage client is using the default profile, ``False`` otherwise. 172 """ 173 return self._delegate.is_default_profile()
174 175 def _is_rust_client_enabled(self) -> bool: 176 """ 177 :return: ``True`` if the storage provider is using the Rust client, ``False`` otherwise. 178 """ 179 return self._delegate._is_rust_client_enabled() 180 181 def _is_posix_file_storage_provider(self) -> bool: 182 """ 183 :return: ``True`` if the storage client is using a POSIX file storage provider, ``False`` otherwise. 184 """ 185 return self._delegate._is_posix_file_storage_provider() 186
[docs] 187 def get_posix_path(self, path: str) -> Optional[str]: 188 """ 189 Returns the physical POSIX filesystem path for POSIX storage providers. 190 191 :param path: The path to resolve (may be a symlink or virtual path). 192 :return: Physical POSIX filesystem path if POSIX storage, None otherwise. 193 """ 194 return self._delegate.get_posix_path(path)
195
[docs] 196 def read( 197 self, 198 path: str, 199 byte_range: Optional[Range] = None, 200 check_source_version: SourceVersionCheckMode = SourceVersionCheckMode.INHERIT, 201 ) -> bytes: 202 """ 203 Read bytes from a file at the specified logical path. 204 205 :param path: The logical path of the object to read. 206 :param byte_range: Optional byte range to read (offset and length). 207 :param check_source_version: Whether to check the source version of cached objects. 208 :return: The content of the object as bytes. 209 :raises FileNotFoundError: If the file at the specified path does not exist. 210 """ 211 return self._delegate.read(path, byte_range, check_source_version)
212
[docs] 213 def open( 214 self, 215 path: str, 216 mode: str = "rb", 217 buffering: int = -1, 218 encoding: Optional[str] = None, 219 disable_read_cache: bool = False, 220 memory_load_limit: int = MEMORY_LOAD_LIMIT, 221 atomic: bool = True, 222 check_source_version: SourceVersionCheckMode = SourceVersionCheckMode.INHERIT, 223 attributes: Optional[dict[str, str]] = None, 224 prefetch_file: bool = True, 225 ) -> Union[PosixFile, ObjectFile]: 226 """ 227 Open a file for reading or writing. 228 229 :param path: The logical path of the object to open. 230 :param mode: The file mode. Supported modes: "r", "rb", "w", "wb", "a", "ab". 231 :param buffering: The buffering mode. Only applies to PosixFile. 232 :param encoding: The encoding to use for text files. 233 :param disable_read_cache: When set to ``True``, disables caching for file content. 234 This parameter is only applicable to ObjectFile when the mode is "r" or "rb". 235 :param memory_load_limit: Size limit in bytes for loading files into memory. Defaults to 512MB. 236 This parameter is only applicable to ObjectFile when the mode is "r" or "rb". Defaults to 512MB. 237 :param atomic: When set to ``True``, file will be written atomically (rename upon close). 238 This parameter is only applicable to PosixFile in write mode. 239 :param check_source_version: Whether to check the source version of cached objects. 240 :param attributes: Attributes to add to the file. 241 This parameter is only applicable when the mode is "w" or "wb" or "a" or "ab". Defaults to None. 242 :param prefetch_file: Whether to prefetch the file content. 243 This parameter is only applicable to ObjectFile when the mode is "r" or "rb". Defaults to True. 244 :return: A file-like object (PosixFile or ObjectFile) for the specified path. 245 :raises FileNotFoundError: If the file does not exist (read mode). 246 :raises NotImplementedError: If the operation is not supported (e.g., write on CompositeStorageClient). 247 """ 248 return self._delegate.open( 249 path, 250 mode, 251 buffering, 252 encoding, 253 disable_read_cache, 254 memory_load_limit, 255 atomic, 256 check_source_version, 257 attributes, 258 prefetch_file, 259 )
260
[docs] 261 def download_file(self, remote_path: str, local_path: Union[str, IO]) -> None: 262 """ 263 Download a remote file to a local path or file-like object. 264 265 :param remote_path: The logical path of the remote file to download. 266 :param local_path: The local file path or file-like object to write to. 267 :raises FileNotFoundError: If the remote file does not exist. 268 """ 269 return self._delegate.download_file(remote_path, local_path)
270
[docs] 271 def download_files( 272 self, 273 remote_paths: list[str], 274 local_paths: list[str], 275 metadata: Optional[Sequence[Optional[ObjectMetadata]]] = None, 276 max_workers: int = 16, 277 ) -> None: 278 """ 279 Download multiple remote files to local paths. 280 281 :param remote_paths: List of logical paths of remote files to download. 282 :param local_paths: List of local file paths to save the downloaded files to. 283 :param metadata: Optional per-file metadata used to decide between regular and multipart download. 284 :param max_workers: Maximum number of concurrent download workers (default: 16). 285 :raises ValueError: If remote_paths and local_paths have different lengths. 286 :raises FileNotFoundError: If any remote file does not exist. 287 """ 288 return self._delegate.download_files(remote_paths, local_paths, metadata, max_workers)
289
[docs] 290 def glob( 291 self, 292 pattern: str, 293 include_url_prefix: bool = False, 294 attribute_filter_expression: Optional[str] = None, 295 ) -> list[str]: 296 """ 297 Matches and retrieves a list of object keys in the storage provider that match the specified pattern. 298 299 :param pattern: The pattern to match object keys against, supporting wildcards (e.g., ``*.txt``). 300 :param include_url_prefix: Whether to include the URL prefix ``msc://profile`` in the result. 301 :param attribute_filter_expression: The attribute filter expression to apply to the result. 302 :return: A list of object paths that match the specified pattern. 303 """ 304 return self._delegate.glob(pattern, include_url_prefix, attribute_filter_expression)
305
[docs] 306 def list_recursive( 307 self, 308 path: str = "", 309 start_after: Optional[str] = None, 310 end_at: Optional[str] = None, 311 max_workers: int = 32, 312 look_ahead: int = 2, 313 include_url_prefix: bool = False, 314 follow_symlinks: bool = True, 315 patterns: Optional[PatternList] = None, 316 ) -> Iterator[ObjectMetadata]: 317 """ 318 List files recursively in the storage provider under the specified path. 319 320 :param path: The directory or file path to list objects under. This should be a 321 complete filesystem path (e.g., "my-bucket/documents/" or "data/2024/"). 322 :param start_after: The key to start after (i.e. exclusive). An object with this key doesn't have to exist. 323 :param end_at: The key to end at (i.e. inclusive). An object with this key doesn't have to exist. 324 :param max_workers: Maximum concurrent workers for provider-level recursive listing. 325 :param look_ahead: Prefixes to buffer per worker for provider-level recursive listing. 326 :param include_url_prefix: Whether to include the URL prefix ``msc://profile`` in the result. 327 :param follow_symlinks: Whether to follow symbolic links. Only applicable for POSIX file storage providers. When ``False``, symlinks are skipped during listing. 328 :param patterns: PatternList for include/exclude filtering. If None, all files are included. 329 :return: An iterator over ObjectMetadata for matching files. 330 """ 331 return self._delegate.list_recursive( 332 path=path, 333 start_after=start_after, 334 end_at=end_at, 335 max_workers=max_workers, 336 look_ahead=look_ahead, 337 include_url_prefix=include_url_prefix, 338 follow_symlinks=follow_symlinks, 339 patterns=patterns, 340 )
341
[docs] 342 def is_file(self, path: str) -> bool: 343 """ 344 Checks whether the specified path points to a file (rather than a folder or directory). 345 346 :param path: The logical path to check. 347 :return: ``True`` if the key points to a file, ``False`` otherwise. 348 """ 349 return self._delegate.is_file(path)
350
[docs] 351 def is_empty(self, path: str) -> bool: 352 """ 353 Check whether the specified path is empty. A path is considered empty if there are no 354 objects whose keys start with the given path as a prefix. 355 356 :param path: The logical path to check (typically a directory or folder prefix). 357 :return: ``True`` if no objects exist under the specified path prefix, ``False`` otherwise. 358 """ 359 return self._delegate.is_empty(path)
360
[docs] 361 def info(self, path: str, strict: bool = True) -> ObjectMetadata: 362 """ 363 Get metadata for a file at the specified path. 364 365 :param path: The logical path of the object. 366 :param strict: When ``True``, only return committed metadata. When ``False``, include pending changes. 367 :return: ObjectMetadata containing file information (size, last modified, etc.). 368 :raises FileNotFoundError: If the file at the specified path does not exist. 369 """ 370 return self._delegate.info(path, strict)
371
[docs] 372 def write( 373 self, 374 path: str, 375 body: bytes, 376 attributes: Optional[dict[str, str]] = None, 377 ) -> None: 378 """ 379 Write bytes to a file at the specified path. 380 381 :param path: The logical path where the object will be written. 382 :param body: The content to write as bytes. 383 :param attributes: Optional attributes to add to the file. 384 :raises NotImplementedError: If write operations are not supported (e.g., CompositeStorageClient). 385 """ 386 return self._delegate.write(path, body, attributes)
387
[docs] 388 def delete(self, path: str, recursive: bool = False) -> None: 389 """ 390 Delete a file or directory at the specified path. 391 392 :param path: The logical path of the object to delete. 393 :param recursive: When True, delete directory and all its contents recursively. 394 :raises FileNotFoundError: If the file or directory does not exist. 395 :raises NotImplementedError: If delete operations are not supported (e.g., CompositeStorageClient). 396 """ 397 return self._delegate.delete(path, recursive)
398
[docs] 399 def delete_many(self, paths: list[str]) -> None: 400 """ 401 Delete multiple files at the specified paths. Only files are supported; directories are not deleted. 402 403 :param paths: List of logical paths of the files to delete. 404 :raises NotImplementedError: If delete operations are not supported (e.g., CompositeStorageClient). 405 """ 406 return self._delegate.delete_many(paths)
407
[docs] 408 def copy(self, src_path: str, dest_path: str) -> None: 409 """ 410 Copy a file from source path to destination path. 411 412 :param src_path: The logical path of the source object. 413 :param dest_path: The logical path where the object will be copied to. 414 :raises FileNotFoundError: If the source file does not exist. 415 :raises NotImplementedError: If copy operations are not supported (e.g., CompositeStorageClient). 416 """ 417 return self._delegate.copy(src_path, dest_path)
418
[docs] 419 def upload_file( 420 self, 421 remote_path: str, 422 local_path: Union[str, IO], 423 attributes: Optional[dict[str, str]] = None, 424 ) -> None: 425 """ 426 Upload a local file to remote storage. 427 428 :param remote_path: The logical path where the file will be uploaded. 429 :param local_path: The local file path or file-like object to upload. 430 :param attributes: Optional attributes to add to the file. 431 :raises FileNotFoundError: If the local file does not exist. 432 :raises NotImplementedError: If upload operations are not supported (e.g., CompositeStorageClient). 433 """ 434 return self._delegate.upload_file(remote_path, local_path, attributes)
435
[docs] 436 def upload_files( 437 self, 438 remote_paths: list[str], 439 local_paths: list[str], 440 attributes: Optional[Sequence[Optional[dict[str, str]]]] = None, 441 max_workers: int = 16, 442 ) -> None: 443 """ 444 Upload multiple local files to remote storage. 445 446 :param remote_paths: List of logical paths where the files will be uploaded. 447 :param local_paths: List of local file paths to upload. 448 :param attributes: Optional list of per-file attributes to add. When provided, must have the same length 449 as remote_paths/local_paths. Each element may be ``None`` for files that need no attributes. 450 :param max_workers: Maximum number of concurrent upload workers (default: 16). 451 :raises ValueError: If remote_paths and local_paths have different lengths. 452 :raises ValueError: If attributes is provided and has a different length than remote_paths. 453 :raises NotImplementedError: If upload operations are not supported (e.g., CompositeStorageClient). 454 """ 455 return self._delegate.upload_files(remote_paths, local_paths, attributes, max_workers)
456
[docs] 457 def commit_metadata(self, prefix: Optional[str] = None) -> None: 458 """ 459 Commits any pending updates to the metadata provider. No-op if not using a metadata provider. 460 461 :param prefix: If provided, scans the prefix to find files to commit. 462 """ 463 return self._delegate.commit_metadata(prefix)
464
[docs] 465 def sync_from( 466 self, 467 source_client: AbstractStorageClient, 468 source_path: str = "", 469 target_path: str = "", 470 delete_unmatched_files: bool = False, 471 description: str = "Syncing", 472 num_worker_processes: Optional[int] = None, 473 execution_mode: ExecutionMode = ExecutionMode.LOCAL, 474 patterns: Optional[PatternList] = None, 475 preserve_source_attributes: bool = False, 476 follow_symlinks: bool = True, 477 source_files: Optional[list[str]] = None, 478 ignore_hidden: bool = True, 479 commit_metadata: bool = True, 480 dryrun: bool = False, 481 dryrun_output_path: Optional[str] = None, 482 ) -> SyncResult: 483 """ 484 Syncs files from the source storage client to "path/". 485 486 :param source_client: The source storage client. 487 :param source_path: The logical path to sync from. 488 :param target_path: The logical path to sync to. 489 :param delete_unmatched_files: Whether to delete files at the target that are not present at the source. 490 :param description: Description of sync process for logging purposes. 491 :param num_worker_processes: The number of worker processes to use. 492 :param execution_mode: The execution mode to use. Currently supports "local" and "ray". 493 :param patterns: PatternList for include/exclude filtering. If None, all files are included. 494 Cannot be used together with source_files. 495 :param preserve_source_attributes: Whether to preserve source file metadata attributes during synchronization. 496 When ``False`` (default), only file content is copied. When ``True``, custom metadata attributes are also preserved. 497 498 .. warning:: 499 **Performance Impact**: When enabled without a ``metadata_provider`` configured, this will make a HEAD 500 request for each object to retrieve attributes, which can significantly impact performance on large-scale 501 sync operations. For production use at scale, configure a ``metadata_provider`` in your storage profile. 502 503 :param follow_symlinks: If the source StorageClient is PosixFile, whether to follow symbolic links. Default is ``True``. 504 :param source_files: Optional list of file paths (relative to source_path) to sync. When provided, only these 505 specific files will be synced, skipping enumeration of the source path. Cannot be used together with patterns. 506 :param ignore_hidden: Whether to ignore hidden files and directories. Default is ``True``. 507 :param commit_metadata: When ``True`` (default), calls :py:meth:`StorageClient.commit_metadata` after sync completes. 508 Set to ``False`` to skip the commit, allowing batching of multiple sync operations before committing manually. 509 :param dryrun: If ``True``, only enumerate and compare objects without performing any copy/delete operations. 510 The returned :py:class:`SyncResult` will include a :py:class:`DryrunResult` with paths to JSONL files. 511 :param dryrun_output_path: Directory to write dryrun JSONL files into. If ``None`` (default), a temporary 512 directory is created automatically. Ignored when ``dryrun`` is ``False``. 513 :raises ValueError: If both source_files and patterns are provided. 514 :raises NotImplementedError: If sync operations are not supported (e.g., CompositeStorageClient as target). 515 """ 516 return self._delegate.sync_from( 517 source_client, 518 source_path, 519 target_path, 520 delete_unmatched_files, 521 description, 522 num_worker_processes, 523 execution_mode, 524 patterns, 525 preserve_source_attributes, 526 follow_symlinks, 527 source_files, 528 ignore_hidden, 529 commit_metadata, 530 dryrun, 531 dryrun_output_path, 532 )
533
[docs] 534 def sync_replicas( 535 self, 536 source_path: str, 537 replica_indices: Optional[list[int]] = None, 538 delete_unmatched_files: bool = False, 539 description: str = "Syncing replica", 540 num_worker_processes: Optional[int] = None, 541 execution_mode: ExecutionMode = ExecutionMode.LOCAL, 542 patterns: Optional[PatternList] = None, 543 ignore_hidden: bool = True, 544 ) -> None: 545 """ 546 Sync files from this client to its replica storage clients. 547 548 :param source_path: The logical path to sync from. 549 :param replica_indices: Specific replica indices to sync to (0-indexed). If None, syncs to all replicas. 550 :param delete_unmatched_files: When set to ``True``, delete files in replicas that don't exist in source. 551 :param description: Description of sync process for logging purposes. 552 :param num_worker_processes: Number of worker processes for parallel sync. 553 :param execution_mode: Execution mode (LOCAL or REMOTE). 554 :param patterns: PatternList for include/exclude filtering. If None, all files are included. 555 :param ignore_hidden: When set to ``True``, ignore hidden files (starting with '.'). Defaults to ``True``. 556 """ 557 return self._delegate.sync_replicas( 558 source_path, 559 replica_indices, 560 delete_unmatched_files, 561 description, 562 num_worker_processes, 563 execution_mode, 564 patterns, 565 ignore_hidden, 566 )
567
[docs] 568 def list( 569 self, 570 prefix: str = "", 571 path: str = "", 572 start_after: Optional[str] = None, 573 end_at: Optional[str] = None, 574 include_directories: bool = False, 575 include_url_prefix: bool = False, 576 attribute_filter_expression: Optional[str] = None, 577 show_attributes: bool = False, 578 follow_symlinks: bool = True, 579 patterns: Optional[PatternList] = None, 580 ) -> Iterator[ObjectMetadata]: 581 """ 582 List objects in the storage provider under the specified path. 583 584 **IMPORTANT**: Use the ``path`` parameter for new code. The ``prefix`` parameter is 585 deprecated and will be removed in a future version. 586 587 :param prefix: [DEPRECATED] Use ``path`` instead. The prefix to list objects under. 588 :param path: The directory or file path to list objects under. This should be a 589 complete filesystem path (e.g., "my-bucket/documents/" or "data/2024/"). 590 Cannot be used together with ``prefix``. 591 :param start_after: The key to start after (i.e. exclusive). An object with this key doesn't have to exist. 592 :param end_at: The key to end at (i.e. inclusive). An object with this key doesn't have to exist. 593 :param include_directories: Whether to include directories in the result. When ``True``, directories are returned alongside objects. 594 :param include_url_prefix: Whether to include the URL prefix ``msc://profile`` in the result. 595 :param attribute_filter_expression: The attribute filter expression to apply to the result. 596 :param show_attributes: Whether to return attributes in the result. WARNING: Depending on implementation, there may be a performance impact if this is set to ``True``. 597 :param follow_symlinks: Whether to follow symbolic links. Only applicable for POSIX file storage providers. When ``False``, symlinks are skipped during listing. 598 :param patterns: PatternList for include/exclude filtering. If None, all files are included. 599 :return: An iterator over ObjectMetadata for matching objects. 600 :raises ValueError: If both ``path`` and ``prefix`` parameters are provided (both non-empty). 601 """ 602 return self._delegate.list( 603 prefix, 604 path, 605 start_after, 606 end_at, 607 include_directories, 608 include_url_prefix, 609 attribute_filter_expression, 610 show_attributes, 611 follow_symlinks, 612 patterns, 613 )
614
[docs] 615 def generate_presigned_url( 616 self, 617 path: str, 618 *, 619 method: str = "GET", 620 signer_type: Optional[SignerType] = None, 621 signer_options: Optional[dict[str, Any]] = None, 622 ) -> str: 623 """ 624 Generate a pre-signed URL granting temporary access to the object at *path*. 625 626 :param path: The logical path of the object. 627 :param method: The HTTP method the URL should authorise (e.g. ``"GET"``, ``"PUT"``). 628 :param signer_type: The signing backend to use. ``None`` means the provider's native signer. 629 :param signer_options: Backend-specific options forwarded to the signer. 630 :return: A pre-signed URL string. 631 :raises NotImplementedError: If the underlying storage provider does not support presigned URLs. 632 """ 633 return self._delegate.generate_presigned_url( 634 path, method=method, signer_type=signer_type, signer_options=signer_options 635 )
636 637 def __getstate__(self) -> dict[str, Any]: 638 """Support for pickling (forward to delegate).""" 639 return self._delegate.__getstate__() 640 641 def __setstate__(self, state: dict[str, Any]) -> None: 642 """Support for unpickling - reconstruct the delegate.""" 643 config = state["_config"] 644 645 if config.storage_provider_profiles: 646 self._delegate = CompositeStorageClient.__new__(CompositeStorageClient) 647 self._delegate.__setstate__(state) 648 else: 649 self._delegate = SingleStorageClient.__new__(SingleStorageClient) 650 self._delegate.__setstate__(state)