Skip to content

Commit 4177f88

Browse files
committed
REST: Add support for unregister_table
1 parent 68898e5 commit 4177f88

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
@@ -157,6 +157,7 @@ class Endpoints:
157157
load_credentials: str = "namespaces/{namespace}/tables/{table}/credentials"
158158
update_table: str = "namespaces/{namespace}/tables/{table}"
159159
drop_table: str = "namespaces/{namespace}/tables/{table}"
160+
unregister_table: str = "namespaces/{namespace}/tables/{table}/unregister"
160161
table_exists: str = "namespaces/{namespace}/tables/{table}"
161162
get_token: str = "oauth/tokens"
162163
rename_table: str = "tables/rename"
@@ -192,6 +193,7 @@ class Capability:
192193
V1_DELETE_TABLE = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_table}")
193194
V1_RENAME_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.rename_table}")
194195
V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")
196+
V1_UNREGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.unregister_table}")
195197
V1_LOAD_CREDENTIALS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_credentials}")
196198

197199
V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
@@ -378,6 +380,16 @@ class RegisterTableRequest(IcebergBaseModel):
378380
overwrite: bool
379381

380382

383+
class UnregisterTableResult(IcebergBaseModel):
384+
"""Result of unregistering a table.
385+
386+
Contains the last metadata location and table metadata at the time of unregistration.
387+
"""
388+
389+
metadata_location: str = Field(..., alias="metadata-location")
390+
metadata: TableMetadata
391+
392+
381393
class RegisterViewRequest(IcebergBaseModel):
382394
name: str
383395
metadata_location: str = Field(..., alias="metadata-location")
@@ -1249,6 +1261,32 @@ def register_table(self, identifier: str | Identifier, metadata_location: str, o
12491261
table_response = TableResponse.model_validate_json(response.text)
12501262
return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
12511263

1264+
@retry(**_RETRY_ARGS)
1265+
def unregister_table(self, identifier: str | Identifier) -> tuple[str, TableMetadata]:
1266+
"""Unregister a table from the catalog without removing data or metadata files.
1267+
1268+
Args:
1269+
identifier (Union[str, Identifier]): Table identifier for the table
1270+
1271+
Returns:
1272+
tuple[str, TableMetadata]: The last metadata location and corresponding table metadata
1273+
1274+
Raises:
1275+
NoSuchTableError: If the table does not exist
1276+
"""
1277+
self._check_endpoint(Capability.V1_UNREGISTER_TABLE)
1278+
namespace_and_table = self._split_identifier_for_path(identifier)
1279+
response = self._session.post(
1280+
self.url(Endpoints.unregister_table, prefixed=True, **namespace_and_table),
1281+
)
1282+
try:
1283+
response.raise_for_status()
1284+
except HTTPError as exc:
1285+
_handle_non_200_response(exc, {404: NoSuchTableError})
1286+
1287+
result = UnregisterTableResult.model_validate_json(response.content)
1288+
return (result.metadata_location, result.metadata)
1289+
12521290
@retry(**_RETRY_ARGS)
12531291
@override
12541292
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)