Skip to content

Commit e61d488

Browse files
committed
feat: add load_view to REST catalog
1 parent 1a54e9c commit e61d488

File tree

9 files changed

+87
-0
lines changed

9 files changed

+87
-0
lines changed

pyiceberg/catalog/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,23 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
644644
NoSuchNamespaceError: If a namespace with the given name does not exist.
645645
"""
646646

647+
@abstractmethod
648+
def load_view(self, identifier: str | Identifier) -> View:
649+
"""Load the view's metadata and returns the view instance.
650+
651+
You can also use this method to check for view existence using 'try catalog.load_view() except NoSuchViewError'.
652+
Note: This method doesn't scan data stored in the view.
653+
654+
Args:
655+
identifier (str | Identifier): View identifier.
656+
657+
Returns:
658+
View: the view instance with its metadata.
659+
660+
Raises:
661+
NoSuchViewError: If a view with the name does not exist.
662+
"""
663+
647664
@abstractmethod
648665
def load_namespace_properties(self, namespace: str | Identifier) -> Properties:
649666
"""Get properties for a namespace.

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
@@ -310,6 +311,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
310311
def view_exists(self, identifier: str | Identifier) -> bool:
311312
raise NotImplementedError
312313

314+
def load_view(self, identifier: str | Identifier) -> View:
315+
raise NotImplementedError
316+
313317
def load_namespace_properties(self, namespace: str | Identifier) -> Properties:
314318
dataset_name = self.identifier_to_database(namespace)
315319

pyiceberg/catalog/dynamodb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
558558
def view_exists(self, identifier: str | Identifier) -> bool:
559559
raise NotImplementedError
560560

561+
def load_view(self, identifier: str | Identifier) -> View:
562+
raise NotImplementedError
563+
561564
def _get_iceberg_table_item(self, database_name: str, table_name: str) -> dict[str, Any]:
562565
try:
563566
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
@@ -972,6 +972,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
972972
def view_exists(self, identifier: str | Identifier) -> bool:
973973
raise NotImplementedError
974974

975+
def load_view(self, identifier: str | Identifier) -> View:
976+
raise NotImplementedError
977+
975978
@staticmethod
976979
def __is_iceberg_table(table: "TableTypeDef") -> bool:
977980
return table.get("Parameters", {}).get(TABLE_TYPE, "").lower() == ICEBERG

pyiceberg/catalog/hive.py

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

485+
def load_view(self, identifier: str | Identifier) -> View:
486+
raise NotImplementedError
487+
485488
def _create_lock_request(self, database_name: str, table_name: str) -> LockRequest:
486489
lock_component: LockComponent = LockComponent(
487490
level=LockLevel.TABLE, type=LockType.EXCLUSIVE, dbname=database_name, tablename=table_name, isTransactional=True

pyiceberg/catalog/noop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,6 @@ def create_view(
143143
properties: Properties = EMPTY_DICT,
144144
) -> View:
145145
raise NotImplementedError
146+
147+
def load_view(self, identifier: str | Identifier) -> View:
148+
raise NotImplementedError

pyiceberg/catalog/rest/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class Endpoints:
152152
get_token: str = "oauth/tokens"
153153
rename_table: str = "tables/rename"
154154
list_views: str = "namespaces/{namespace}/views"
155+
load_view: str = "namespaces/{namespace}/views/{view}"
155156
create_view: str = "namespaces/{namespace}/views"
156157
drop_view: str = "namespaces/{namespace}/views/{view}"
157158
view_exists: str = "namespaces/{namespace}/views/{view}"
@@ -180,6 +181,7 @@ class Capability:
180181
V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")
181182

182183
V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
184+
V1_LOAD_VIEW = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_view}")
183185
V1_VIEW_EXISTS = Endpoint(http_method=HttpMethod.HEAD, path=f"{API_PREFIX}/{Endpoints.view_exists}")
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}")
@@ -209,6 +211,7 @@ class Capability:
209211
VIEW_ENDPOINTS: frozenset[Endpoint] = frozenset(
210212
(
211213
Capability.V1_LIST_VIEWS,
214+
Capability.V1_LOAD_VIEW,
212215
Capability.V1_DELETE_VIEW,
213216
)
214217
)
@@ -1099,6 +1102,21 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
10991102
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
11001103
return [(*view.namespace, view.name) for view in ListViewsResponse.model_validate_json(response.text).identifiers]
11011104

1105+
@retry(**_RETRY_ARGS)
1106+
def load_view(self, identifier: str | Identifier) -> View:
1107+
self._check_endpoint(Capability.V1_LOAD_VIEW)
1108+
response = self._session.get(
1109+
self.url(Endpoints.load_view, prefixed=True, **self._split_identifier_for_path(identifier, IdentifierKind.VIEW)),
1110+
params={},
1111+
)
1112+
try:
1113+
response.raise_for_status()
1114+
except HTTPError as exc:
1115+
_handle_non_200_response(exc, {404: NoSuchViewError})
1116+
1117+
view_response = ViewResponse.model_validate_json(response.text)
1118+
return self._response_to_view(self.identifier_to_tuple(identifier), view_response)
1119+
11021120
@retry(**_RETRY_ARGS)
11031121
def commit_table(
11041122
self, table: Table, requirements: tuple[TableRequirement, ...], updates: tuple[TableUpdate, ...]

pyiceberg/catalog/sql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,9 @@ def view_exists(self, identifier: str | Identifier) -> bool:
744744
def drop_view(self, identifier: str | Identifier) -> None:
745745
raise NotImplementedError
746746

747+
def load_view(self, identifier: str | Identifier) -> View:
748+
raise NotImplementedError
749+
747750
def close(self) -> None:
748751
"""Close the catalog and release database connections.
749752

tests/catalog/test_rest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
Capability.V1_RENAME_TABLE,
104104
Capability.V1_REGISTER_TABLE,
105105
Capability.V1_LIST_VIEWS,
106+
Capability.V1_LOAD_VIEW,
106107
Capability.V1_VIEW_EXISTS,
107108
Capability.V1_DELETE_VIEW,
108109
Capability.V1_SUBMIT_TABLE_SCAN_PLAN,
@@ -1416,6 +1417,38 @@ def test_create_view_409(
14161417
assert "View already exists" in str(e.value)
14171418

14181419

1420+
def test_load_view_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None:
1421+
rest_mock.get(
1422+
f"{TEST_URI}v1/namespaces/fokko/views/view",
1423+
json=example_view_metadata_rest_json,
1424+
status_code=200,
1425+
request_headers=TEST_HEADERS,
1426+
)
1427+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
1428+
actual = catalog.load_view(("fokko", "view"))
1429+
expected = View(identifier=("fokko", "view"), metadata=ViewMetadata(**example_view_metadata_rest_json["metadata"]))
1430+
assert actual == expected
1431+
1432+
1433+
def test_load_view_404(rest_mock: Mocker) -> None:
1434+
rest_mock.get(
1435+
f"{TEST_URI}v1/namespaces/fokko/views/non_existent_view",
1436+
json={
1437+
"error": {
1438+
"message": "View does not exist: examples.non_existent_view in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
1439+
"type": "NoSuchNamespaceErrorException",
1440+
"code": 404,
1441+
}
1442+
},
1443+
status_code=404,
1444+
request_headers=TEST_HEADERS,
1445+
)
1446+
1447+
with pytest.raises(NoSuchViewError) as e:
1448+
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_view(("fokko", "non_existent_view"))
1449+
assert "View does not exist" in str(e.value)
1450+
1451+
14191452
def test_create_table_if_not_exists_200(
14201453
rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: dict[str, Any]
14211454
) -> None:

0 commit comments

Comments
 (0)