|
32 | 32 | from pyiceberg import __version__ |
33 | 33 | from pyiceberg.catalog import BOTOCORE_SESSION, TOKEN, URI, WAREHOUSE_LOCATION, Catalog, PropertiesUpdateSummary |
34 | 34 | from pyiceberg.catalog.rest.auth import AUTH_MANAGER, AuthManager, AuthManagerAdapter, AuthManagerFactory, LegacyOAuth2AuthManager |
| 35 | +from pyiceberg.catalog.rest.credential_provider import ( |
| 36 | + REFRESH_CREDENTIALS_ENABLED, |
| 37 | + CredentialsProvider, |
| 38 | + resolve_storage_credentials, |
| 39 | +) |
35 | 40 | from pyiceberg.catalog.rest.response import _handle_non_200_response |
36 | 41 | from pyiceberg.catalog.rest.scan_planning import ( |
37 | 42 | FetchScanTasksRequest, |
@@ -466,26 +471,6 @@ def _create_session(self) -> Session: |
466 | 471 |
|
467 | 472 | return session |
468 | 473 |
|
469 | | - @staticmethod |
470 | | - def _resolve_storage_credentials(storage_credentials: list[StorageCredential], location: str | None) -> Properties: |
471 | | - """Resolve the best-matching storage credential by longest prefix match. |
472 | | -
|
473 | | - Mirrors the Java implementation in S3FileIO.clientForStoragePath() which iterates |
474 | | - over storage credential prefixes and selects the one with the longest match. |
475 | | -
|
476 | | - See: https://github.com/apache/iceberg/blob/main/aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java |
477 | | - """ |
478 | | - if not storage_credentials or not location: |
479 | | - return {} |
480 | | - |
481 | | - best_match: StorageCredential | None = None |
482 | | - for cred in storage_credentials: |
483 | | - if location.startswith(cred.prefix): |
484 | | - if best_match is None or len(cred.prefix) > len(best_match.prefix): |
485 | | - best_match = cred |
486 | | - |
487 | | - return best_match.config if best_match else {} |
488 | | - |
489 | 474 | def _load_file_io(self, properties: Properties = EMPTY_DICT, location: str | None = None) -> FileIO: |
490 | 475 | merged_properties = {**self.properties, **properties} |
491 | 476 | if self._auth_manager: |
@@ -827,37 +812,50 @@ def add_headers(self, request: PreparedRequest, **kwargs: Any) -> None: # pylin |
827 | 812 |
|
828 | 813 | def _response_to_table(self, identifier_tuple: tuple[str, ...], table_response: TableResponse) -> Table: |
829 | 814 | # Per Iceberg spec: storage-credentials take precedence over config |
830 | | - credential_config = self._resolve_storage_credentials( |
831 | | - table_response.storage_credentials, table_response.metadata_location |
| 815 | + credential_config = resolve_storage_credentials(table_response.storage_credentials, table_response.metadata_location) |
| 816 | + io = self._load_file_io( |
| 817 | + {**table_response.metadata.properties, **table_response.config, **credential_config}, |
| 818 | + table_response.metadata_location, |
832 | 819 | ) |
| 820 | + self._attach_credentials_provider(io, identifier_tuple, table_response.storage_credentials) |
833 | 821 | return Table( |
834 | 822 | identifier=identifier_tuple, |
835 | 823 | metadata_location=table_response.metadata_location, # type: ignore |
836 | 824 | metadata=table_response.metadata, |
837 | | - io=self._load_file_io( |
838 | | - {**table_response.metadata.properties, **table_response.config, **credential_config}, |
839 | | - table_response.metadata_location, |
840 | | - ), |
| 825 | + io=io, |
841 | 826 | catalog=self, |
842 | 827 | config=table_response.config, |
843 | 828 | ) |
844 | 829 |
|
845 | 830 | def _response_to_staged_table(self, identifier_tuple: tuple[str, ...], table_response: TableResponse) -> StagedTable: |
846 | 831 | # Per Iceberg spec: storage-credentials take precedence over config |
847 | | - credential_config = self._resolve_storage_credentials( |
848 | | - table_response.storage_credentials, table_response.metadata_location |
| 832 | + credential_config = resolve_storage_credentials(table_response.storage_credentials, table_response.metadata_location) |
| 833 | + io = self._load_file_io( |
| 834 | + {**table_response.metadata.properties, **table_response.config, **credential_config}, |
| 835 | + table_response.metadata_location, |
849 | 836 | ) |
| 837 | + self._attach_credentials_provider(io, identifier_tuple, table_response.storage_credentials) |
850 | 838 | return StagedTable( |
851 | 839 | identifier=identifier_tuple, |
852 | 840 | metadata_location=table_response.metadata_location, # type: ignore |
853 | 841 | metadata=table_response.metadata, |
854 | | - io=self._load_file_io( |
855 | | - {**table_response.metadata.properties, **table_response.config, **credential_config}, |
856 | | - table_response.metadata_location, |
857 | | - ), |
| 842 | + io=io, |
858 | 843 | catalog=self, |
859 | 844 | ) |
860 | 845 |
|
| 846 | + def _attach_credentials_provider( |
| 847 | + self, io: FileIO, identifier: str | Identifier, storage_credentials: list[StorageCredential] |
| 848 | + ) -> None: |
| 849 | + """Attach a CredentialsProvider to io if credential refresh is enabled and credentials were vended. |
| 850 | +
|
| 851 | + The refresh callback returns the full LoadCredentialsResponse so the provider can re-run |
| 852 | + longest-prefix matching against the freshly vended credentials. |
| 853 | + """ |
| 854 | + if storage_credentials and property_as_bool(self.properties, REFRESH_CREDENTIALS_ENABLED, False): |
| 855 | + io.set_credentials_provider( |
| 856 | + CredentialsProvider(storage_credentials, refresh_fn=lambda: self._load_credentials(identifier)) |
| 857 | + ) |
| 858 | + |
861 | 859 | def _response_to_view(self, identifier_tuple: tuple[str, ...], view_response: ViewResponse) -> View: |
862 | 860 | return View( |
863 | 861 | identifier=identifier_tuple, |
@@ -1124,7 +1122,7 @@ def load_credentials( |
1124 | 1122 | ) -> Properties: |
1125 | 1123 | """Load vended storage credentials and return the best match for a location.""" |
1126 | 1124 | credentials_response = self._load_credentials(identifier) |
1127 | | - return self._resolve_storage_credentials(credentials_response.storage_credentials, location) |
| 1125 | + return resolve_storage_credentials(credentials_response.storage_credentials, location) |
1128 | 1126 |
|
1129 | 1127 | @retry(**_RETRY_ARGS) |
1130 | 1128 | @override |
|
0 commit comments