This is an automated email from the ASF dual-hosted git repository.

sungwy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 86728e22 Add list_views to rest catalog (#817)
86728e22 is described below

commit 86728e223ad1f4704a3cc464959f40307f8ef114
Author: Andre Luis Anastacio <[email protected]>
AuthorDate: Thu Aug 29 10:34:22 2024 -0300

    Add list_views to rest catalog (#817)
    
    * Add list_views to rest catalog
    
    * Update pyiceberg/catalog/__init__.py
    
    Co-authored-by: Sung Yun <[email protected]>
    
    ---------
    
    Co-authored-by: Sung Yun <[email protected]>
---
 pyiceberg/catalog/__init__.py | 16 +++++++++++++++
 pyiceberg/catalog/dynamodb.py |  3 +++
 pyiceberg/catalog/glue.py     |  3 +++
 pyiceberg/catalog/hive.py     |  3 +++
 pyiceberg/catalog/noop.py     |  3 +++
 pyiceberg/catalog/rest.py     | 21 ++++++++++++++++++++
 pyiceberg/catalog/sql.py      |  3 +++
 tests/catalog/test_base.py    |  3 +++
 tests/catalog/test_rest.py    | 46 +++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 101 insertions(+)

diff --git a/pyiceberg/catalog/__init__.py b/pyiceberg/catalog/__init__.py
index 7b15a60c..71ed9114 100644
--- a/pyiceberg/catalog/__init__.py
+++ b/pyiceberg/catalog/__init__.py
@@ -583,6 +583,22 @@ class Catalog(ABC):
             NoSuchNamespaceError: If a namespace with the given name does not 
exist.
         """
 
+    @abstractmethod
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        """List views under the given namespace in the catalog.
+
+        If namespace is not provided, lists all views in the catalog.
+
+        Args:
+            namespace (str | Identifier): Namespace identifier to search.
+
+        Returns:
+            List[Identifier]: list of table identifiers.
+
+        Raises:
+            NoSuchNamespaceError: If a namespace with the given name does not 
exist.
+        """
+
     @abstractmethod
     def load_namespace_properties(self, namespace: Union[str, Identifier]) -> 
Properties:
         """Get properties for a namespace.
diff --git a/pyiceberg/catalog/dynamodb.py b/pyiceberg/catalog/dynamodb.py
index f0ef7c6b..07d9d693 100644
--- a/pyiceberg/catalog/dynamodb.py
+++ b/pyiceberg/catalog/dynamodb.py
@@ -528,6 +528,9 @@ class DynamoDbCatalog(MetastoreCatalog):
 
         return properties_update_summary
 
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        raise NotImplementedError
+
     def _get_iceberg_table_item(self, database_name: str, table_name: str) -> 
Dict[str, Any]:
         try:
             return 
self._get_dynamo_item(identifier=f"{database_name}.{table_name}", 
namespace=database_name)
diff --git a/pyiceberg/catalog/glue.py b/pyiceberg/catalog/glue.py
index 148b02f9..2c3082b7 100644
--- a/pyiceberg/catalog/glue.py
+++ b/pyiceberg/catalog/glue.py
@@ -769,3 +769,6 @@ class GlueCatalog(MetastoreCatalog):
         self.glue.update_database(Name=database_name, 
DatabaseInput=_construct_database_input(database_name, updated_properties))
 
         return properties_update_summary
+
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        raise NotImplementedError
diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py
index 755cd34c..096cfc14 100644
--- a/pyiceberg/catalog/hive.py
+++ b/pyiceberg/catalog/hive.py
@@ -389,6 +389,9 @@ class HiveCatalog(MetastoreCatalog):
         """
         raise NotImplementedError
 
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        raise NotImplementedError
+
     def _create_lock_request(self, database_name: str, table_name: str) -> 
LockRequest:
         lock_component: LockComponent = LockComponent(
             level=LockLevel.TABLE, type=LockType.EXCLUSIVE, 
dbname=database_name, tablename=table_name, isTransactional=True
diff --git a/pyiceberg/catalog/noop.py b/pyiceberg/catalog/noop.py
index 1dfeb952..eaa5e289 100644
--- a/pyiceberg/catalog/noop.py
+++ b/pyiceberg/catalog/noop.py
@@ -113,3 +113,6 @@ class NoopCatalog(Catalog):
         self, namespace: Union[str, Identifier], removals: Optional[Set[str]] 
= None, updates: Properties = EMPTY_DICT
     ) -> PropertiesUpdateSummary:
         raise NotImplementedError
+
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        raise NotImplementedError
diff --git a/pyiceberg/catalog/rest.py b/pyiceberg/catalog/rest.py
index d2acc701..a7a19d10 100644
--- a/pyiceberg/catalog/rest.py
+++ b/pyiceberg/catalog/rest.py
@@ -96,6 +96,7 @@ class Endpoints:
     table_exists: str = "namespaces/{namespace}/tables/{table}"
     get_token: str = "oauth/tokens"
     rename_table: str = "tables/rename"
+    list_views: str = "namespaces/{namespace}/views"
 
 
 AUTHORIZATION_HEADER = "Authorization"
@@ -200,10 +201,19 @@ class ListTableResponseEntry(IcebergBaseModel):
     namespace: Identifier = Field()
 
 
+class ListViewResponseEntry(IcebergBaseModel):
+    name: str = Field()
+    namespace: Identifier = Field()
+
+
 class ListTablesResponse(IcebergBaseModel):
     identifiers: List[ListTableResponseEntry] = Field()
 
 
+class ListViewsResponse(IcebergBaseModel):
+    identifiers: List[ListViewResponseEntry] = Field()
+
+
 class ErrorResponseMessage(IcebergBaseModel):
     message: str = Field()
     type: str = Field()
@@ -719,6 +729,17 @@ class RestCatalog(Catalog):
             )
         return table_request
 
+    @retry(**_RETRY_ARGS)
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        namespace_tuple = self._check_valid_namespace_identifier(namespace)
+        namespace_concat = NAMESPACE_SEPARATOR.join(namespace_tuple)
+        response = self._session.get(self.url(Endpoints.list_views, 
namespace=namespace_concat))
+        try:
+            response.raise_for_status()
+        except HTTPError as exc:
+            self._handle_non_200_response(exc, {404: NoSuchNamespaceError})
+        return [(*view.namespace, view.name) for view in 
ListViewsResponse(**response.json()).identifiers]
+
     @retry(**_RETRY_ARGS)
     def _commit_table(self, table_request: CommitTableRequest) -> 
CommitTableResponse:
         """Update the table.
diff --git a/pyiceberg/catalog/sql.py b/pyiceberg/catalog/sql.py
index 229124ad..5b9e697e 100644
--- a/pyiceberg/catalog/sql.py
+++ b/pyiceberg/catalog/sql.py
@@ -696,3 +696,6 @@ class SqlCatalog(MetastoreCatalog):
                 session.execute(insert_stmt)
             session.commit()
         return properties_update_summary
+
+    def list_views(self, namespace: Union[str, Identifier]) -> 
List[Identifier]:
+        raise NotImplementedError
diff --git a/tests/catalog/test_base.py b/tests/catalog/test_base.py
index af5c67a9..095d9346 100644
--- a/tests/catalog/test_base.py
+++ b/tests/catalog/test_base.py
@@ -256,6 +256,9 @@ class InMemoryCatalog(MetastoreCatalog):
             removed=list(removed or []), updated=list(updates.keys() if 
updates else []), missing=list(expected_to_change)
         )
 
+    def list_views(self, namespace: Optional[Union[str, Identifier]] = None) 
-> List[Identifier]:
+        raise NotImplementedError
+
 
 @pytest.fixture
 def catalog(tmp_path: PosixPath) -> InMemoryCatalog:
diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py
index bb609b41..470f60c2 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -402,6 +402,52 @@ def test_list_tables_404(rest_mock: Mocker) -> None:
     assert "Namespace does not exist" in str(e.value)
 
 
+def test_list_views_200(rest_mock: Mocker) -> None:
+    namespace = "examples"
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces/{namespace}/views",
+        json={"identifiers": [{"namespace": ["examples"], "name": 
"fooshare"}]},
+        status_code=200,
+        request_headers=TEST_HEADERS,
+    )
+
+    assert RestCatalog("rest", uri=TEST_URI, 
token=TEST_TOKEN).list_views(namespace) == [("examples", "fooshare")]
+
+
+def test_list_views_200_sigv4(rest_mock: Mocker) -> None:
+    namespace = "examples"
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces/{namespace}/views",
+        json={"identifiers": [{"namespace": ["examples"], "name": 
"fooshare"}]},
+        status_code=200,
+        request_headers=TEST_HEADERS,
+    )
+
+    assert RestCatalog("rest", **{"uri": TEST_URI, "token": TEST_TOKEN, 
"rest.sigv4-enabled": "true"}).list_views(namespace) == [
+        ("examples", "fooshare")
+    ]
+    assert rest_mock.called
+
+
+def test_list_views_404(rest_mock: Mocker) -> None:
+    namespace = "examples"
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces/{namespace}/views",
+        json={
+            "error": {
+                "message": "Namespace does not exist: personal in warehouse 
8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
+                "type": "NoSuchNamespaceException",
+                "code": 404,
+            }
+        },
+        status_code=404,
+        request_headers=TEST_HEADERS,
+    )
+    with pytest.raises(NoSuchNamespaceError) as e:
+        RestCatalog("rest", uri=TEST_URI, 
token=TEST_TOKEN).list_views(namespace)
+    assert "Namespace does not exist" in str(e.value)
+
+
 def test_list_namespaces_200(rest_mock: Mocker) -> None:
     rest_mock.get(
         f"{TEST_URI}v1/namespaces",

Reply via email to