|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
19 | 19 | from collections import deque |
| 20 | +from collections.abc import Sequence |
20 | 21 | from enum import Enum |
21 | 22 | from typing import ( |
22 | 23 | TYPE_CHECKING, |
@@ -147,6 +148,7 @@ class Endpoints: |
147 | 148 | create_table: str = "namespaces/{namespace}/tables" |
148 | 149 | register_table: str = "namespaces/{namespace}/register" |
149 | 150 | load_table: str = "namespaces/{namespace}/tables/{table}" |
| 151 | + load_credentials: str = "namespaces/{namespace}/tables/{table}/credentials" |
150 | 152 | update_table: str = "namespaces/{namespace}/tables/{table}" |
151 | 153 | drop_table: str = "namespaces/{namespace}/tables/{table}" |
152 | 154 | table_exists: str = "namespaces/{namespace}/tables/{table}" |
@@ -181,6 +183,7 @@ class Capability: |
181 | 183 | V1_DELETE_TABLE = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_table}") |
182 | 184 | V1_RENAME_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.rename_table}") |
183 | 185 | V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}") |
| 186 | + V1_LOAD_CREDENTIALS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_credentials}") |
184 | 187 |
|
185 | 188 | V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}") |
186 | 189 | V1_LOAD_VIEW = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_view}") |
@@ -293,6 +296,10 @@ class TableResponse(IcebergBaseModel): |
293 | 296 | storage_credentials: list[StorageCredential] = Field(alias="storage-credentials", default_factory=list) |
294 | 297 |
|
295 | 298 |
|
| 299 | +class LoadCredentialsResponse(IcebergBaseModel): |
| 300 | + storage_credentials: list[StorageCredential] = Field(alias="storage-credentials") |
| 301 | + |
| 302 | + |
296 | 303 | class ViewResponse(IcebergBaseModel): |
297 | 304 | metadata_location: str | None = Field(alias="metadata-location", default=None) |
298 | 305 | metadata: ViewMetadata |
@@ -480,6 +487,12 @@ def _resolve_storage_credentials(storage_credentials: list[StorageCredential], l |
480 | 487 |
|
481 | 488 | return best_match.config if best_match else {} |
482 | 489 |
|
| 490 | + @staticmethod |
| 491 | + def _format_referenced_by(referenced_by: str | Sequence[str]) -> str: |
| 492 | + if isinstance(referenced_by, str): |
| 493 | + return referenced_by |
| 494 | + return ",".join(referenced_by) |
| 495 | + |
483 | 496 | def _load_file_io(self, properties: Properties = EMPTY_DICT, location: str | None = None) -> FileIO: |
484 | 497 | merged_properties = {**self.properties, **properties} |
485 | 498 | if self._auth_manager: |
@@ -545,6 +558,43 @@ def _fetch_scan_tasks(self, identifier: str | Identifier, plan_task: str) -> Sca |
545 | 558 |
|
546 | 559 | return ScanTasks.model_validate_json(response.text) |
547 | 560 |
|
| 561 | + @retry(**_RETRY_ARGS) |
| 562 | + def _load_credentials( |
| 563 | + self, |
| 564 | + identifier: str | Identifier, |
| 565 | + plan_id: str | None = None, |
| 566 | + referenced_by: str | Sequence[str] | None = None, |
| 567 | + ) -> LoadCredentialsResponse: |
| 568 | + """Load raw vended storage credentials for a table.""" |
| 569 | + self._check_endpoint(Capability.V1_LOAD_CREDENTIALS) |
| 570 | + params: dict[str, str] = {} |
| 571 | + if plan_id is not None: |
| 572 | + params["planId"] = plan_id |
| 573 | + if referenced_by is not None: |
| 574 | + params["referenced-by"] = self._format_referenced_by(referenced_by) |
| 575 | + |
| 576 | + response = self._session.get( |
| 577 | + self.url(Endpoints.load_credentials, prefixed=True, **self._split_identifier_for_path(identifier)), |
| 578 | + params=params, |
| 579 | + ) |
| 580 | + try: |
| 581 | + response.raise_for_status() |
| 582 | + except HTTPError as exc: |
| 583 | + _handle_non_200_response(exc, {404: NoSuchTableError}) |
| 584 | + |
| 585 | + return LoadCredentialsResponse.model_validate_json(response.text) |
| 586 | + |
| 587 | + def load_credentials( |
| 588 | + self, |
| 589 | + identifier: str | Identifier, |
| 590 | + location: str, |
| 591 | + plan_id: str | None = None, |
| 592 | + referenced_by: str | Sequence[str] | None = None, |
| 593 | + ) -> Properties: |
| 594 | + """Load vended storage credentials and return the best match for a location.""" |
| 595 | + credentials_response = self._load_credentials(identifier, plan_id=plan_id, referenced_by=referenced_by) |
| 596 | + return self._resolve_storage_credentials(credentials_response.storage_credentials, location) |
| 597 | + |
548 | 598 | def plan_scan(self, identifier: str | Identifier, request: PlanTableScanRequest) -> list[FileScanTask]: |
549 | 599 | """Plan a table scan and return FileScanTasks. |
550 | 600 |
|
|
0 commit comments