This is an automated email from the ASF dual-hosted git repository.

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 492cabc99f [#11008] feat(client-python): add tests and update 
documentation of view CRUD support for relational catalog  (#14637)
492cabc99f is described below

commit 492cabc99fce6b034c7df476bae4c64718a87337
Author: PiotrAndrzejewsky <[email protected]>
AuthorDate: Fri Nov 6 15:16:15 2026 +0100

    [#11008] feat(client-python): add tests and update documentation of view 
CRUD support for relational catalog  (#14637)
    
    ### What changes were proposed in this pull request?
    
     This PR adds unit and integration tests and user
    documentation for View CRUD operations in the python relational catalog:
    - **Documentation**:
    
    - Updated `docs/manage-view-metadata-using-gravitino.md` with examples
    for creating, loading, altering, dropping, and listing views.
    - Updated `docs/how-to-use-python-client.md` to reference the view
    management capabilities and link to the guide.
    
    - **Unit Tests**:
    - Added unit test coverage in
    `tests/unittests/test_relational_catalog.py` matching
    `TestRelationalCatalogView.java` (create, load, list, drop, and alter
    operations including rename, set property, remove property, and replace
    view).
    
    - **Integration Tests**:
    - Added integration tests in
    `tests/integration/test_relational_catalog.py` for view rename and
    replace operations.
    
    - **Client Validation**:
    - Added `_check_full_schema_namespace` in `RelationalCatalog` to
    validate 3-level schema namespaces (`metalake.catalog.schema`) when
    formatting table and view REST request paths. This check is already
    present in java-client
    
    ### Why are the changes needed?
    
    To increase test coverage and add missing documentation after
    implementing the core functionality here #11947
    
    Fixes #11008
    
    ### Does this PR introduce _any_ user-facing change?
    
    No API change, only changes in documentation, to reflect actual code
    
    ### How was this patch tested?
    
    - **Unit tests**: Passed all Python client unit tests (1,429 tests):
        ` ./gradlew :clients:client-python:test -PskipITs`
    
    - Integration tests: Passed relational catalog integration tests against
    a live Gravitino server and Hive container:
    `./gradlew :clients:client-python:integrationTest
    -PskipDockerTests=false`
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../gravitino/client/relational_catalog.py         |  13 +
 .../tests/integration/test_relational_catalog.py   |  70 ++++
 .../tests/unittests/test_relational_catalog.py     | 365 +++++++++++++++++----
 docs/how-to-use-python-client.md                   |   7 +
 docs/manage-view-metadata-using-gravitino.md       |  94 +++++-
 5 files changed, 479 insertions(+), 70 deletions(-)

diff --git a/clients/client-python/gravitino/client/relational_catalog.py 
b/clients/client-python/gravitino/client/relational_catalog.py
index 427046d901..7ca4d053e2 100644
--- a/clients/client-python/gravitino/client/relational_catalog.py
+++ b/clients/client-python/gravitino/client/relational_catalog.py
@@ -188,7 +188,19 @@ class RelationalCatalog(
             f"Table namespace must be non-null and have 1 level, the input 
namespace is {namespace}",
         )
 
+    def _check_full_schema_namespace(
+        self, namespace: Namespace, entity_type: str
+    ) -> None:
+        Namespace.check(
+            namespace is not None and namespace.length() == 3,
+            (
+                f"{entity_type} full namespace must be non-null and have 3 
levels "
+                f'in the format "metalake.catalog.schema", the input namespace 
is {namespace}'
+            ),
+        )
+
     def _format_table_request_path(self, ns: Namespace) -> str:
+        self._check_full_schema_namespace(ns, "Table")
         schema_ns = Namespace.of(ns.level(0), ns.level(1))
         return (
             f"{BaseSchemaCatalog.format_schema_request_path(schema_ns)}"
@@ -228,6 +240,7 @@ class RelationalCatalog(
         )
 
     def _format_view_request_path(self, ns: Namespace) -> str:
+        self._check_full_schema_namespace(ns, "View")
         schema_ns = Namespace.of(ns.level(0), ns.level(1))
         return (
             f"{BaseSchemaCatalog.format_schema_request_path(schema_ns)}"
diff --git a/clients/client-python/tests/integration/test_relational_catalog.py 
b/clients/client-python/tests/integration/test_relational_catalog.py
index dae7fa8f04..e5e7439721 100644
--- a/clients/client-python/tests/integration/test_relational_catalog.py
+++ b/clients/client-python/tests/integration/test_relational_catalog.py
@@ -484,3 +484,73 @@ class TestRelationalCatalog(IntegrationTestEnv):
 
         with self.assertRaises(NoSuchViewException):
             view_catalog.alter_view(ident, ViewChange.set_property("property", 
"value"))
+
+    def _assert_view_renamed(
+        self,
+        view_catalog,
+        altered_view,
+        old_view_ident,
+        new_view_ident,
+        new_view_name,
+    ):
+        self.assertEqual(altered_view.name(), new_view_name)
+        self.assertTrue(view_catalog.view_exists(new_view_ident))
+        self.assertFalse(view_catalog.view_exists(old_view_ident))
+
+    def test_relational_catalog_alter_view_rename(self):
+        """Test altering view name in the relational catalog."""
+        self._create_test_table()
+        self._create_test_view()
+        view_catalog = self.catalog.as_view_catalog()
+
+        new_view_name = f"{self.view_name}_renamed"
+        new_view_ident = NameIdentifier.of(
+            TestRelationalCatalog.SCHEMA_NAME, new_view_name
+        )
+
+        altered_view = view_catalog.alter_view(
+            self.view_ident, ViewChange.rename(new_view_name)
+        )
+        self._assert_view_renamed(
+            view_catalog,
+            altered_view,
+            self.view_ident,
+            new_view_ident,
+            new_view_name,
+        )
+
+    def test_relational_catalog_alter_view_replace(self):
+        """Test replacing a view definition in the relational catalog."""
+        self._create_test_table()
+        self._create_test_view()
+        view_catalog = self.catalog.as_view_catalog()
+
+        new_comment = "Updated view comment"
+        new_columns = [
+            Column.of("id", Types.LongType.get(), "Primary key"),
+        ]
+        new_representations = [
+            SQLRepresentation(
+                Dialects.HIVE,
+                f"SELECT id FROM {TestRelationalCatalog.TABLE_NAME}",
+            )
+        ]
+
+        altered_view = view_catalog.alter_view(
+            self.view_ident,
+            ViewChange.replace_view(
+                new_columns,
+                new_representations,
+                None,
+                None,
+                new_comment,
+            ),
+        )
+
+        self.assertEqual(altered_view.comment(), new_comment)
+        self.assertEqual(len(altered_view.columns()), 1)
+        self.assertEqual(altered_view.columns()[0].name(), "id")
+        self.assertEqual(
+            altered_view.sql_for(Dialects.HIVE).sql(),
+            f"SELECT id FROM {TestRelationalCatalog.TABLE_NAME}",
+        )
diff --git a/clients/client-python/tests/unittests/test_relational_catalog.py 
b/clients/client-python/tests/unittests/test_relational_catalog.py
index bdc093cc7e..2c352ccb85 100644
--- a/clients/client-python/tests/unittests/test_relational_catalog.py
+++ b/clients/client-python/tests/unittests/test_relational_catalog.py
@@ -18,12 +18,14 @@
 import json
 import unittest
 from http.client import HTTPResponse
+from typing import Optional
 from unittest.mock import Mock, patch
 
 from gravitino.api.authorization.privileges import Privilege
 from gravitino.api.rel.column import Column
 from gravitino.api.rel.dialects import Dialects
 from gravitino.api.rel.indexes.index import Index
+from gravitino.api.rel.representation import Representation
 from gravitino.api.rel.sql_representation import SQLRepresentation
 from gravitino.api.rel.table_change import TableChange
 from gravitino.api.rel.types.types import Types
@@ -35,7 +37,11 @@ from gravitino.dto.rel.distribution_dto import 
DistributionDTO
 from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
 from gravitino.dto.rel.table_dto import TableDTO
 from gravitino.dto.rel.view_dto import ViewDTO
-from gravitino.dto.requests.view_update_request import ViewUpdateRequest
+from gravitino.dto.requests.view_update_request import (
+    ViewUpdateRequest,
+    ViewUpdateRequestBase,
+)
+from gravitino.dto.requests.view_updates_request import ViewUpdatesRequest
 from gravitino.dto.responses.drop_response import DropResponse
 from gravitino.dto.responses.entity_list_response import EntityListResponse
 from gravitino.dto.responses.table_response import TableResponse
@@ -43,6 +49,8 @@ from gravitino.dto.responses.view_response import ViewResponse
 from gravitino.dto.util.dto_converters import DTOConverters
 from gravitino.exceptions.base import (
     IllegalArgumentException,
+    IllegalNameIdentifierException,
+    IllegalNamespaceException,
     NoSuchSchemaException,
     NoSuchTableException,
     NoSuchViewException,
@@ -58,6 +66,69 @@ from tests.unittests.fixtures.table_fixtures import (
 
 
 class TestRelationalCatalog(unittest.TestCase):
+    @classmethod
+    def _default_columns(cls) -> list[ColumnDTO]:
+        return [
+            ColumnDTO(
+                _name="id",
+                _data_type=Types.IntegerType.get(),
+                _comment="id column",
+                _nullable=False,
+            )
+        ]
+
+    @classmethod
+    def _default_representations(cls) -> list[SQLRepresentationDTO]:
+        return [
+            SQLRepresentationDTO(
+                _dialect=Dialects.TRINO,
+                _sql="SELECT id FROM test_table",
+            )
+        ]
+
+    @classmethod
+    def _default_api_columns(cls) -> list[Column]:
+        return [
+            Column.of(
+                "id",
+                Types.IntegerType.get(),
+                comment="id column",
+                nullable=False,
+            )
+        ]
+
+    @classmethod
+    def _default_api_representations(cls) -> list[Representation]:
+        return [SQLRepresentation(Dialects.TRINO, "SELECT id FROM test_table")]
+
+    @classmethod
+    def _new_view_dto(
+        cls,
+        name: str,
+        comment: str = "test view comment",
+        columns: Optional[list[ColumnDTO]] = None,
+        representations: Optional[list[SQLRepresentationDTO]] = None,
+        properties: Optional[dict[str, str]] = None,
+        default_catalog: Optional[str] = "test_catalog",
+        default_schema: Optional[str] = "test_schema",
+    ) -> ViewDTO:
+        return ViewDTO(
+            _name=name,
+            _columns=columns if columns is not None else 
cls._default_columns(),
+            _representations=(
+                representations
+                if representations is not None
+                else cls._default_representations()
+            ),
+            _comment=comment,
+            _default_catalog=default_catalog,
+            _default_schema=default_schema,
+            _properties=properties if properties is not None else {},
+            _audit=AuditDTO(
+                "creator", "2022-01-01T00:00:00Z", "modifier", 
"2022-01-01T00:00:00Z"
+            ),
+        )
+
     @classmethod
     def setUpClass(cls) -> None:
         cls.metalake_name = "test_metalake"
@@ -79,30 +150,7 @@ class TestRelationalCatalog(unittest.TestCase):
         )
         cls.TABLE_DTO_JSON_STRING = 
TABLE_DTO_JSON_STRING_WITH_STARTING_DATE_SORT
         cls.table_dto = TableDTO.from_json(cls.TABLE_DTO_JSON_STRING)
-        cls.view_dto = ViewDTO(
-            _name=cls.view_name,
-            _columns=[
-                ColumnDTO(
-                    _name="id",
-                    _data_type=Types.IntegerType.get(),
-                    _comment="id column",
-                    _nullable=False,
-                )
-            ],
-            _representations=[
-                SQLRepresentationDTO(
-                    _dialect=Dialects.TRINO,
-                    _sql="SELECT id FROM test_table",
-                )
-            ],
-            _comment="test view comment",
-            _default_catalog="test_catalog",
-            _default_schema="test_schema",
-            _properties={"k1": "v1"},
-            _audit=AuditDTO(
-                "creator", "2022-01-01T00:00:00Z", "modifier", 
"2022-01-01T00:00:00Z"
-            ),
-        )
+        cls.view_dto = cls._new_view_dto(cls.view_name, properties={"k1": 
"v1"})
 
     def _get_mock_http_resp(self, json_str: str, return_code: int = 200):
         mock_http_resp = Mock(HTTPResponse)
@@ -458,6 +506,42 @@ class TestRelationalCatalog(unittest.TestCase):
                     Namespace.of("invalid_schema")
                 )
 
+    def test_list_views_empty(self):
+        resp_body = EntityListResponse(0, [])
+        mock_resp = self._get_mock_http_resp(resp_body.to_json())
+
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.get",
+            return_value=mock_resp,
+        ):
+            views = self.catalog.as_view_catalog().list_views(
+                Namespace.of(self.schema_name)
+            )
+            self.assertEqual(0, len(views))
+
+    def test_list_views_server_internal_error(self):
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.get",
+            side_effect=RuntimeError("internal error"),
+        ):
+            with self.assertRaises(RuntimeError):
+                self.catalog.as_view_catalog().list_views(
+                    Namespace.of(self.schema_name)
+                )
+
+    def test_list_views_invalid_namespace_length(self):
+        with self.assertRaisesRegex(
+            IllegalNamespaceException,
+            "View namespace must be non-null and have 1 level",
+        ):
+            self.catalog.as_view_catalog().list_views(Namespace.of("s1", "s2"))
+
+        with self.assertRaisesRegex(
+            IllegalNamespaceException,
+            "View namespace must be non-null and have 1 level",
+        ):
+            self.catalog.as_view_catalog().list_views(Namespace.empty())
+
     def test_load_view(self):
         resp_body = ViewResponse(0, self.view_dto)
         mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -484,6 +568,20 @@ class TestRelationalCatalog(unittest.TestCase):
             with self.assertRaises(NoSuchViewException):
                 self.catalog.as_view_catalog().load_view(self.view_identifier)
 
+    def test_load_view_invalid_identifier(self):
+        with self.assertRaisesRegex(
+            IllegalNameIdentifierException, "NameIdentifier must not be null"
+        ):
+            self.catalog.as_view_catalog().load_view(None)
+
+        with self.assertRaisesRegex(
+            IllegalNamespaceException,
+            "View namespace must be non-null and have 1 level",
+        ):
+            self.catalog.as_view_catalog().load_view(
+                NameIdentifier.of("s1", "s2", "view1")
+            )
+
     def test_create_view(self):
         resp_body = ViewResponse(0, self.view_dto)
         mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -494,8 +592,8 @@ class TestRelationalCatalog(unittest.TestCase):
         ):
             view = self.catalog.as_view_catalog().create_view(
                 self.view_identifier,
-                [Column.of("id", Types.IntegerType.get(), nullable=False)],
-                [SQLRepresentation(Dialects.TRINO, "SELECT id FROM 
test_table")],
+                self._default_api_columns(),
+                self._default_api_representations(),
                 comment="test view comment",
                 default_catalog="test_catalog",
                 default_schema="test_schema",
@@ -512,47 +610,96 @@ class TestRelationalCatalog(unittest.TestCase):
             with self.assertRaises(ViewAlreadyExistsException):
                 self.catalog.as_view_catalog().create_view(
                     self.view_identifier,
-                    [Column.of("id", Types.IntegerType.get())],
-                    [SQLRepresentation(Dialects.TRINO, "SELECT id FROM 
test_table")],
+                    self._default_api_columns(),
+                    self._default_api_representations(),
                 )
 
-    def test_alter_view(self):
-        updated_view_dto = ViewDTO(
-            _name="new_view",
-            _columns=[
-                ColumnDTO(
-                    _name="id",
-                    _data_type=Types.IntegerType.get(),
-                    _comment="id column",
-                    _nullable=False,
-                )
-            ],
-            _representations=[
-                SQLRepresentationDTO(
-                    _dialect=Dialects.TRINO,
-                    _sql="SELECT id FROM test_table",
+    def test_create_view_schema_not_found(self):
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.post",
+            side_effect=NoSuchSchemaException("Schema not found"),
+        ):
+            with self.assertRaises(NoSuchSchemaException):
+                self.catalog.as_view_catalog().create_view(
+                    self.view_identifier,
+                    self._default_api_columns(),
+                    self._default_api_representations(),
                 )
-            ],
-            _comment="test view comment",
-            _default_catalog="test_catalog",
-            _default_schema="test_schema",
-            _properties={"k1": "v1"},
-            _audit=AuditDTO(
-                "creator", "2022-01-01T00:00:00Z", "modifier", 
"2022-01-01T00:00:00Z"
-            ),
+
+    def test_create_view_rejects_empty_representations(self):
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            '"representations" field is required and cannot be empty',
+        ):
+            self.catalog.as_view_catalog().create_view(
+                self.view_identifier,
+                self._default_api_columns(),
+                [],
+            )
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            '"representations" field is required and cannot be empty',
+        ):
+            self.catalog.as_view_catalog().create_view(
+                self.view_identifier,
+                self._default_api_columns(),
+                None,
+            )
+
+    def test_create_view_invalid_identifier(self):
+        with self.assertRaises(IllegalNamespaceException):
+            self.catalog.as_view_catalog().create_view(
+                NameIdentifier.of("s1", "s2", "v1"),
+                self._default_api_columns(),
+                self._default_api_representations(),
+            )
+
+    def _run_alter_view(
+        self,
+        ident: NameIdentifier,
+        req: ViewUpdateRequestBase,
+        updated: ViewDTO,
+        change: ViewChange,
+    ):
+        full_namespace = Namespace.of(
+            self.metalake_name, self.catalog_name, ident.namespace().level(0)
         )
-        resp_body = ViewResponse(0, updated_view_dto)
+        # pylint: disable=protected-access
+        view_path = (
+            
f"{self.catalog._format_view_request_path(full_namespace)}/{ident.name()}"
+        )
+        updates_request = ViewUpdatesRequest(_updates=[req])
+        resp_body = ViewResponse(0, updated)
         mock_resp = self._get_mock_http_resp(resp_body.to_json())
 
         with patch(
             "gravitino.utils.http_client.HTTPClient.put",
             return_value=mock_resp,
-        ):
-            view = self.catalog.as_view_catalog().alter_view(
-                self.view_identifier,
-                ViewChange.rename("new_view"),
+        ) as mock_put:
+            altered = self.catalog.as_view_catalog().alter_view(ident, change)
+            self.assertEqual(mock_put.call_args[0][0], view_path)
+            self.assertEqual(mock_put.call_args[1]["json"], updates_request)
+            self.assertEqual(updated.name(), altered.name())
+            self.assertEqual(updated.comment(), altered.comment())
+            self.assertEqual(updated.properties(), altered.properties())
+            self.assertEqual(
+                len(updated.representations()), len(altered.representations())
             )
-            self.assertEqual("new_view", view.name())
+            return altered
+
+    def test_alter_view_rename(self):
+        ident = NameIdentifier.of(self.schema_name, "view1")
+        updated = self._new_view_dto(
+            "view2",
+            "comment",
+            self._default_columns(),
+            self._default_representations(),
+        )
+        req = ViewUpdateRequest.RenameViewRequest(_new_name="view2")
+        self._run_alter_view(ident, req, updated, ViewChange.rename("view2"))
+
+    test_alter_view = test_alter_view_rename
 
     def test_alter_view_not_exists(self):
         with patch(
@@ -565,6 +712,76 @@ class TestRelationalCatalog(unittest.TestCase):
                     ViewChange.rename("new_view"),
                 )
 
+    def test_alter_view_set_property(self):
+        ident = NameIdentifier.of(self.schema_name, "view1")
+        updated = self._new_view_dto(
+            "view1",
+            "comment",
+            self._default_columns(),
+            self._default_representations(),
+            properties={"k1": "v1"},
+        )
+        req = ViewUpdateRequest.SetViewPropertyRequest(_property="k1", 
_value="v1")
+        self._run_alter_view(ident, req, updated, 
ViewChange.set_property("k1", "v1"))
+
+    def test_alter_view_remove_property(self):
+        ident = NameIdentifier.of(self.schema_name, "view1")
+        updated = self._new_view_dto(
+            "view1",
+            "comment",
+            self._default_columns(),
+            self._default_representations(),
+        )
+        req = ViewUpdateRequest.RemoveViewPropertyRequest(_property="k1")
+        self._run_alter_view(ident, req, updated, 
ViewChange.remove_property("k1"))
+
+    def test_alter_view_replace(self):
+        ident = NameIdentifier.of(self.schema_name, "view1")
+        new_rep = SQLRepresentationDTO(
+            _dialect=Dialects.TRINO,
+            _sql="SELECT 3",
+        )
+        new_columns = self._default_columns()
+        new_reps = [new_rep]
+        updated = self._new_view_dto(
+            "view1",
+            "new comment",
+            new_columns,
+            new_reps,
+            default_catalog=None,
+            default_schema=None,
+        )
+
+        req = ViewUpdateRequest.ReplaceViewRequest(
+            _columns=new_columns,
+            _representations=new_reps,
+            _default_catalog=None,
+            _default_schema=None,
+            _comment="new comment",
+        )
+        new_rep_api = SQLRepresentation(Dialects.TRINO, "SELECT 3")
+        altered = self._run_alter_view(
+            ident,
+            req,
+            updated,
+            ViewChange.replace_view(
+                self._default_api_columns(),
+                [new_rep_api],
+                None,
+                None,
+                "new comment",
+            ),
+        )
+        self.assertIsNone(altered.default_catalog())
+        self.assertIsNone(altered.default_schema())
+
+    def test_alter_view_invalid_identifier(self):
+        with self.assertRaises(IllegalNamespaceException):
+            self.catalog.as_view_catalog().alter_view(
+                NameIdentifier.of("s1", "s2", "v1"),
+                ViewChange.rename("new_name"),
+            )
+
     def test_drop_view(self):
         resp_body = DropResponse(0, True)
         mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -587,6 +804,38 @@ class TestRelationalCatalog(unittest.TestCase):
             is_dropped = 
self.catalog.as_view_catalog().drop_view(self.view_identifier)
             self.assertFalse(is_dropped)
 
+    def test_drop_view_server_internal_error(self):
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.delete",
+            side_effect=RuntimeError("internal error"),
+        ):
+            with self.assertRaises(RuntimeError):
+                self.catalog.as_view_catalog().drop_view(self.view_identifier)
+
+    def test_drop_view_invalid_identifier(self):
+        with self.assertRaises(IllegalNamespaceException):
+            self.catalog.as_view_catalog().drop_view(
+                NameIdentifier.of("s1", "s2", "v1")
+            )
+
+    def test_format_table_request_path_invalid_namespace(self):
+        with self.assertRaisesRegex(
+            IllegalNamespaceException,
+            "Table full namespace must be non-null and have 3 levels in the 
format",
+        ):
+            self.catalog._format_table_request_path(  # pylint: 
disable=protected-access
+                Namespace.of("schema1")
+            )
+
+    def test_format_view_request_path_invalid_namespace(self):
+        with self.assertRaisesRegex(
+            IllegalNamespaceException,
+            "View full namespace must be non-null and have 3 levels in the 
format",
+        ):
+            self.catalog._format_view_request_path(  # pylint: 
disable=protected-access
+                Namespace.of("schema1")
+            )
+
     def test_to_view_update_request(self):
         to_request = (
             RelationalCatalog._to_view_update_request  # pylint: 
disable=protected-access
diff --git a/docs/how-to-use-python-client.md b/docs/how-to-use-python-client.md
index 6618439da0..695f6558bf 100644
--- a/docs/how-to-use-python-client.md
+++ b/docs/how-to-use-python-client.md
@@ -54,6 +54,13 @@ extras.
 1. [Manage metalake using Gravitino Python 
API](./manage-metalake-using-gravitino.md?language=python)
 2. [Manage fileset metadata using Gravitino Python 
API](./manage-fileset-metadata-using-gravitino.md?language=python)
 3. [Manage Semantic Model metadata using Gravitino Python 
API](./manage-semantic-model-metadata-using-gravitino.md?language=python)
+4. [Manage view metadata using Gravitino Python 
API](./manage-view-metadata-using-gravitino.md?language=python)
+
+### View Operations
+
+Use `as_view_catalog()` on a loaded relational catalog to manage views. The
+[View guide](./manage-view-metadata-using-gravitino.md?language=python) 
includes Python examples for view
+creation, list/load, alteration (rename, set/remove property, and replace), 
and drop operations.
 
 ### HTTP Error Handling
 
diff --git a/docs/manage-view-metadata-using-gravitino.md 
b/docs/manage-view-metadata-using-gravitino.md
index 5118040e51..3d24e5bae7 100644
--- a/docs/manage-view-metadata-using-gravitino.md
+++ b/docs/manage-view-metadata-using-gravitino.md
@@ -18,6 +18,8 @@ Views are supported by the Hive, Iceberg, and Paimon catalogs.
 
 ## View Operations
 
+For client setup and configuration, see [Client 
Setup](./how-to-use-gravitino-client.md) and [Python 
Client](./how-to-use-python-client.md).
+
 ### Create a View
 
 A view carries its columns and one or more representations, each holding a 
query and the dialect it
@@ -32,13 +34,20 @@ curl -X POST -H "Accept: application/vnd.gravitino.v1+json" 
\
   -H "Content-Type: application/json" -d '{
   "name": "active_customers",
   "comment": "Customers with orders in the last year",
-  "query": "SELECT * FROM customers WHERE last_order_at > current_date - 
interval 365 day",
-  "dialect": "spark",
   "columns": [
     {"name": "id", "type": "integer", "nullable": false},
     {"name": "name", "type": "varchar(500)", "nullable": true}
   ],
-  "properties": {}
+  "representations": [
+    {
+      "type": "sql",
+      "sql": "SELECT * FROM customers WHERE last_order_at > current_date - 
interval 365 day",
+      "dialect": "spark"
+    }
+  ],
+  "properties": {
+    "spark.sql.create.version": "3.5.0"
+  }
 }' 
http://localhost:8090/api/metalakes/example/catalogs/sales/schemas/public/views
 ```
 
@@ -69,7 +78,37 @@ View view = views.createView(
     representations,
     "sales",
     "public",
-    ImmutableMap.of());
+    ImmutableMap.of("spark.sql.create.version", "3.5.0"));
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+catalog = client.load_catalog("sales")
+views = catalog.as_view_catalog()
+
+columns = [
+    Column.of("id", Types.IntegerType.get(), nullable=False),
+    Column.of("name", Types.VarCharType.of(500)),
+]
+
+representations = [
+    SQLRepresentation(
+        Dialects.SPARK,
+        "SELECT * FROM customers WHERE last_order_at > current_date - interval 
365 day",
+    )
+]
+
+view = views.create_view(
+    NameIdentifier.of("public", "active_customers"),
+    columns=columns,
+    representations=representations,
+    comment="Customers with orders in the last year",
+    default_catalog="sales",
+    default_schema="public",
+    properties={"spark.sql.create.version": "3.5.0"},
+)
 ```
 
 </TabItem>
@@ -95,17 +134,24 @@ curl -X GET -H "Accept: application/vnd.gravitino.v1+json" 
\
 View view = views.loadView(NameIdentifier.of("public", "active_customers"));
 ```
 
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+view = views.load_view(NameIdentifier.of("public", "active_customers"))
+```
+
 </TabItem>
 </Tabs>
 
 ### Alter a View
 
-| Change             | JSON                                                    
     | Java                                       |
-|--------------------|--------------------------------------------------------------|--------------------------------------------|
-| Rename             | `{"@type":"rename","newName":"view_renamed"}`           
     | `ViewChange.rename("view_renamed")`        |
-| Update the comment | `{"@type":"updateComment","newComment":"new_comment"}`  
     | `ViewChange.updateComment("new_comment")`  |
-| Set a property     | 
`{"@type":"setProperty","property":"key1","value":"value1"}` | 
`ViewChange.setProperty("key1", "value1")` |
-| Remove a property  | `{"@type":"removeProperty","property":"key1"}`          
     | `ViewChange.removeProperty("key1")`        |
+| Change             | JSON                                                    
     | Java                                       | Python                      
               |
+|--------------------|--------------------------------------------------------------|--------------------------------------------|--------------------------------------------|
+| Rename             | `{"@type":"rename","newName":"view_renamed"}`           
     | `ViewChange.rename("view_renamed")`        | 
`ViewChange.rename("view_renamed")`        |
+| Set a property     | 
`{"@type":"setProperty","property":"key1","value":"value1"}` | 
`ViewChange.setProperty("key1", "value1")` | `ViewChange.set_property("key1", 
"value1")` |
+| Remove a property  | `{"@type":"removeProperty","property":"key1"}`          
     | `ViewChange.removeProperty("key1")`        | 
`ViewChange.remove_property("key1")`       |
+| Replace a view     | `{"@type":"replaceView",...}`                           
     | `ViewChange.replaceView(...)`              | 
`ViewChange.replace_view(...)`             |
 
 <Tabs groupId='language' queryString>
 <TabItem value="shell" label="REST">
@@ -114,7 +160,7 @@ View view = views.loadView(NameIdentifier.of("public", 
"active_customers"));
 curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
   -H "Content-Type: application/json" -d '{
   "updates": [
-    {"@type": "updateComment", "newComment": "Customers active in the last 
year"}
+    {"@type": "setProperty", "property": "key1", "value": "value1"}
   ]
 }' 
http://localhost:8090/api/metalakes/example/catalogs/sales/schemas/public/views/active_customers
 ```
@@ -125,7 +171,17 @@ curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" 
\
 ```java
 View view = views.alterView(
     NameIdentifier.of("public", "active_customers"),
-    ViewChange.updateComment("Customers active in the last year"));
+    ViewChange.setProperty("key1", "value1"));
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+view = views.alter_view(
+    NameIdentifier.of("public", "active_customers"),
+    ViewChange.set_property("key1", "value1"),
+)
 ```
 
 </TabItem>
@@ -150,6 +206,13 @@ curl -X DELETE -H "Accept: 
application/vnd.gravitino.v1+json" \
 boolean dropped = views.dropView(NameIdentifier.of("public", 
"active_customers"));
 ```
 
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+dropped = views.drop_view(NameIdentifier.of("public", "active_customers"))
+```
+
 </TabItem>
 </Tabs>
 
@@ -170,5 +233,12 @@ curl -X GET -H "Accept: application/vnd.gravitino.v1+json" 
\
 NameIdentifier[] identifiers = views.listViews(Namespace.of("public"));
 ```
 
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+identifiers = views.list_views(Namespace.of("public"))
+```
+
 </TabItem>
 </Tabs>

Reply via email to