From fd5d75acaa4f5b42a91ba7fa361c05eac522ffc6 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 16:34:25 -0700 Subject: [PATCH 1/6] Respect REST endpoint capabilities when checking view existence Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pyiceberg/catalog/rest/__init__.py | 7 ++++ tests/catalog/test_rest.py | 55 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index d8f58773e6..0ca4abc180 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -1796,6 +1796,13 @@ def view_exists(self, identifier: str | Identifier) -> bool: Returns: bool: True if the view exists, False otherwise. """ + if Capability.V1_VIEW_EXISTS not in self._supported_endpoints: + try: + self.load_view(identifier) + return True + except NoSuchViewError: + return False + response = self._session.head( self.url(Endpoints.view_exists, prefixed=True, **self._split_identifier_for_path(identifier, IdentifierKind.VIEW)), ) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index a918829c24..60957118f1 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -945,6 +945,61 @@ def test_view_exists_multilevel_namespace_404(rest_mock: Mocker) -> None: assert not catalog.view_exists((multilevel_namespace, view)) +def test_view_exists_fallback_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None: + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/views/view", + json=example_view_metadata_rest_json, + request_headers=TEST_HEADERS, + ) + for config in ({"endpoints": [str(Capability.V1_LOAD_VIEW)]}, {"defaults": {"view-endpoints-supported": "true"}}): + rest_mock.get(f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}, **config}) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + assert catalog.view_exists(("fokko", "view")) + assert rest_mock.last_request.method == "GET" + + +def test_view_exists_fallback_404(rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + ) + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/views/view", + status_code=404, + request_headers=TEST_HEADERS, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + assert not catalog.view_exists(("fokko", "view")) + assert rest_mock.last_request.method == "GET" + + +def test_view_exists_fallback_500(rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + ) + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/views/view", + status_code=500, + request_headers=TEST_HEADERS, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + with pytest.raises(ServerError): + catalog.view_exists(("fokko", "view")) + assert rest_mock.last_request.method == "GET" + + +def test_view_exists_fallback_unsupported(rest_mock: Mocker) -> None: + rest_mock.get(f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}}) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + with pytest.raises(NotImplementedError, match="Server does not support endpoint"): + catalog.view_exists(("fokko", "view")) + + def test_list_namespaces_200(rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/namespaces", From b3fd9ce6e612b7ccb399b521862a97ed2eb76fca Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 16:39:01 -0700 Subject: [PATCH 2/6] Trim redundant view existence fallback tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/catalog/test_rest.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 60957118f1..2c5968806b 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -956,7 +956,6 @@ def test_view_exists_fallback_200(rest_mock: Mocker, example_view_metadata_rest_ catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) assert catalog.view_exists(("fokko", "view")) - assert rest_mock.last_request.method == "GET" def test_view_exists_fallback_404(rest_mock: Mocker) -> None: @@ -972,7 +971,6 @@ def test_view_exists_fallback_404(rest_mock: Mocker) -> None: catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) assert not catalog.view_exists(("fokko", "view")) - assert rest_mock.last_request.method == "GET" def test_view_exists_fallback_500(rest_mock: Mocker) -> None: @@ -989,15 +987,6 @@ def test_view_exists_fallback_500(rest_mock: Mocker) -> None: with pytest.raises(ServerError): catalog.view_exists(("fokko", "view")) - assert rest_mock.last_request.method == "GET" - - -def test_view_exists_fallback_unsupported(rest_mock: Mocker) -> None: - rest_mock.get(f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}}) - catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - - with pytest.raises(NotImplementedError, match="Server does not support endpoint"): - catalog.view_exists(("fokko", "view")) def test_list_namespaces_200(rest_mock: Mocker) -> None: From f0216740207f148c293284c228bd03a04e267fe5 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 16:40:51 -0700 Subject: [PATCH 3/6] Clarify view existence fallback test setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/catalog/test_rest.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 2c5968806b..3d4a0403c2 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -945,40 +945,44 @@ def test_view_exists_multilevel_namespace_404(rest_mock: Mocker) -> None: assert not catalog.view_exists((multilevel_namespace, view)) -def test_view_exists_fallback_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None: - rest_mock.get( +def test_view_exists_fallback_200(requests_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None: + requests_mock.get( f"{TEST_URI}v1/namespaces/fokko/views/view", json=example_view_metadata_rest_json, + status_code=200, request_headers=TEST_HEADERS, ) - for config in ({"endpoints": [str(Capability.V1_LOAD_VIEW)]}, {"defaults": {"view-endpoints-supported": "true"}}): - rest_mock.get(f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}, **config}) + for config_response in ( + {"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + {"defaults": {"view-endpoints-supported": "true"}, "overrides": {}}, + ): + requests_mock.get(f"{TEST_URI}v1/config", json=config_response, status_code=200) catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - assert catalog.view_exists(("fokko", "view")) -def test_view_exists_fallback_404(rest_mock: Mocker) -> None: - rest_mock.get( +def test_view_exists_fallback_404(requests_mock: Mocker) -> None: + requests_mock.get( f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + status_code=200, ) - rest_mock.get( + requests_mock.get( f"{TEST_URI}v1/namespaces/fokko/views/view", status_code=404, request_headers=TEST_HEADERS, ) catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - assert not catalog.view_exists(("fokko", "view")) -def test_view_exists_fallback_500(rest_mock: Mocker) -> None: - rest_mock.get( +def test_view_exists_fallback_500(requests_mock: Mocker) -> None: + requests_mock.get( f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + status_code=200, ) - rest_mock.get( + requests_mock.get( f"{TEST_URI}v1/namespaces/fokko/views/view", status_code=500, request_headers=TEST_HEADERS, From e9843e2edce39a199e85918872f1410f570e9453 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 16:41:51 -0700 Subject: [PATCH 4/6] Keep only the missing-view fallback error regression Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/catalog/test_rest.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 3d4a0403c2..6892feaf68 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -976,23 +976,6 @@ def test_view_exists_fallback_404(requests_mock: Mocker) -> None: assert not catalog.view_exists(("fokko", "view")) -def test_view_exists_fallback_500(requests_mock: Mocker) -> None: - requests_mock.get( - f"{TEST_URI}v1/config", - json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, - status_code=200, - ) - requests_mock.get( - f"{TEST_URI}v1/namespaces/fokko/views/view", - status_code=500, - request_headers=TEST_HEADERS, - ) - catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - - with pytest.raises(ServerError): - catalog.view_exists(("fokko", "view")) - - def test_list_namespaces_200(rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/namespaces", From bd6370ec86d623afac33bf7691e02e55d49d3cae Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 16:43:32 -0700 Subject: [PATCH 5/6] Split view existence fallback configuration cases Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/catalog/test_rest.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 6892feaf68..3eb3e5d748 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -946,19 +946,35 @@ def test_view_exists_multilevel_namespace_404(rest_mock: Mocker) -> None: def test_view_exists_fallback_200(requests_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None: + requests_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, + status_code=200, + ) requests_mock.get( f"{TEST_URI}v1/namespaces/fokko/views/view", json=example_view_metadata_rest_json, status_code=200, request_headers=TEST_HEADERS, ) - for config_response in ( - {"defaults": {}, "overrides": {}, "endpoints": [str(Capability.V1_LOAD_VIEW)]}, - {"defaults": {"view-endpoints-supported": "true"}, "overrides": {}}, - ): - requests_mock.get(f"{TEST_URI}v1/config", json=config_response, status_code=200) - catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - assert catalog.view_exists(("fokko", "view")) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + assert catalog.view_exists(("fokko", "view")) + + +def test_view_exists_fallback_200_legacy_server(requests_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None: + requests_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {"view-endpoints-supported": "true"}, "overrides": {}}, + status_code=200, + ) + requests_mock.get( + f"{TEST_URI}v1/namespaces/fokko/views/view", + json=example_view_metadata_rest_json, + status_code=200, + request_headers=TEST_HEADERS, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + assert catalog.view_exists(("fokko", "view")) def test_view_exists_fallback_404(requests_mock: Mocker) -> None: From a648775cb2438d11224b423e1647b82ba36a4253 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Sun, 13 Sep 2026 19:20:42 -0700 Subject: [PATCH 6/6] Explain the view existence fallback for older REST catalogs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pyiceberg/catalog/rest/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 0ca4abc180..8a50c22f15 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -1796,6 +1796,7 @@ def view_exists(self, identifier: str | Identifier) -> bool: Returns: bool: True if the view exists, False otherwise. """ + # fallback in order to work with older rest catalog implementations if Capability.V1_VIEW_EXISTS not in self._supported_endpoints: try: self.load_view(identifier)