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",