Skip to content

Commit 9857107

Browse files
authored
Add drop_view to the rest catalog (#820)
* Use NoSuchIdentifier instead of NoTableIdentifier This change allows for the validation of both Tables and Views. * Add drop_view to the rest catalog * fixup! Add drop_view to the rest catalog
1 parent dc6d242 commit 9857107

File tree

10 files changed

+111
-9
lines changed

10 files changed

+111
-9
lines changed

pyiceberg/catalog/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,17 @@ def update_namespace_properties(
629629
ValueError: If removals and updates have overlapping keys.
630630
"""
631631

632+
@abstractmethod
633+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
634+
"""Drop a view.
635+
636+
Args:
637+
identifier (str | Identifier): View identifier.
638+
639+
Raises:
640+
NoSuchViewError: If a view with the given name does not exist.
641+
"""
642+
632643
@deprecated(
633644
deprecated_in="0.8.0",
634645
removed_in="0.9.0",

pyiceberg/catalog/dynamodb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ def update_namespace_properties(
531531
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
532532
raise NotImplementedError
533533

534+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
535+
raise NotImplementedError
536+
534537
def _get_iceberg_table_item(self, database_name: str, table_name: str) -> Dict[str, Any]:
535538
try:
536539
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
@@ -772,3 +772,6 @@ def update_namespace_properties(
772772

773773
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
774774
raise NotImplementedError
775+
776+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
777+
raise NotImplementedError

pyiceberg/catalog/hive.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,6 @@ def update_namespace_properties(
707707
expected_to_change = (removals or set()).difference(removed)
708708

709709
return PropertiesUpdateSummary(removed=list(removed or []), updated=list(updated or []), missing=list(expected_to_change))
710+
711+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
712+
raise NotImplementedError

pyiceberg/catalog/noop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,6 @@ def update_namespace_properties(
116116

117117
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
118118
raise NotImplementedError
119+
120+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
121+
raise NotImplementedError

pyiceberg/catalog/rest.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
from enum import Enum
1718
from json import JSONDecodeError
1819
from typing import (
1920
TYPE_CHECKING,
@@ -48,8 +49,10 @@
4849
ForbiddenError,
4950
NamespaceAlreadyExistsError,
5051
NamespaceNotEmptyError,
52+
NoSuchIdentifierError,
5153
NoSuchNamespaceError,
5254
NoSuchTableError,
55+
NoSuchViewError,
5356
OAuthError,
5457
RESTError,
5558
ServerError,
@@ -97,6 +100,12 @@ class Endpoints:
97100
get_token: str = "oauth/tokens"
98101
rename_table: str = "tables/rename"
99102
list_views: str = "namespaces/{namespace}/views"
103+
drop_view: str = "namespaces/{namespace}/views/{view}"
104+
105+
106+
class IdentifierKind(Enum):
107+
TABLE = "table"
108+
VIEW = "view"
100109

101110

102111
AUTHORIZATION_HEADER = "Authorization"
@@ -389,17 +398,20 @@ def _fetch_config(self) -> None:
389398
def _identifier_to_validated_tuple(self, identifier: Union[str, Identifier]) -> Identifier:
390399
identifier_tuple = self.identifier_to_tuple(identifier)
391400
if len(identifier_tuple) <= 1:
392-
raise NoSuchTableError(f"Missing namespace or invalid identifier: {'.'.join(identifier_tuple)}")
401+
raise NoSuchIdentifierError(f"Missing namespace or invalid identifier: {'.'.join(identifier_tuple)}")
393402
return identifier_tuple
394403

395-
def _split_identifier_for_path(self, identifier: Union[str, Identifier, TableIdentifier]) -> Properties:
404+
def _split_identifier_for_path(
405+
self, identifier: Union[str, Identifier, TableIdentifier], kind: IdentifierKind = IdentifierKind.TABLE
406+
) -> Properties:
396407
if isinstance(identifier, TableIdentifier):
397408
if identifier.namespace.root[0] == self.name:
398-
return {"namespace": NAMESPACE_SEPARATOR.join(identifier.namespace.root[1:]), "table": identifier.name}
409+
return {"namespace": NAMESPACE_SEPARATOR.join(identifier.namespace.root[1:]), kind.value: identifier.name}
399410
else:
400-
return {"namespace": NAMESPACE_SEPARATOR.join(identifier.namespace.root), "table": identifier.name}
411+
return {"namespace": NAMESPACE_SEPARATOR.join(identifier.namespace.root), kind.value: identifier.name}
401412
identifier_tuple = self._identifier_to_validated_tuple(identifier)
402-
return {"namespace": NAMESPACE_SEPARATOR.join(identifier_tuple[:-1]), "table": identifier_tuple[-1]}
413+
414+
return {"namespace": NAMESPACE_SEPARATOR.join(identifier_tuple[:-1]), kind.value: identifier_tuple[-1]}
403415

404416
def _split_identifier_for_json(self, identifier: Union[str, Identifier]) -> Dict[str, Union[Identifier, str]]:
405417
identifier_tuple = self._identifier_to_validated_tuple(identifier)
@@ -867,3 +879,16 @@ def table_exists(self, identifier: Union[str, Identifier]) -> bool:
867879
self._handle_non_200_response(exc, {})
868880

869881
return False
882+
883+
@retry(**_RETRY_ARGS)
884+
def drop_view(self, identifier: Union[str]) -> None:
885+
identifier_tuple = self.identifier_to_tuple_without_catalog(identifier)
886+
response = self._session.delete(
887+
self.url(
888+
Endpoints.drop_view, prefixed=True, **self._split_identifier_for_path(identifier_tuple, IdentifierKind.VIEW)
889+
),
890+
)
891+
try:
892+
response.raise_for_status()
893+
except HTTPError as exc:
894+
self._handle_non_200_response(exc, {404: NoSuchViewError})

pyiceberg/catalog/sql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,3 +699,6 @@ def update_namespace_properties(
699699

700700
def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
701701
raise NotImplementedError
702+
703+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
704+
raise NotImplementedError

pyiceberg/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class NoSuchIcebergTableError(NoSuchTableError):
4040
"""Raises when the table found in the REST catalog is not an iceberg table."""
4141

4242

43+
class NoSuchViewError(Exception):
44+
"""Raises when the view can't be found in the REST catalog."""
45+
46+
47+
class NoSuchIdentifierError(Exception):
48+
"""Raises when the identifier can't be found in the REST catalog."""
49+
50+
4351
class NoSuchNamespaceError(Exception):
4452
"""Raised when a referenced name-space is not found."""
4553

tests/catalog/test_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ def update_namespace_properties(
259259
def list_views(self, namespace: Optional[Union[str, Identifier]] = None) -> List[Identifier]:
260260
raise NotImplementedError
261261

262+
def drop_view(self, identifier: Union[str, Identifier]) -> None:
263+
raise NotImplementedError
264+
262265

263266
@pytest.fixture
264267
def catalog(tmp_path: PosixPath) -> InMemoryCatalog:

tests/catalog/test_rest.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
AuthorizationExpiredError,
3030
NamespaceAlreadyExistsError,
3131
NamespaceNotEmptyError,
32+
NoSuchIdentifierError,
3233
NoSuchNamespaceError,
3334
NoSuchTableError,
35+
NoSuchViewError,
3436
OAuthError,
3537
ServerError,
3638
TableAlreadyExistsError,
@@ -1158,31 +1160,31 @@ def test_delete_table_404(rest_mock: Mocker) -> None:
11581160

11591161
def test_create_table_missing_namespace(rest_mock: Mocker, table_schema_simple: Schema) -> None:
11601162
table = "table"
1161-
with pytest.raises(NoSuchTableError) as e:
1163+
with pytest.raises(NoSuchIdentifierError) as e:
11621164
# Missing namespace
11631165
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).create_table(table, table_schema_simple)
11641166
assert f"Missing namespace or invalid identifier: {table}" in str(e.value)
11651167

11661168

11671169
def test_load_table_invalid_namespace(rest_mock: Mocker) -> None:
11681170
table = "table"
1169-
with pytest.raises(NoSuchTableError) as e:
1171+
with pytest.raises(NoSuchIdentifierError) as e:
11701172
# Missing namespace
11711173
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_table(table)
11721174
assert f"Missing namespace or invalid identifier: {table}" in str(e.value)
11731175

11741176

11751177
def test_drop_table_invalid_namespace(rest_mock: Mocker) -> None:
11761178
table = "table"
1177-
with pytest.raises(NoSuchTableError) as e:
1179+
with pytest.raises(NoSuchIdentifierError) as e:
11781180
# Missing namespace
11791181
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).drop_table(table)
11801182
assert f"Missing namespace or invalid identifier: {table}" in str(e.value)
11811183

11821184

11831185
def test_purge_table_invalid_namespace(rest_mock: Mocker) -> None:
11841186
table = "table"
1185-
with pytest.raises(NoSuchTableError) as e:
1187+
with pytest.raises(NoSuchIdentifierError) as e:
11861188
# Missing namespace
11871189
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).purge_table(table)
11881190
assert f"Missing namespace or invalid identifier: {table}" in str(e.value)
@@ -1307,3 +1309,41 @@ def test_table_identifier_in_commit_table_request(rest_mock: Mocker, example_tab
13071309
rest_mock.last_request.text
13081310
== """{"identifier":{"namespace":["namespace"],"name":"table_name"},"requirements":[],"updates":[]}"""
13091311
)
1312+
1313+
1314+
def test_drop_view_invalid_namespace(rest_mock: Mocker) -> None:
1315+
view = "view"
1316+
with pytest.raises(NoSuchIdentifierError) as e:
1317+
# Missing namespace
1318+
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).drop_view(view)
1319+
1320+
assert f"Missing namespace or invalid identifier: {view}" in str(e.value)
1321+
1322+
1323+
def test_drop_view_404(rest_mock: Mocker) -> None:
1324+
rest_mock.delete(
1325+
f"{TEST_URI}v1/namespaces/some_namespace/views/does_not_exists",
1326+
json={
1327+
"error": {
1328+
"message": "The given view does not exist",
1329+
"type": "NoSuchViewException",
1330+
"code": 404,
1331+
}
1332+
},
1333+
status_code=404,
1334+
request_headers=TEST_HEADERS,
1335+
)
1336+
1337+
with pytest.raises(NoSuchViewError) as e:
1338+
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).drop_view(("some_namespace", "does_not_exists"))
1339+
assert "The given view does not exist" in str(e.value)
1340+
1341+
1342+
def test_drop_view_204(rest_mock: Mocker) -> None:
1343+
rest_mock.delete(
1344+
f"{TEST_URI}v1/namespaces/some_namespace/views/some_view",
1345+
json={},
1346+
status_code=204,
1347+
request_headers=TEST_HEADERS,
1348+
)
1349+
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).drop_view(("some_namespace", "some_view"))

0 commit comments

Comments
 (0)