Skip to content

Commit 29c9a96

Browse files
shiv-ioshivonchain
andauthored
Add view_exists method to REST Catalog (#1242)
Part of the adding view support to the REST catalog: #818 Todo: - [x] Add tests - [x] Add docs Please let me know what the appropriate place to add docs would be --------- Co-authored-by: Shiv Gupta <[email protected]>
1 parent 4fbcd6e commit 29c9a96

File tree

11 files changed

+121
-0
lines changed

11 files changed

+121
-0
lines changed

mkdocs/docs/api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,19 @@ with table.manage_snapshots() as ms:
12581258
ms.create_branch(snapshot_id1, "Branch_A").create_tag(snapshot_id2, "tag789")
12591259
```
12601260

1261+
## Views
1262+
1263+
PyIceberg supports view operations.
1264+
1265+
### Check if a view exists
1266+
1267+
```python
1268+
from pyiceberg.catalog import load_catalog
1269+
1270+
catalog = load_catalog("default")
1271+
catalog.view_exists("default.bar")
1272+
```
1273+
12611274
## Table Statistics Management
12621275

12631276
Manage table statistics with operations through the `Table` API:

pyiceberg/catalog/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,17 @@ def table_exists(self, identifier: Union[str, Identifier]) -> bool:
438438
bool: True if the table exists, False otherwise.
439439
"""
440440

441+
@abstractmethod
442+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
443+
"""Check if a view exists.
444+
445+
Args:
446+
identifier (str | Identifier): View identifier.
447+
448+
Returns:
449+
bool: True if the view exists, False otherwise.
450+
"""
451+
441452
@abstractmethod
442453
def register_table(self, identifier: Union[str, Identifier], metadata_location: str) -> Table:
443454
"""Register a new table using existing metadata.

pyiceberg/catalog/dynamodb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,9 @@ def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
529529
def drop_view(self, identifier: Union[str, Identifier]) -> None:
530530
raise NotImplementedError
531531

532+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
533+
raise NotImplementedError
534+
532535
def _get_iceberg_table_item(self, database_name: str, table_name: str) -> Dict[str, Any]:
533536
try:
534537
return self._get_dynamo_item(identifier=f"{database_name}.{table_name}", namespace=database_name)

pyiceberg/catalog/glue.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,9 @@ def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
788788
def drop_view(self, identifier: Union[str, Identifier]) -> None:
789789
raise NotImplementedError
790790

791+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
792+
raise NotImplementedError
793+
791794
@staticmethod
792795
def __is_iceberg_table(table: TableTypeDef) -> bool:
793796
return table.get("Parameters", {}).get(TABLE_TYPE, "").lower() == ICEBERG

pyiceberg/catalog/hive.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,9 @@ def register_table(self, identifier: Union[str, Identifier], metadata_location:
409409
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
410410
raise NotImplementedError
411411

412+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
413+
raise NotImplementedError
414+
412415
def _create_lock_request(self, database_name: str, table_name: str) -> LockRequest:
413416
lock_component: LockComponent = LockComponent(
414417
level=LockLevel.TABLE, type=LockType.EXCLUSIVE, dbname=database_name, tablename=table_name, isTransactional=True

pyiceberg/catalog/noop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,8 @@ def update_namespace_properties(
123123
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
124124
raise NotImplementedError
125125

126+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
127+
raise NotImplementedError
128+
126129
def drop_view(self, identifier: Union[str, Identifier]) -> None:
127130
raise NotImplementedError

pyiceberg/catalog/rest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class Endpoints:
106106
rename_table: str = "tables/rename"
107107
list_views: str = "namespaces/{namespace}/views"
108108
drop_view: str = "namespaces/{namespace}/views/{view}"
109+
view_exists: str = "namespaces/{namespace}/views/{view}"
109110

110111

111112
class IdentifierKind(Enum):
@@ -906,6 +907,31 @@ def table_exists(self, identifier: Union[str, Identifier]) -> bool:
906907

907908
return False
908909

910+
@retry(**_RETRY_ARGS)
911+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
912+
"""Check if a view exists.
913+
914+
Args:
915+
identifier (str | Identifier): View identifier.
916+
917+
Returns:
918+
bool: True if the view exists, False otherwise.
919+
"""
920+
response = self._session.head(
921+
self.url(Endpoints.view_exists, prefixed=True, **self._split_identifier_for_path(identifier, IdentifierKind.VIEW)),
922+
)
923+
if response.status_code == 404:
924+
return False
925+
elif response.status_code in [200, 204]:
926+
return True
927+
928+
try:
929+
response.raise_for_status()
930+
except HTTPError as exc:
931+
self._handle_non_200_response(exc, {})
932+
933+
return False
934+
909935
@retry(**_RETRY_ARGS)
910936
def drop_view(self, identifier: Union[str]) -> None:
911937
response = self._session.delete(

pyiceberg/catalog/sql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,5 +702,8 @@ def update_namespace_properties(
702702
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
703703
raise NotImplementedError
704704

705+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
706+
raise NotImplementedError
707+
705708
def drop_view(self, identifier: Union[str, Identifier]) -> None:
706709
raise NotImplementedError

tests/catalog/test_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ def list_views(self, namespace: Optional[Union[str, Identifier]] = None) -> List
264264
def drop_view(self, identifier: Union[str, Identifier]) -> None:
265265
raise NotImplementedError
266266

267+
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
268+
raise NotImplementedError
269+
267270

268271
@pytest.fixture
269272
def catalog(tmp_path: PosixPath) -> InMemoryCatalog:

tests/catalog/test_rest.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,42 @@ def test_list_views_404(rest_mock: Mocker) -> None:
492492
assert "Namespace does not exist" in str(e.value)
493493

494494

495+
def test_view_exists_204(rest_mock: Mocker) -> None:
496+
namespace = "examples"
497+
view = "some_view"
498+
rest_mock.head(
499+
f"{TEST_URI}v1/namespaces/{namespace}/views/{view}",
500+
status_code=204,
501+
request_headers=TEST_HEADERS,
502+
)
503+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
504+
assert catalog.view_exists((namespace, view))
505+
506+
507+
def test_view_exists_404(rest_mock: Mocker) -> None:
508+
namespace = "examples"
509+
view = "some_view"
510+
rest_mock.head(
511+
f"{TEST_URI}v1/namespaces/{namespace}/views/{view}",
512+
status_code=404,
513+
request_headers=TEST_HEADERS,
514+
)
515+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
516+
assert not catalog.view_exists((namespace, view))
517+
518+
519+
def test_view_exists_multilevel_namespace_404(rest_mock: Mocker) -> None:
520+
multilevel_namespace = "core.examples.some_namespace"
521+
view = "some_view"
522+
rest_mock.head(
523+
f"{TEST_URI}v1/namespaces/{multilevel_namespace}/views/{view}",
524+
status_code=404,
525+
request_headers=TEST_HEADERS,
526+
)
527+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
528+
assert not catalog.view_exists((multilevel_namespace, view))
529+
530+
495531
def test_list_namespaces_200(rest_mock: Mocker) -> None:
496532
rest_mock.get(
497533
f"{TEST_URI}v1/namespaces",

0 commit comments

Comments
 (0)