diff --git a/mkdocs/docs/configuration.md b/mkdocs/docs/configuration.md index 1e364a11fe..dc51f81434 100644 --- a/mkdocs/docs/configuration.md +++ b/mkdocs/docs/configuration.md @@ -332,6 +332,7 @@ catalog: | rest.signing-region | us-east-1 | The region to use when SigV4 signing a request | | rest.signing-name | execute-api | The service signing name to use when SigV4 signing a request | | oauth2-server-uri | | Authentication URL to use for client credentials authentication (default: uri + 'v1/oauth/tokens') | +| snapshot-loading-mode | refs | The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. | diff --git a/poetry.lock b/poetry.lock index 849485ffbb..ec2872048b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1396,23 +1396,24 @@ typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flask" -version = "3.1.0" +version = "3.1.1" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, - {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, + {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, + {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, ] [package.dependencies] -blinker = ">=1.9" +blinker = ">=1.9.0" click = ">=8.1.3" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.1" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" [package.extras] async = ["asgiref (>=3.2)"] diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 18d875ea65..633ef6498b 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -138,6 +138,7 @@ class IdentifierKind(Enum): SIGV4_REGION = "rest.signing-region" SIGV4_SERVICE = "rest.signing-name" OAUTH2_SERVER_URI = "oauth2-server-uri" +SNAPSHOT_LOADING_MODE = "snapshot-loading-mode" NAMESPACE_SEPARATOR = b"\x1f".decode(UTF8) @@ -678,7 +679,16 @@ def list_tables(self, namespace: Union[str, Identifier]) -> List[Identifier]: @retry(**_RETRY_ARGS) def load_table(self, identifier: Union[str, Identifier]) -> Table: - response = self._session.get(self.url(Endpoints.load_table, prefixed=True, **self._split_identifier_for_path(identifier))) + params = {} + if mode := self.properties.get(SNAPSHOT_LOADING_MODE): + if mode in {"all", "refs"}: + params["snapshots"] = mode + else: + raise ValueError("Invalid snapshot-loading-mode: {}") + + response = self._session.get( + self.url(Endpoints.load_table, prefixed=True, **self._split_identifier_for_path(identifier)), params=params + ) try: response.raise_for_status() except HTTPError as exc: diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index b9c88d2fc4..dfc6a0e2fb 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -24,7 +24,7 @@ import pyiceberg from pyiceberg.catalog import PropertiesUpdateSummary, load_catalog -from pyiceberg.catalog.rest import OAUTH2_SERVER_URI, RestCatalog +from pyiceberg.catalog.rest import OAUTH2_SERVER_URI, SNAPSHOT_LOADING_MODE, RestCatalog from pyiceberg.exceptions import ( AuthorizationExpiredError, NamespaceAlreadyExistsError, @@ -853,6 +853,29 @@ def test_load_table_200(rest_mock: Mocker, example_table_metadata_with_snapshot_ assert actual == expected +def test_load_table_200_loading_mode( + rest_mock: Mocker, example_table_metadata_with_snapshot_v1_rest_json: Dict[str, Any] +) -> None: + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/tables/table?snapshots=refs", + json=example_table_metadata_with_snapshot_v1_rest_json, + status_code=200, + request_headers=TEST_HEADERS, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN, **{SNAPSHOT_LOADING_MODE: "refs"}) + actual = catalog.load_table(("fokko", "table")) + expected = Table( + identifier=("fokko", "table"), + metadata_location=example_table_metadata_with_snapshot_v1_rest_json["metadata-location"], + metadata=TableMetadataV1(**example_table_metadata_with_snapshot_v1_rest_json["metadata"]), + io=load_file_io(), + catalog=catalog, + ) + # First compare the dicts + assert actual.metadata.model_dump() == expected.metadata.model_dump() + assert actual == expected + + def test_load_table_honor_access_delegation( rest_mock: Mocker, example_table_metadata_with_snapshot_v1_rest_json: Dict[str, Any] ) -> None: