Add `close()` method to RestCatalog (#2403)
<!--
Thanks for opening a pull request!
-->
<!-- In the case this PR will resolve an issue, please replace
${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
Part of #2399
# Rationale for this change
This PR introducs the implementation of `close()` method to the
`RestCatalog`.
And add corresponding test case which following the test pattern in
#2390
## Are these changes tested?
Yes
## Are there any user-facing changes?
<!-- In the case of user-facing changes, please add the changelog label.
-->diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py
index afbaa7d..207b3c4 100644
--- a/pyiceberg/catalog/rest/__init__.py
+++ b/pyiceberg/catalog/rest/__init__.py
@@ -876,3 +876,10 @@
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 2619105..223c6d2 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -1845,3 +1845,76 @@
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