From c2cd4874d3e57c2772fa4c3aa3f556ca4ee86312 Mon Sep 17 00:00:00 2001 From: geruh Date: Thu, 18 Dec 2025 19:33:12 -0800 Subject: [PATCH 1/2] feat: Add scan planning endpoints and configs --- pyiceberg/catalog/rest/__init__.py | 11 +++++++ tests/catalog/test_rest.py | 50 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 3b77fd47f0..7674fc2362 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -96,6 +96,8 @@ class Endpoints: list_views: str = "namespaces/{namespace}/views" drop_view: str = "namespaces/{namespace}/views/{view}" view_exists: str = "namespaces/{namespace}/views/{view}" + plan_table_scan: str = "namespaces/{namespace}/tables/{table}/plan" + fetch_scan_tasks: str = "namespaces/{namespace}/tables/{table}/tasks" class IdentifierKind(Enum): @@ -130,6 +132,7 @@ class IdentifierKind(Enum): SNAPSHOT_LOADING_MODE = "snapshot-loading-mode" AUTH = "auth" CUSTOM = "custom" +REST_SCAN_PLANNING_ENABLED = "rest-scan-planning-enabled" NAMESPACE_SEPARATOR = b"\x1f".decode(UTF8) @@ -269,6 +272,14 @@ def _create_session(self) -> Session: return session + def is_scan_planning_enabled(self) -> bool: + """Check if server-side scan planning is enabled. + + Returns: + True if enabled, False otherwise. + """ + return property_as_bool(self.properties, REST_SCAN_PLANNING_ENABLED, False) + def _create_legacy_oauth2_auth_manager(self, session: Session) -> AuthManager: """Create the LegacyOAuth2AuthManager by fetching required properties. diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index b8bee00225..98f3782306 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -1993,3 +1993,53 @@ def test_rest_catalog_context_manager_with_exception_sigv4(self, rest_mock: Mock assert catalog is not None and hasattr(catalog, "_session") assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS_SIGV4 + + def test_scan_planning_disabled_by_default(self, rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + assert catalog.is_scan_planning_enabled() is False + + def test_scan_planning_enabled_by_property(self, rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + catalog = RestCatalog( + "rest", + uri=TEST_URI, + token=TEST_TOKEN, + **{"rest-scan-planning-enabled": "true"}, + ) + + assert catalog.is_scan_planning_enabled() is True + + def test_scan_planning_explicitly_disabled(self, rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + catalog = RestCatalog( + "rest", + uri=TEST_URI, + token=TEST_TOKEN, + **{"rest.scan-planning.enabled": "false"}, + ) + + assert catalog.is_scan_planning_enabled() is False + + def test_scan_planning_enabled_from_server_config(self, rest_mock: Mocker) -> None: + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {"rest-scan-planning-enabled": "true"}, "overrides": {}}, + status_code=200, + ) + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + + assert catalog.is_scan_planning_enabled() is True From 4463c406c5e7044b7fcbc9b16b7066e972be5e45 Mon Sep 17 00:00:00 2001 From: geruh Date: Fri, 19 Dec 2025 14:04:53 -0800 Subject: [PATCH 2/2] address feedback --- pyiceberg/catalog/rest/__init__.py | 7 ++++--- tests/catalog/test_rest.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 7674fc2362..a28ff562bd 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -133,6 +133,7 @@ class IdentifierKind(Enum): AUTH = "auth" CUSTOM = "custom" REST_SCAN_PLANNING_ENABLED = "rest-scan-planning-enabled" +REST_SCAN_PLANNING_ENABLED_DEFAULT = False NAMESPACE_SEPARATOR = b"\x1f".decode(UTF8) @@ -272,13 +273,13 @@ def _create_session(self) -> Session: return session - def is_scan_planning_enabled(self) -> bool: - """Check if server-side scan planning is enabled. + def is_rest_scan_planning_enabled(self) -> bool: + """Check if rest server-side scan planning is enabled. Returns: True if enabled, False otherwise. """ - return property_as_bool(self.properties, REST_SCAN_PLANNING_ENABLED, False) + return property_as_bool(self.properties, REST_SCAN_PLANNING_ENABLED, REST_SCAN_PLANNING_ENABLED_DEFAULT) def _create_legacy_oauth2_auth_manager(self, session: Session) -> AuthManager: """Create the LegacyOAuth2AuthManager by fetching required properties. diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 98f3782306..464314f3be 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -1994,7 +1994,7 @@ def test_rest_catalog_context_manager_with_exception_sigv4(self, rest_mock: Mock assert catalog is not None and hasattr(catalog, "_session") assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS_SIGV4 - def test_scan_planning_disabled_by_default(self, rest_mock: Mocker) -> None: + def test_rest_scan_planning_disabled_by_default(self, rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}}, @@ -2002,9 +2002,9 @@ def test_scan_planning_disabled_by_default(self, rest_mock: Mocker) -> None: ) catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - assert catalog.is_scan_planning_enabled() is False + assert catalog.is_rest_scan_planning_enabled() is False - def test_scan_planning_enabled_by_property(self, rest_mock: Mocker) -> None: + def test_rest_scan_planning_enabled_by_property(self, rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}}, @@ -2017,9 +2017,9 @@ def test_scan_planning_enabled_by_property(self, rest_mock: Mocker) -> None: **{"rest-scan-planning-enabled": "true"}, ) - assert catalog.is_scan_planning_enabled() is True + assert catalog.is_rest_scan_planning_enabled() is True - def test_scan_planning_explicitly_disabled(self, rest_mock: Mocker) -> None: + def test_rest_scan_planning_explicitly_disabled(self, rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/config", json={"defaults": {}, "overrides": {}}, @@ -2029,12 +2029,12 @@ def test_scan_planning_explicitly_disabled(self, rest_mock: Mocker) -> None: "rest", uri=TEST_URI, token=TEST_TOKEN, - **{"rest.scan-planning.enabled": "false"}, + **{"rest-scan-planning-enabled": "false"}, ) - assert catalog.is_scan_planning_enabled() is False + assert catalog.is_rest_scan_planning_enabled() is False - def test_scan_planning_enabled_from_server_config(self, rest_mock: Mocker) -> None: + def test_rest_scan_planning_enabled_from_server_config(self, rest_mock: Mocker) -> None: rest_mock.get( f"{TEST_URI}v1/config", json={"defaults": {"rest-scan-planning-enabled": "true"}, "overrides": {}}, @@ -2042,4 +2042,4 @@ def test_scan_planning_enabled_from_server_config(self, rest_mock: Mocker) -> No ) catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - assert catalog.is_scan_planning_enabled() is True + assert catalog.is_rest_scan_planning_enabled() is True