1414# KIND, either express or implied. See the License for the
1515# specific language governing permissions and limitations
1616# under the License.
17+ from __future__ import annotations
18+
1719from collections import deque
1820from enum import Enum
1921from typing import (
2022 TYPE_CHECKING ,
2123 Any ,
22- Union ,
2324)
2425from urllib .parse import quote , unquote
2526
5556 NoSuchViewError ,
5657 TableAlreadyExistsError ,
5758 UnauthorizedError ,
59+ ViewAlreadyExistsError ,
5860)
5961from pyiceberg .io import (
6062 AWS_ACCESS_KEY_ID ,
8789from pyiceberg .types import transform_dict_value_to_str
8890from pyiceberg .utils .deprecated import deprecation_message
8991from pyiceberg .utils .properties import get_first_property_value , get_header_properties , property_as_bool , property_as_int
92+ from pyiceberg .view import View
93+ from pyiceberg .view .metadata import ViewMetadata , ViewVersion
9094
9195if TYPE_CHECKING :
9296 import pyarrow as pa
@@ -123,7 +127,7 @@ def __str__(self) -> str:
123127 return f"{ self .http_method .value } { self .path } "
124128
125129 @classmethod
126- def from_string (cls , endpoint : str ) -> " Endpoint" :
130+ def from_string (cls , endpoint : str ) -> Endpoint :
127131 elements = endpoint .strip ().split (None , 1 )
128132 if len (elements ) != 2 :
129133 raise ValueError (f"Invalid endpoint (must consist of two elements separated by a single space): { endpoint } " )
@@ -148,6 +152,7 @@ class Endpoints:
148152 get_token : str = "oauth/tokens"
149153 rename_table : str = "tables/rename"
150154 list_views : str = "namespaces/{namespace}/views"
155+ create_view : str = "namespaces/{namespace}/views"
151156 drop_view : str = "namespaces/{namespace}/views/{view}"
152157 view_exists : str = "namespaces/{namespace}/views/{view}"
153158 plan_table_scan : str = "namespaces/{namespace}/tables/{table}/plan"
@@ -275,6 +280,12 @@ class TableResponse(IcebergBaseModel):
275280 storage_credentials : list [StorageCredential ] = Field (alias = "storage-credentials" , default_factory = list )
276281
277282
283+ class ViewResponse (IcebergBaseModel ):
284+ metadata_location : str | None = Field (alias = "metadata-location" , default = None )
285+ metadata : ViewMetadata
286+ config : Properties = Field (default_factory = dict )
287+
288+
278289class CreateTableRequest (IcebergBaseModel ):
279290 name : str = Field ()
280291 location : str | None = Field ()
@@ -290,6 +301,18 @@ def transform_properties_dict_value_to_str(cls, properties: Properties) -> dict[
290301 return transform_dict_value_to_str (properties )
291302
292303
304+ class CreateViewRequest (IcebergBaseModel ):
305+ name : str = Field ()
306+ location : str | None = Field ()
307+ view_schema : Schema = Field (alias = "schema" )
308+ view_version : ViewVersion = Field (alias = "view-version" )
309+ properties : Properties = Field (default_factory = dict )
310+
311+ @field_validator ("properties" , mode = "before" )
312+ def transform_properties_dict_value_to_str (cls , properties : Properties ) -> dict [str , str ]:
313+ return transform_dict_value_to_str (properties )
314+
315+
293316class RegisterTableRequest (IcebergBaseModel ):
294317 name : str
295318 metadata_location : str = Field (..., alias = "metadata-location" )
@@ -800,6 +823,12 @@ def _response_to_staged_table(self, identifier_tuple: tuple[str, ...], table_res
800823 catalog = self ,
801824 )
802825
826+ def _response_to_view (self , identifier_tuple : tuple [str , ...], view_response : ViewResponse ) -> View :
827+ return View (
828+ identifier = identifier_tuple ,
829+ metadata = view_response .metadata ,
830+ )
831+
803832 def _refresh_token (self ) -> None :
804833 # Reactive token refresh is atypical - we should proactively refresh tokens in a separate thread
805834 # instead of retrying on Auth Exceptions. Keeping refresh behavior for the LegacyOAuth2AuthManager
@@ -819,7 +848,7 @@ def _config_headers(self, session: Session) -> None:
819848 def _create_table (
820849 self ,
821850 identifier : str | Identifier ,
822- schema : Union [ Schema , " pa.Schema" ] ,
851+ schema : Schema | pa .Schema ,
823852 location : str | None = None ,
824853 partition_spec : PartitionSpec = UNPARTITIONED_PARTITION_SPEC ,
825854 sort_order : SortOrder = UNSORTED_SORT_ORDER ,
@@ -862,7 +891,7 @@ def _create_table(
862891 def create_table (
863892 self ,
864893 identifier : str | Identifier ,
865- schema : Union [ Schema , " pa.Schema" ] ,
894+ schema : Schema | pa .Schema ,
866895 location : str | None = None ,
867896 partition_spec : PartitionSpec = UNPARTITIONED_PARTITION_SPEC ,
868897 sort_order : SortOrder = UNSORTED_SORT_ORDER ,
@@ -883,7 +912,7 @@ def create_table(
883912 def create_table_transaction (
884913 self ,
885914 identifier : str | Identifier ,
886- schema : Union [ Schema , " pa.Schema" ] ,
915+ schema : Schema | pa .Schema ,
887916 location : str | None = None ,
888917 partition_spec : PartitionSpec = UNPARTITIONED_PARTITION_SPEC ,
889918 sort_order : SortOrder = UNSORTED_SORT_ORDER ,
@@ -901,6 +930,44 @@ def create_table_transaction(
901930 staged_table = self ._response_to_staged_table (self .identifier_to_tuple (identifier ), table_response )
902931 return CreateTableTransaction (staged_table )
903932
933+ @retry (** _RETRY_ARGS )
934+ def create_view (
935+ self ,
936+ identifier : str | Identifier ,
937+ schema : Schema | pa .Schema ,
938+ view_version : ViewVersion ,
939+ location : str | None = None ,
940+ properties : Properties = EMPTY_DICT ,
941+ ) -> View :
942+ iceberg_schema = self ._convert_schema_if_needed (schema )
943+ fresh_schema = assign_fresh_schema_ids (iceberg_schema )
944+
945+ namespace_and_view = self ._split_identifier_for_path (identifier , IdentifierKind .VIEW )
946+ if location :
947+ location = location .rstrip ("/" )
948+
949+ request = CreateViewRequest (
950+ name = namespace_and_view ["view" ],
951+ location = location ,
952+ view_schema = fresh_schema ,
953+ view_version = view_version ,
954+ properties = properties ,
955+ )
956+
957+ serialized_json = request .model_dump_json ().encode (UTF8 )
958+ response = self ._session .post (
959+ self .url (Endpoints .create_view , namespace = namespace_and_view ["namespace" ]),
960+ data = serialized_json ,
961+ )
962+
963+ try :
964+ response .raise_for_status ()
965+ except HTTPError as exc :
966+ _handle_non_200_response (exc , {409 : ViewAlreadyExistsError })
967+
968+ view_response = ViewResponse .model_validate_json (response .text )
969+ return self ._response_to_view (self .identifier_to_tuple (identifier ), view_response )
970+
904971 @retry (** _RETRY_ARGS )
905972 def register_table (self , identifier : str | Identifier , metadata_location : str ) -> Table :
906973 """Register a new table using existing metadata.
0 commit comments