Skip to content

Commit b4ea1b7

Browse files
committed
make validator reusable
1 parent 44591bc commit b4ea1b7

File tree

4 files changed

+21
-13
lines changed

4 files changed

+21
-13
lines changed

pyiceberg/catalog/rest.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
Union,
2929
)
3030

31-
from pydantic import Field, ValidationError
31+
from pydantic import Field, ValidationError, field_validator
3232
from requests import HTTPError, Session
3333
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
3434

@@ -69,6 +69,7 @@
6969
)
7070
from pyiceberg.table.sorting import UNSORTED_SORT_ORDER, SortOrder, assign_fresh_sort_order_ids
7171
from pyiceberg.typedef import EMPTY_DICT, UTF8, IcebergBaseModel
72+
from pyiceberg.types import transform_dict_value_to_str
7273

7374
if TYPE_CHECKING:
7475
import pyarrow as pa
@@ -146,6 +147,8 @@ class CreateTableRequest(IcebergBaseModel):
146147
write_order: Optional[SortOrder] = Field(alias="write-order")
147148
stage_create: bool = Field(alias="stage-create", default=False)
148149
properties: Properties = Field(default_factory=dict)
150+
# validators
151+
transform_properties_dict_value_to_str = field_validator('properties', mode='before')(transform_dict_value_to_str)
149152

150153

151154
class RegisterTableRequest(IcebergBaseModel):

pyiceberg/table/metadata.py

+4-9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
IcebergRootModel,
5050
Properties,
5151
)
52+
from pyiceberg.types import transform_dict_value_to_str
5253
from pyiceberg.utils.datetime import datetime_to_millis
5354

5455
CURRENT_SNAPSHOT_ID = "current-snapshot-id"
@@ -178,15 +179,6 @@ class TableMetadataCommonFields(IcebergBaseModel):
178179
to be used for arbitrary metadata. For example, commit.retry.num-retries
179180
is used to control the number of commit retries."""
180181

181-
@field_validator("properties", mode='before')
182-
@classmethod
183-
def transform_dict_value_to_str(cls, dict: Dict[str, Any]) -> Dict[str, str]:
184-
"""Transform all values in the dictionary to string. Raise an error if any value is None."""
185-
for value in dict.values():
186-
if value is None:
187-
raise ValueError("None type is not a supported value in properties")
188-
return {k: str(v) for k, v in dict.items()}
189-
190182
current_snapshot_id: Optional[int] = Field(alias="current-snapshot-id", default=None)
191183
"""ID of the current table snapshot."""
192184

@@ -235,6 +227,9 @@ def schema_by_id(self, schema_id: int) -> Optional[Schema]:
235227
"""Get the schema by schema_id."""
236228
return next((schema for schema in self.schemas if schema.schema_id == schema_id), None)
237229

230+
# validators
231+
transform_properties_dict_value_to_str = field_validator('properties', mode='before')(transform_dict_value_to_str)
232+
238233

239234
class TableMetadataV1(TableMetadataCommonFields, IcebergBaseModel):
240235
"""Represents version 1 of the Table Metadata.

pyiceberg/types.py

+9
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from typing import (
3838
Any,
3939
ClassVar,
40+
Dict,
4041
Literal,
4142
Optional,
4243
Tuple,
@@ -61,6 +62,14 @@
6162
FIXED_PARSER = ParseNumberFromBrackets(FIXED)
6263

6364

65+
def transform_dict_value_to_str(dict: Dict[str, Any]) -> Dict[str, str]:
66+
"""Transform all values in the dictionary to string. Raise an error if any value is None."""
67+
for value in dict.values():
68+
if value is None:
69+
raise ValueError("None type is not a supported value in properties")
70+
return {k: str(v) for k, v in dict.items()}
71+
72+
6473
def _parse_decimal_type(decimal: Any) -> Tuple[int, int]:
6574
if isinstance(decimal, str):
6675
matches = DECIMAL_REGEX.search(decimal)

tests/integration/test_writes.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@
2828
import pytest
2929
import pytz
3030
from pyarrow.fs import S3FileSystem
31+
from pydantic_core import ValidationError
3132
from pyspark.sql import SparkSession
3233
from pytest_mock.plugin import MockerFixture
3334

3435
from pyiceberg.catalog import Catalog, Properties, Table, load_catalog
3536
from pyiceberg.catalog.sql import SqlCatalog
36-
from pyiceberg.exceptions import NamespaceAlreadyExistsError, NoSuchTableError, ServerError
37+
from pyiceberg.exceptions import NamespaceAlreadyExistsError, NoSuchTableError
3738
from pyiceberg.schema import Schema
3839
from pyiceberg.table import _dataframe_to_data_files
3940
from pyiceberg.types import (
@@ -705,8 +706,8 @@ def test_table_properties_raise_for_none_value(
705706
property_with_none = {"property_name": None}
706707
identifier = "default.test_table_properties_raise_for_none_value"
707708

708-
with pytest.raises(ServerError) as exc_info:
709+
with pytest.raises(ValidationError) as exc_info:
709710
_ = _create_table(
710711
session_catalog, identifier, {"format-version": format_version, **property_with_none}, [arrow_table_with_null]
711712
)
712-
assert "NullPointerException: null value in entry: property_name=null" in str(exc_info.value)
713+
assert "None type is not a supported value in properties" in str(exc_info.value)

0 commit comments

Comments
 (0)