From ed4396b7a68888f1a63b79406b64c911fbfc1f1e Mon Sep 17 00:00:00 2001 From: Stanley Law <19900516+dingo4dev@users.noreply.github.com> Date: Fri, 29 Aug 2025 04:59:39 +0000 Subject: [PATCH] Add close method to RestCatalog and corresponding tests --- pyiceberg/catalog/rest/__init__.py | 7 +++ tests/catalog/test_rest.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index afbaa7db07..207b3c4ce2 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -876,3 +876,10 @@ def drop_view(self, identifier: Union[str]) -> None: response.raise_for_status() except HTTPError as exc: _handle_non_200_response(exc, {404: NoSuchViewError}) + + def close(self) -> None: + """Close the catalog and release Session connection adapters. + + This method closes mounted HttpAdapters' pooled connections and any active Proxy pooled connections. + """ + self._session.close() diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 261910575f..223c6d2f9e 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -1845,3 +1845,76 @@ def test_rest_catalog_with_google_credentials_path( assert len(history) == 1 actual_headers = history[0].headers assert actual_headers["Authorization"] == expected_auth_header + + +class TestRestCatalogClose: + """Tests RestCatalog close functionality""" + + EXPECTED_ADAPTERS = 2 + EXPECTED_ADAPTERS_SIGV4 = 3 + + def test_catalog_close(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) + catalog.close() + # Verify session still exists after close the session pooled connections + assert hasattr(catalog, "_session") + assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS + # Second close should not raise any exception + catalog.close() + + def test_rest_catalog_close_sigv4(self, rest_mock: Mocker) -> None: + catalog = 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.sigv4-enabled": "true"}) + catalog.close() + assert hasattr(catalog, "_session") + assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS_SIGV4 + + def test_rest_catalog_context_manager_with_exception(self, rest_mock: Mocker) -> None: + """Test RestCatalog context manager properly closes with exceptions.""" + catalog = None + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + + try: + with RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) as cat: + catalog = cat + raise ValueError("Test exception") + except ValueError: + pass + + assert catalog is not None and hasattr(catalog, "_session") + assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS + + def test_rest_catalog_context_manager_with_exception_sigv4(self, rest_mock: Mocker) -> None: + """Test RestCatalog context manager properly closes with exceptions.""" + catalog = None + rest_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + + try: + with RestCatalog("rest", **{"uri": TEST_URI, "token": TEST_TOKEN, "rest.sigv4-enabled": "true"}) as cat: + catalog = cat + raise ValueError("Test exception") + except ValueError: + pass + + assert catalog is not None and hasattr(catalog, "_session") + assert len(catalog._session.adapters) == self.EXPECTED_ADAPTERS_SIGV4