Skip to content

Commit 14a971b

Browse files
committed
REST: Add support for unregister_table
1 parent 154288f commit 14a971b

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

pyiceberg/catalog/rest/__init__.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class Endpoints:
150150
load_credentials: str = "namespaces/{namespace}/tables/{table}/credentials"
151151
update_table: str = "namespaces/{namespace}/tables/{table}"
152152
drop_table: str = "namespaces/{namespace}/tables/{table}"
153+
unregister_table: str = "namespaces/{namespace}/tables/{table}/unregister"
153154
table_exists: str = "namespaces/{namespace}/tables/{table}"
154155
get_token: str = "oauth/tokens"
155156
rename_table: str = "tables/rename"
@@ -182,6 +183,7 @@ class Capability:
182183
V1_DELETE_TABLE = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_table}")
183184
V1_RENAME_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.rename_table}")
184185
V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")
186+
V1_UNREGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.unregister_table}")
185187
V1_LOAD_CREDENTIALS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_credentials}")
186188

187189
V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
@@ -338,6 +340,16 @@ class RegisterTableRequest(IcebergBaseModel):
338340
overwrite: bool
339341

340342

343+
class UnregisterTableResult(IcebergBaseModel):
344+
"""Result of unregistering a table.
345+
346+
Contains the last metadata location and table metadata at the time of unregistration.
347+
"""
348+
349+
metadata_location: str = Field(..., alias="metadata-location")
350+
metadata: TableMetadata
351+
352+
341353
class RegisterViewRequest(IcebergBaseModel):
342354
name: str
343355
metadata_location: str = Field(..., alias="metadata-location")
@@ -1042,6 +1054,32 @@ def register_table(self, identifier: str | Identifier, metadata_location: str, o
10421054
table_response = TableResponse.model_validate_json(response.text)
10431055
return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
10441056

1057+
@retry(**_RETRY_ARGS)
1058+
def unregister_table(self, identifier: str | Identifier) -> tuple[str, TableMetadata]:
1059+
"""Unregister a table from the catalog without removing data or metadata files.
1060+
1061+
Args:
1062+
identifier (Union[str, Identifier]): Table identifier for the table
1063+
1064+
Returns:
1065+
tuple[str, TableMetadata]: The last metadata location and corresponding table metadata
1066+
1067+
Raises:
1068+
NoSuchTableError: If the table does not exist
1069+
"""
1070+
self._check_endpoint(Capability.V1_UNREGISTER_TABLE)
1071+
namespace_and_table = self._split_identifier_for_path(identifier)
1072+
response = self._session.post(
1073+
self.url(Endpoints.unregister_table, prefixed=True, **namespace_and_table),
1074+
)
1075+
try:
1076+
response.raise_for_status()
1077+
except HTTPError as exc:
1078+
_handle_non_200_response(exc, {404: NoSuchTableError})
1079+
1080+
result = UnregisterTableResult.model_validate_json(response.content)
1081+
return (result.metadata_location, result.metadata)
1082+
10451083
@retry(**_RETRY_ARGS)
10461084
@override
10471085
def list_tables(self, namespace: str | Identifier) -> list[Identifier]:

tests/catalog/test_rest.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
Capability.V1_DELETE_TABLE,
105105
Capability.V1_RENAME_TABLE,
106106
Capability.V1_REGISTER_TABLE,
107+
Capability.V1_UNREGISTER_TABLE,
107108
Capability.V1_LOAD_CREDENTIALS,
108109
Capability.V1_LIST_VIEWS,
109110
Capability.V1_LOAD_VIEW,
@@ -1994,6 +1995,46 @@ def test_register_table_overwrite(
19941995
assert actual.name() == expected.name()
19951996

19961997

1998+
def test_unregister_table_200(
1999+
rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: dict[str, Any]
2000+
) -> None:
2001+
unregister_response = {
2002+
"metadata-location": "s3://warehouse/database/table/metadata.json",
2003+
"metadata": example_table_metadata_no_snapshot_v1_rest_json["metadata"],
2004+
}
2005+
rest_mock.post(
2006+
f"{TEST_URI}v1/namespaces/default/tables/my_table/unregister",
2007+
json=unregister_response,
2008+
status_code=200,
2009+
request_headers=TEST_HEADERS,
2010+
)
2011+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
2012+
metadata_location, metadata = catalog.unregister_table(identifier=("default", "my_table"))
2013+
2014+
assert metadata_location == "s3://warehouse/database/table/metadata.json"
2015+
assert metadata.model_dump() == TableMetadataV1(**example_table_metadata_no_snapshot_v1_rest_json["metadata"]).model_dump()
2016+
2017+
2018+
def test_unregister_table_404(rest_mock: Mocker) -> None:
2019+
rest_mock.post(
2020+
f"{TEST_URI}v1/namespaces/default/tables/does_not_exist/unregister",
2021+
json={
2022+
"error": {
2023+
"message": "Table does not exist: default.does_not_exist",
2024+
"type": "NoSuchTableException",
2025+
"code": 404,
2026+
}
2027+
},
2028+
status_code=404,
2029+
request_headers=TEST_HEADERS,
2030+
)
2031+
2032+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
2033+
with pytest.raises(NoSuchTableError) as e:
2034+
catalog.unregister_table(identifier=("default", "does_not_exist"))
2035+
assert "Table does not exist" in str(e.value)
2036+
2037+
19972038
def test_delete_namespace_204(rest_mock: Mocker) -> None:
19982039
namespace = "example"
19992040
rest_mock.delete(

0 commit comments

Comments
 (0)