Skip to content

Commit d59b5d8

Browse files
committed
Add support for registering views
1 parent cdb625f commit d59b5d8

10 files changed

Lines changed: 115 additions & 0 deletions

File tree

mkdocs/docs/api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,17 @@ catalog = load_catalog("default")
15191519
catalog.view_exists("default.bar")
15201520
```
15211521

1522+
## Register a view
1523+
1524+
To register a view using existing metadata:
1525+
1526+
```python
1527+
catalog.register_view(
1528+
identifier="docs_example.bids",
1529+
metadata_location="s3://warehouse/path/to/metadata.json"
1530+
)
1531+
```
1532+
15221533
## Table Statistics Management
15231534

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

pyiceberg/catalog/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,21 @@ def update_namespace_properties(
674674
ValueError: If removals and updates have overlapping keys.
675675
"""
676676

677+
@abstractmethod
678+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
679+
"""Register a new view using existing metadata.
680+
681+
Args:
682+
identifier (Union[str, Identifier]): View identifier for the view
683+
metadata_location (str): The location to the metadata
684+
685+
Returns:
686+
View: The newly registered view
687+
688+
Raises:
689+
ViewAlreadyExistsError: If the view already exists
690+
"""
691+
677692
@abstractmethod
678693
def drop_view(self, identifier: str | Identifier) -> None:
679694
"""Drop a view.

pyiceberg/catalog/bigquery_metastore.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from pyiceberg.table.update import TableRequirement, TableUpdate
4242
from pyiceberg.typedef import EMPTY_DICT, Identifier, Properties
4343
from pyiceberg.utils.config import Config
44+
from pyiceberg.view import View
4445

4546
if TYPE_CHECKING:
4647
import pyarrow as pa
@@ -304,6 +305,9 @@ def register_table(self, identifier: str | Identifier, metadata_location: str) -
304305
def list_views(self, namespace: str | Identifier) -> list[Identifier]:
305306
raise NotImplementedError
306307

308+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
309+
raise NotImplementedError
310+
307311
def drop_view(self, identifier: str | Identifier) -> None:
308312
raise NotImplementedError
309313

pyiceberg/catalog/dynamodb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,9 @@ def create_view(
552552
def list_views(self, namespace: str | Identifier) -> list[Identifier]:
553553
raise NotImplementedError
554554

555+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
556+
raise NotImplementedError
557+
555558
def drop_view(self, identifier: str | Identifier) -> None:
556559
raise NotImplementedError
557560

pyiceberg/catalog/glue.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,9 @@ def create_view(
966966
def list_views(self, namespace: str | Identifier) -> list[Identifier]:
967967
raise NotImplementedError
968968

969+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
970+
raise NotImplementedError
971+
969972
def drop_view(self, identifier: str | Identifier) -> None:
970973
raise NotImplementedError
971974

pyiceberg/catalog/hive.py

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

848848
return PropertiesUpdateSummary(removed=list(removed or []), updated=list(updated or []), missing=list(expected_to_change))
849849

850+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
851+
raise NotImplementedError
852+
850853
def drop_view(self, identifier: str | Identifier) -> None:
851854
raise NotImplementedError
852855

pyiceberg/catalog/noop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ def view_exists(self, identifier: str | Identifier) -> bool:
131131
def namespace_exists(self, namespace: str | Identifier) -> bool:
132132
raise NotImplementedError
133133

134+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
135+
raise NotImplementedError
136+
134137
def drop_view(self, identifier: str | Identifier) -> None:
135138
raise NotImplementedError
136139

pyiceberg/catalog/rest/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class Endpoints:
153153
rename_table: str = "tables/rename"
154154
list_views: str = "namespaces/{namespace}/views"
155155
create_view: str = "namespaces/{namespace}/views"
156+
register_view: str = "namespaces/{namespace}/register-view"
156157
drop_view: str = "namespaces/{namespace}/views/{view}"
157158
view_exists: str = "namespaces/{namespace}/views/{view}"
158159
plan_table_scan: str = "namespaces/{namespace}/tables/{table}/plan"
@@ -181,6 +182,7 @@ class Capability:
181182

182183
V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
183184
V1_VIEW_EXISTS = Endpoint(http_method=HttpMethod.HEAD, path=f"{API_PREFIX}/{Endpoints.view_exists}")
185+
V1_REGISTER_VIEW = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_view}")
184186
V1_DELETE_VIEW = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_view}")
185187
V1_SUBMIT_TABLE_SCAN_PLAN = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.plan_table_scan}")
186188
V1_TABLE_SCAN_PLAN_TASKS = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.fetch_scan_tasks}")
@@ -318,6 +320,11 @@ class RegisterTableRequest(IcebergBaseModel):
318320
metadata_location: str = Field(..., alias="metadata-location")
319321

320322

323+
class RegisterViewRequest(IcebergBaseModel):
324+
name: str
325+
metadata_location: str = Field(..., alias="metadata-location")
326+
327+
321328
class ConfigResponse(IcebergBaseModel):
322329
defaults: Properties | None = Field(default_factory=dict)
323330
overrides: Properties | None = Field(default_factory=dict)
@@ -1312,6 +1319,27 @@ def view_exists(self, identifier: str | Identifier) -> bool:
13121319

13131320
return False
13141321

1322+
@retry(**_RETRY_ARGS)
1323+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
1324+
self._check_endpoint(Capability.V1_REGISTER_VIEW)
1325+
namespace_and_view = self._split_identifier_for_path(identifier)
1326+
request = RegisterViewRequest(
1327+
name=self._identifier_to_validated_tuple(identifier)[-1],
1328+
metadata_location=metadata_location,
1329+
)
1330+
serialized_json = request.model_dump_json().encode(UTF8)
1331+
response = self._session.post(
1332+
self.url(Endpoints.register_view, namespace=namespace_and_view["namespace"]),
1333+
data=serialized_json,
1334+
)
1335+
try:
1336+
response.raise_for_status()
1337+
except HTTPError as exc:
1338+
_handle_non_200_response(exc, {409: ViewAlreadyExistsError})
1339+
1340+
view_response = ViewResponse.model_validate_json(response.text)
1341+
return self._response_to_view(self.identifier_to_tuple(identifier), view_response)
1342+
13151343
@retry(**_RETRY_ARGS)
13161344
def drop_view(self, identifier: str) -> None:
13171345
self._check_endpoint(Capability.V1_DELETE_VIEW)

pyiceberg/catalog/sql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,9 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
741741
def view_exists(self, identifier: str | Identifier) -> bool:
742742
raise NotImplementedError
743743

744+
def register_view(self, identifier: str | Identifier, metadata_location: str) -> View:
745+
raise NotImplementedError
746+
744747
def drop_view(self, identifier: str | Identifier) -> None:
745748
raise NotImplementedError
746749

tests/catalog/test_rest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
Capability.V1_REGISTER_TABLE,
105105
Capability.V1_LIST_VIEWS,
106106
Capability.V1_VIEW_EXISTS,
107+
Capability.V1_REGISTER_VIEW,
107108
Capability.V1_DELETE_VIEW,
108109
Capability.V1_SUBMIT_TABLE_SCAN_PLAN,
109110
Capability.V1_TABLE_SCAN_PLAN_TASKS,
@@ -2122,6 +2123,47 @@ def test_table_identifier_in_commit_table_request(
21222123
)
21232124

21242125

2126+
def test_register_view_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None:
2127+
rest_mock.post(
2128+
f"{TEST_URI}v1/namespaces/default/register-view",
2129+
json=example_view_metadata_rest_json,
2130+
status_code=200,
2131+
request_headers=TEST_HEADERS,
2132+
)
2133+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
2134+
actual = catalog.register_view(
2135+
identifier=("default", "registered_view"), metadata_location="s3://warehouse/database/view/metadata.json"
2136+
)
2137+
expected = View(
2138+
identifier=("default", "registered_view"),
2139+
metadata=ViewMetadata(**example_view_metadata_rest_json["metadata"]),
2140+
)
2141+
assert actual.metadata.model_dump() == expected.metadata.model_dump()
2142+
assert actual.name() == expected.name()
2143+
2144+
2145+
def test_register_view_409(rest_mock: Mocker) -> None:
2146+
rest_mock.post(
2147+
f"{TEST_URI}v1/namespaces/default/register-view",
2148+
json={
2149+
"error": {
2150+
"message": "View already exists: default.view in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
2151+
"type": "AlreadyExistsException",
2152+
"code": 409,
2153+
}
2154+
},
2155+
status_code=409,
2156+
request_headers=TEST_HEADERS,
2157+
)
2158+
2159+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
2160+
with pytest.raises(ViewAlreadyExistsError) as e:
2161+
catalog.register_view(
2162+
identifier=("default", "registered_view"), metadata_location="s3://warehouse/database/view/metadata.json"
2163+
)
2164+
assert "View already exists" in str(e.value)
2165+
2166+
21252167
def test_drop_view_invalid_namespace(rest_mock: Mocker) -> None:
21262168
view = "view"
21272169
with pytest.raises(NoSuchIdentifierError) as e:

0 commit comments

Comments
 (0)