Skip to content

Commit 7fb55cd

Browse files
authored
feat: add load_view to REST catalog (#3224)
1 parent ee7d04b commit 7fb55cd

11 files changed

Lines changed: 138 additions & 0 deletions

File tree

pyiceberg/catalog/__init__.py

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

649+
@abstractmethod
650+
def load_view(self, identifier: str | Identifier) -> View:
651+
"""Load the view's metadata and returns the view instance.
652+
653+
Args:
654+
identifier (str | Identifier): View identifier.
655+
656+
Returns:
657+
View: the view instance with its metadata.
658+
659+
Raises:
660+
NoSuchViewError: If a view with the name does not exist.
661+
"""
662+
649663
@abstractmethod
650664
def load_namespace_properties(self, namespace: str | Identifier) -> Properties:
651665
"""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
@@ -559,6 +559,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
559559
def view_exists(self, identifier: str | Identifier) -> bool:
560560
raise NotImplementedError
561561

562+
def load_view(self, identifier: str | Identifier) -> View:
563+
raise NotImplementedError
564+
562565
def _get_iceberg_table_item(self, database_name: str, table_name: str) -> dict[str, Any]:
563566
try:
564567
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
@@ -976,6 +976,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
976976
def view_exists(self, identifier: str | Identifier) -> bool:
977977
raise NotImplementedError
978978

979+
def load_view(self, identifier: str | Identifier) -> View:
980+
raise NotImplementedError
981+
979982
@staticmethod
980983
def __is_iceberg_table(table: "TableTypeDef") -> bool:
981984
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
@@ -486,6 +486,9 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
486486
def view_exists(self, identifier: str | Identifier) -> bool:
487487
raise NotImplementedError
488488

489+
def load_view(self, identifier: str | Identifier) -> View:
490+
raise NotImplementedError
491+
489492
def _create_lock_request(self, database_name: str, table_name: str) -> LockRequest:
490493
lock_component: LockComponent = LockComponent(
491494
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
@@ -144,3 +144,6 @@ def create_view(
144144
properties: Properties = EMPTY_DICT,
145145
) -> View:
146146
raise NotImplementedError
147+
148+
def load_view(self, identifier: str | Identifier) -> View:
149+
raise NotImplementedError

pyiceberg/catalog/rest/__init__.py

Lines changed: 17 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
)
@@ -1109,6 +1112,20 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
11091112
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
11101113
return [(*view.namespace, view.name) for view in ListViewsResponse.model_validate_json(response.text).identifiers]
11111114

1115+
@retry(**_RETRY_ARGS)
1116+
def load_view(self, identifier: str | Identifier) -> View:
1117+
self._check_endpoint(Capability.V1_LOAD_VIEW)
1118+
response = self._session.get(
1119+
self.url(Endpoints.load_view, prefixed=True, **self._split_identifier_for_path(identifier, IdentifierKind.VIEW))
1120+
)
1121+
try:
1122+
response.raise_for_status()
1123+
except HTTPError as exc:
1124+
_handle_non_200_response(exc, {404: NoSuchViewError})
1125+
1126+
view_response = ViewResponse.model_validate_json(response.text)
1127+
return self._response_to_view(self.identifier_to_tuple(identifier), view_response)
1128+
11121129
@retry(**_RETRY_ARGS)
11131130
def commit_table(
11141131
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
@@ -748,6 +748,9 @@ def view_exists(self, identifier: str | Identifier) -> bool:
748748
def drop_view(self, identifier: str | Identifier) -> None:
749749
raise NotImplementedError
750750

751+
def load_view(self, identifier: str | Identifier) -> View:
752+
raise NotImplementedError
753+
751754
def close(self) -> None:
752755
"""Close the catalog and release database connections.
753756

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,
@@ -1449,6 +1450,38 @@ def test_create_view_409(
14491450
assert "View already exists" in str(e.value)
14501451

14511452

1453+
def test_load_view_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None:
1454+
rest_mock.get(
1455+
f"{TEST_URI}v1/namespaces/fokko/views/view",
1456+
json=example_view_metadata_rest_json,
1457+
status_code=200,
1458+
request_headers=TEST_HEADERS,
1459+
)
1460+
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
1461+
actual = catalog.load_view(("fokko", "view"))
1462+
expected = View(identifier=("fokko", "view"), metadata=ViewMetadata(**example_view_metadata_rest_json["metadata"]))
1463+
assert actual == expected
1464+
1465+
1466+
def test_load_view_404(rest_mock: Mocker) -> None:
1467+
rest_mock.get(
1468+
f"{TEST_URI}v1/namespaces/fokko/views/non_existent_view",
1469+
json={
1470+
"error": {
1471+
"message": "View does not exist: examples.non_existent_view in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
1472+
"type": "NoSuchViewException",
1473+
"code": 404,
1474+
}
1475+
},
1476+
status_code=404,
1477+
request_headers=TEST_HEADERS,
1478+
)
1479+
1480+
with pytest.raises(NoSuchViewError) as e:
1481+
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_view(("fokko", "non_existent_view"))
1482+
assert "View does not exist" in str(e.value)
1483+
1484+
14521485
def test_create_table_if_not_exists_200(
14531486
rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: dict[str, Any]
14541487
) -> None:

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2353,6 +2353,13 @@ def table_list(table_name: str) -> list[str]:
23532353
return [f"{table_name}_{idx}" for idx in range(NUM_TABLES)]
23542354

23552355

2356+
@pytest.fixture()
2357+
def view_name() -> str:
2358+
prefix = "my_iceberg_view-"
2359+
random_tag = "".join(choice(string.ascii_letters) for _ in range(RANDOM_LENGTH))
2360+
return (prefix + random_tag).lower()
2361+
2362+
23562363
@pytest.fixture()
23572364
def database_name() -> str:
23582365
prefix = "my_iceberg_database-"

0 commit comments

Comments
 (0)