Skip to content

Commit 1e2e7a6

Browse files
committed
REST: Add support for unregister_table
1 parent d99e463 commit 1e2e7a6

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
@@ -149,6 +149,7 @@ class Endpoints:
149149
load_table: str = "namespaces/{namespace}/tables/{table}"
150150
update_table: str = "namespaces/{namespace}/tables/{table}"
151151
drop_table: str = "namespaces/{namespace}/tables/{table}"
152+
unregister_table: str = "namespaces/{namespace}/tables/{table}/unregister"
152153
table_exists: str = "namespaces/{namespace}/tables/{table}"
153154
get_token: str = "oauth/tokens"
154155
rename_table: str = "tables/rename"
@@ -181,6 +182,7 @@ class Capability:
181182
V1_DELETE_TABLE = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_table}")
182183
V1_RENAME_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.rename_table}")
183184
V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")
185+
V1_UNREGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.unregister_table}")
184186

185187
V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
186188
V1_LOAD_VIEW = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_view}")
@@ -332,6 +334,16 @@ class RegisterTableRequest(IcebergBaseModel):
332334
overwrite: bool
333335

334336

337+
class UnregisterTableResult(IcebergBaseModel):
338+
"""Result of unregistering a table.
339+
340+
Contains the last metadata location and table metadata at the time of unregistration.
341+
"""
342+
343+
metadata_location: str = Field(..., alias="metadata-location")
344+
metadata: TableMetadata
345+
346+
335347
class RegisterViewRequest(IcebergBaseModel):
336348
name: str
337349
metadata_location: str = Field(..., alias="metadata-location")
@@ -1036,6 +1048,32 @@ def register_table(self, identifier: str | Identifier, metadata_location: str, o
10361048
table_response = TableResponse.model_validate_json(response.text)
10371049
return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
10381050

1051+
@retry(**_RETRY_ARGS)
1052+
def unregister_table(self, identifier: str | Identifier) -> tuple[str, TableMetadata]:
1053+
"""Unregister a table from the catalog without removing data or metadata files.
1054+
1055+
Args:
1056+
identifier (Union[str, Identifier]): Table identifier for the table
1057+
1058+
Returns:
1059+
tuple[str, TableMetadata]: The last metadata location and corresponding table metadata
1060+
1061+
Raises:
1062+
NoSuchTableError: If the table does not exist
1063+
"""
1064+
self._check_endpoint(Capability.V1_UNREGISTER_TABLE)
1065+
namespace_and_table = self._split_identifier_for_path(identifier)
1066+
response = self._session.post(
1067+
self.url(Endpoints.unregister_table, prefixed=True, **namespace_and_table),
1068+
)
1069+
try:
1070+
response.raise_for_status()
1071+
except HTTPError as exc:
1072+
_handle_non_200_response(exc, {404: NoSuchTableError})
1073+
1074+
result = UnregisterTableResult.model_validate_json(response.content)
1075+
return (result.metadata_location, result.metadata)
1076+
10391077
@retry(**_RETRY_ARGS)
10401078
@override
10411079
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_LIST_VIEWS,
108109
Capability.V1_LOAD_VIEW,
109110
Capability.V1_VIEW_EXISTS,
@@ -1997,6 +1998,46 @@ def test_register_table_overwrite(
19971998
assert actual.name() == expected.name()
19981999

19992000

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

0 commit comments

Comments
 (0)