Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pyiceberg/catalog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,23 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
NoSuchNamespaceError: If a namespace with the given name does not exist.
"""

@abstractmethod
def load_view(self, identifier: str | Identifier) -> View:
"""Load the view's metadata and returns the view instance.

You can also use this method to check for view existence using 'try catalog.load_view() except NoSuchViewError'.
Note: This method doesn't scan data stored in the view.

Args:
identifier (str | Identifier): View identifier.

Returns:
View: the view instance with its metadata.

Raises:
NoSuchViewError: If a view with the name does not exist.
"""

@abstractmethod
def load_namespace_properties(self, namespace: str | Identifier) -> Properties:
"""Get properties for a namespace.
Expand Down
4 changes: 4 additions & 0 deletions pyiceberg/catalog/bigquery_metastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from pyiceberg.table.update import TableRequirement, TableUpdate
from pyiceberg.typedef import EMPTY_DICT, Identifier, Properties
from pyiceberg.utils.config import Config
from pyiceberg.view import View

if TYPE_CHECKING:
import pyarrow as pa
Expand Down Expand Up @@ -310,6 +311,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
def view_exists(self, identifier: str | Identifier) -> bool:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError

def load_namespace_properties(self, namespace: str | Identifier) -> Properties:
dataset_name = self.identifier_to_database(namespace)

Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
def view_exists(self, identifier: str | Identifier) -> bool:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError

def _get_iceberg_table_item(self, database_name: str, table_name: str) -> dict[str, Any]:
try:
return self._get_dynamo_item(identifier=f"{database_name}.{table_name}", namespace=database_name)
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/glue.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,9 @@ def drop_view(self, identifier: str | Identifier) -> None:
def view_exists(self, identifier: str | Identifier) -> bool:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError

@staticmethod
def __is_iceberg_table(table: "TableTypeDef") -> bool:
return table.get("Parameters", {}).get(TABLE_TYPE, "").lower() == ICEBERG
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/hive.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,9 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
def view_exists(self, identifier: str | Identifier) -> bool:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError

def _create_lock_request(self, database_name: str, table_name: str) -> LockRequest:
lock_component: LockComponent = LockComponent(
level=LockLevel.TABLE, type=LockType.EXCLUSIVE, dbname=database_name, tablename=table_name, isTransactional=True
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/noop.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,6 @@ def create_view(
properties: Properties = EMPTY_DICT,
) -> View:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError
18 changes: 18 additions & 0 deletions pyiceberg/catalog/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class Endpoints:
get_token: str = "oauth/tokens"
rename_table: str = "tables/rename"
list_views: str = "namespaces/{namespace}/views"
load_view: str = "namespaces/{namespace}/views/{view}"
create_view: str = "namespaces/{namespace}/views"
drop_view: str = "namespaces/{namespace}/views/{view}"
view_exists: str = "namespaces/{namespace}/views/{view}"
Expand Down Expand Up @@ -180,6 +181,7 @@ class Capability:
V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")

V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
V1_LOAD_VIEW = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_view}")
V1_VIEW_EXISTS = Endpoint(http_method=HttpMethod.HEAD, path=f"{API_PREFIX}/{Endpoints.view_exists}")
V1_DELETE_VIEW = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_view}")
V1_SUBMIT_TABLE_SCAN_PLAN = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.plan_table_scan}")
Expand Down Expand Up @@ -209,6 +211,7 @@ class Capability:
VIEW_ENDPOINTS: frozenset[Endpoint] = frozenset(
(
Capability.V1_LIST_VIEWS,
Capability.V1_LOAD_VIEW,
Capability.V1_DELETE_VIEW,
)
)
Expand Down Expand Up @@ -1099,6 +1102,21 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]:
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
return [(*view.namespace, view.name) for view in ListViewsResponse.model_validate_json(response.text).identifiers]

@retry(**_RETRY_ARGS)
def load_view(self, identifier: str | Identifier) -> View:
self._check_endpoint(Capability.V1_LOAD_VIEW)
response = self._session.get(
self.url(Endpoints.load_view, prefixed=True, **self._split_identifier_for_path(identifier, IdentifierKind.VIEW)),
params={},
)
try:
response.raise_for_status()
except HTTPError as exc:
_handle_non_200_response(exc, {404: NoSuchViewError})

view_response = ViewResponse.model_validate_json(response.text)
return self._response_to_view(self.identifier_to_tuple(identifier), view_response)

@retry(**_RETRY_ARGS)
def commit_table(
self, table: Table, requirements: tuple[TableRequirement, ...], updates: tuple[TableUpdate, ...]
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,9 @@ def view_exists(self, identifier: str | Identifier) -> bool:
def drop_view(self, identifier: str | Identifier) -> None:
raise NotImplementedError

def load_view(self, identifier: str | Identifier) -> View:
raise NotImplementedError

def close(self) -> None:
"""Close the catalog and release database connections.
Expand Down
33 changes: 33 additions & 0 deletions tests/catalog/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
Capability.V1_RENAME_TABLE,
Capability.V1_REGISTER_TABLE,
Capability.V1_LIST_VIEWS,
Capability.V1_LOAD_VIEW,
Capability.V1_VIEW_EXISTS,
Capability.V1_DELETE_VIEW,
Capability.V1_SUBMIT_TABLE_SCAN_PLAN,
Expand Down Expand Up @@ -1416,6 +1417,38 @@ def test_create_view_409(
assert "View already exists" in str(e.value)


def test_load_view_200(rest_mock: Mocker, example_view_metadata_rest_json: dict[str, Any]) -> None:
rest_mock.get(
f"{TEST_URI}v1/namespaces/fokko/views/view",
json=example_view_metadata_rest_json,
status_code=200,
request_headers=TEST_HEADERS,
)
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
actual = catalog.load_view(("fokko", "view"))
expected = View(identifier=("fokko", "view"), metadata=ViewMetadata(**example_view_metadata_rest_json["metadata"]))
assert actual == expected


def test_load_view_404(rest_mock: Mocker) -> None:
rest_mock.get(
f"{TEST_URI}v1/namespaces/fokko/views/non_existent_view",
json={
"error": {
"message": "View does not exist: examples.non_existent_view in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
"type": "NoSuchNamespaceErrorException",
"code": 404,
}
},
status_code=404,
request_headers=TEST_HEADERS,
)

with pytest.raises(NoSuchViewError) as e:
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_view(("fokko", "non_existent_view"))
assert "View does not exist" in str(e.value)


def test_create_table_if_not_exists_200(
rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: dict[str, Any]
) -> None:
Expand Down
Loading