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

kevinjqliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 1d31eff86 feat(view): View object API (#3338)
1d31eff86 is described below

commit 1d31eff86154540129f40fe9146c8f27d5e54d41
Author: Gabriel Igliozzi <[email protected]>
AuthorDate: Sun May 24 00:14:50 2026 +0200

    feat(view): View object API (#3338)
    
    <!--
    Thanks for opening a pull request!
    -->
    
    <!-- In the case this PR will resolve an issue, please replace
    ${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
    <!-- Closes #${GITHUB_ISSUE_ID} -->
    
    # Rationale for this change
    
    Add view api to abstract away access to the metadata
    
    ## Are these changes tested?
    
    Yes, created tests/test_view.py
    
    ## Are there any user-facing changes?
    
    Yes, a new api is available for views
    
    <!-- In the case of user-facing changes, please add the changelog label.
    -->
---
 pyiceberg/view/__init__.py |  50 +++++++++++++++++--
 tests/conftest.py          |  37 ++++++++++++++
 tests/test_view.py         | 121 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 204 insertions(+), 4 deletions(-)

diff --git a/pyiceberg/view/__init__.py b/pyiceberg/view/__init__.py
index 4ddb21a11..a0bcefa85 100644
--- a/pyiceberg/view/__init__.py
+++ b/pyiceberg/view/__init__.py
@@ -16,12 +16,12 @@
 # under the License.
 from __future__ import annotations
 
-from typing import (
-    Any,
-)
+from typing import Any
+from uuid import UUID
 
+from pyiceberg.schema import Schema
 from pyiceberg.typedef import Identifier
-from pyiceberg.view.metadata import ViewMetadata
+from pyiceberg.view.metadata import SQLViewRepresentation, ViewHistoryEntry, 
ViewMetadata, ViewVersion
 
 
 class View:
@@ -42,6 +42,48 @@ class View:
         """Return the identifier of this view."""
         return self._identifier
 
+    def schema(self) -> Schema:
+        """Return the schema for this view."""
+        return next(schema for schema in self.metadata.schemas if 
schema.schema_id == self.current_version().schema_id)
+
+    def schemas(self) -> dict[int, Schema]:
+        """Return the schemas for this view."""
+        return {schema.schema_id: schema for schema in self.metadata.schemas}
+
+    def current_version(self) -> ViewVersion:
+        """Get the version of this view."""
+        return next(version for version in self.metadata.versions if 
version.version_id == self.metadata.current_version_id)
+
+    @property
+    def versions(self) -> list[ViewVersion]:
+        """Get the versions of this view."""
+        return self.metadata.versions
+
+    def version(self, version_id: int) -> ViewVersion:
+        """Get the version in this view by ID."""
+        return next(version for version in self.metadata.versions if 
version.version_id == version_id)
+
+    def history(self) -> list[ViewHistoryEntry]:
+        """Get the version of this history view."""
+        return self.metadata.version_log
+
+    @property
+    def properties(self) -> dict[str, str]:
+        """Return a map of string properties for this view."""
+        return self.metadata.properties
+
+    def location(self) -> str:
+        """Return the view's base location."""
+        return self.metadata.location
+
+    def uuid(self) -> UUID:
+        """Return the view's UUID."""
+        return UUID(self.metadata.view_uuid)
+
+    def sql_for(self, dialect: str) -> SQLViewRepresentation:
+        """Return the view representation for the sql dialect."""
+        return next(repr.root for repr in 
self.current_version().representations if repr.root.dialect == dialect)
+
     def __eq__(self, other: Any) -> bool:
         """Return the equality of two instances of the View class."""
         return self.name() == other.name() and self.metadata == other.metadata 
if isinstance(other, View) else False
diff --git a/tests/conftest.py b/tests/conftest.py
index b74e2ecab..68db3d254 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1174,6 +1174,43 @@ def example_view_metadata_v1() -> dict[str, Any]:
     }
 
 
[email protected]
+def example_view_metadata_v1_multiple_versions() -> dict[str, Any]:
+    return {
+        "view-uuid": "a20125c8-7284-442c-9aea-15fee620737c",
+        "format-version": 1,
+        "location": "s3://bucket/test/location/test_view",
+        "current-version-id": 2,
+        "versions": [
+            {
+                "version-id": 1,
+                "timestamp-ms": 1602638573874,
+                "schema-id": 1,
+                "summary": {},
+                "representations": [{"type": "sql", "sql": "SELECT 1", 
"dialect": "spark"}],
+                "default-namespace": ["default"],
+            },
+            {
+                "version-id": 2,
+                "timestamp-ms": 1602638573875,
+                "schema-id": 2,
+                "summary": {},
+                "representations": [{"type": "sql", "sql": "SELECT 2", 
"dialect": "spark"}],
+                "default-namespace": ["default"],
+            },
+        ],
+        "schemas": [
+            {"type": "struct", "schema-id": 1, "fields": [{"id": 1, "name": 
"a", "required": True, "type": "long"}]},
+            {"type": "struct", "schema-id": 2, "fields": [{"id": 2, "name": 
"b", "required": True, "type": "string"}]},
+        ],
+        "version-log": [
+            {"timestamp-ms": 1602638573874, "version-id": 1},
+            {"timestamp-ms": 1602638573875, "version-id": 2},
+        ],
+        "properties": {},
+    }
+
+
 @pytest.fixture
 def example_table_metadata_v3() -> dict[str, Any]:
     return EXAMPLE_TABLE_METADATA_V3
diff --git a/tests/test_view.py b/tests/test_view.py
new file mode 100644
index 000000000..3919e1ab8
--- /dev/null
+++ b/tests/test_view.py
@@ -0,0 +1,121 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from typing import Any
+from uuid import UUID
+
+import pytest
+
+from pyiceberg.schema import Schema
+from pyiceberg.view import View
+from pyiceberg.view.metadata import SQLViewRepresentation, ViewHistoryEntry, 
ViewMetadata, ViewVersion
+
+
[email protected]
+def view(example_view_metadata_v1: dict[str, Any]) -> View:
+    metadata = ViewMetadata.model_validate(example_view_metadata_v1)
+    return View(("default", "test_view"), metadata)
+
+
+def test_view_schema(view: View) -> None:
+    schema = view.schema()
+    assert isinstance(schema, Schema)
+    assert schema.schema_id == 1
+    assert len(schema.fields) == 3
+    assert schema.find_field("x") is not None
+    assert schema.find_field("y") is not None
+    assert schema.find_field("z") is not None
+
+
+def test_view_schemas(view: View) -> None:
+    schemas = view.schemas()
+    assert isinstance(schemas, dict)
+    assert len(schemas) == 1
+    assert 1 in schemas
+    assert isinstance(schemas[1], Schema)
+
+
+def test_view_current_version(view: View) -> None:
+    version = view.current_version()
+    assert isinstance(version, ViewVersion)
+    assert version.version_id == 1
+    assert version.schema_id == 1
+
+
+def test_view_versions(view: View) -> None:
+    versions = view.versions
+    assert len(versions) == 1
+    assert isinstance(versions[0], ViewVersion)
+    assert versions[0].version_id == 1
+
+
+def test_view_version_by_id(view: View) -> None:
+    version = view.version(1)
+    assert isinstance(version, ViewVersion)
+    assert version.version_id == 1
+    assert version == view.current_version()
+
+
+def test_view_history(view: View) -> None:
+    history = view.history()
+    assert len(history) == 1
+    assert isinstance(history[0], ViewHistoryEntry)
+    assert history[0].version_id == 1
+    assert history[0].timestamp_ms == 1602638573874
+
+
+def test_view_properties(view: View) -> None:
+    assert view.properties == {"comment": "this is a test view"}
+
+
+def test_view_location(view: View) -> None:
+    assert view.location() == "s3://bucket/test/location/test_view"
+
+
+def test_view_uuid(view: View) -> None:
+    assert view.uuid() == UUID("a20125c8-7284-442c-9aea-15fee620737c")
+
+
+def test_view_sql_for_dialect(view: View) -> None:
+    repr = view.sql_for("spark")
+    assert isinstance(repr, SQLViewRepresentation)
+    assert repr.dialect == "spark"
+    assert repr.sql == "SELECT * FROM prod.db.table"
+
+
+def test_view_schemas_multiple(example_view_metadata_v1_multiple_versions: 
dict[str, Any]) -> None:
+    view = View(("default", "test_view"), 
ViewMetadata.model_validate(example_view_metadata_v1_multiple_versions))
+    schemas = view.schemas()
+    assert len(schemas) == 2
+    assert 1 in schemas
+    assert 2 in schemas
+    assert view.schema().schema_id == 2
+
+
+def test_view_versions_multiple(example_view_metadata_v1_multiple_versions: 
dict[str, Any]) -> None:
+    view = View(("default", "test_view"), 
ViewMetadata.model_validate(example_view_metadata_v1_multiple_versions))
+    assert len(view.versions) == 2
+    assert view.current_version().version_id == 2
+
+
+def test_view_version_unknown_id(view: View) -> None:
+    with pytest.raises(StopIteration):
+        view.version(999)
+
+
+def test_view_sql_for_unknown_dialect(view: View) -> None:
+    with pytest.raises(StopIteration):
+        view.sql_for("trino")

Reply via email to