From a7d8b4602786bfbb3098212d00d84ca09e360833 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Fri, 23 Jan 2026 09:53:49 -0800 Subject: [PATCH 01/18] add default value field --- protos/feast/core/Feature.proto | 3 + protos/feast/serving/ServingService.proto | 14 +++++ sdk/python/feast/field.py | 69 ++++++++++++++++++++--- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/protos/feast/core/Feature.proto b/protos/feast/core/Feature.proto index 9f7708c65e7..472df8f25b8 100644 --- a/protos/feast/core/Feature.proto +++ b/protos/feast/core/Feature.proto @@ -45,4 +45,7 @@ message FeatureSpecV2 { // Field indicating the vector length int32 vector_length = 7; + + // Default value to be used for the feature when its value is missing/expired. + feast.types.Value default_value = 8; } diff --git a/protos/feast/serving/ServingService.proto b/protos/feast/serving/ServingService.proto index ebadeb6f7ff..36af202d23a 100644 --- a/protos/feast/serving/ServingService.proto +++ b/protos/feast/serving/ServingService.proto @@ -108,6 +108,16 @@ message GetOnlineFeaturesRequest { // Whether to include the timestamp/status metadata in the response bool include_metadata = 10; + + // Mode for handling features with default values when feature value is missing + UseDefaultsMode use_defaults = 11; +} + +enum UseDefaultsMode { + USE_DEFAULTS_UNSPECIFIED = 0; // Field not set - use server default behavior (currently OFF) + USE_DEFAULTS_OFF = 1; // Explicitly disable default replacement + USE_DEFAULTS_FLEXIBLE = 2; // Ignore if default missing + USE_DEFAULTS_STRICT = 3; // Fail if default is missing } message GetOnlineFeaturesResponse { @@ -200,6 +210,10 @@ message GetOnlineFeaturesRangeRequest { // Whether to include the timestamp and status metadata in the response bool include_metadata = 9; + + // Mode for handling features with default values when feature value is missing + UseDefaultsMode use_defaults = 11; + } message GetOnlineFeaturesRangeResponse { diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index d03a5ccdaac..5b69dfd669d 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -21,6 +21,7 @@ from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType +from feast.protos.feast.types import Value_pb2 as ValueProto @typechecked @@ -47,6 +48,20 @@ class Field(BaseModel): vector_index: bool = False vector_length: int = 0 vector_search_metric: Optional[str] = "" + default_value: Optional[ValueProto.Value] = None + + @field_validator("default_value") + def validate_default_value_type(cls, v, info): + """Validate that default_value matches dtype""" + if v is not None: + dtype = info.data.get('dtype') + if dtype is not None: + value_type = dtype.to_value_type() + if not _is_value_compatible_with_type(v, value_type): + raise ValueError( + f"default_value type does not match dtype {dtype}" + ) + return v @field_validator("dtype", mode="before") def dtype_is_feasttype_or_string_feasttype(cls, v): @@ -110,7 +125,7 @@ def to_proto(self) -> FieldProto: """Converts a Field object to its protobuf representation.""" value_type = self.dtype.to_value_type() vector_search_metric = self.vector_search_metric or "" - return FieldProto( + proto = FieldProto( name=self.name, value_type=value_type.value, description=self.description, @@ -119,6 +134,10 @@ def to_proto(self) -> FieldProto: vector_length=self.vector_length, vector_search_metric=vector_search_metric, ) + if self.default_value is not None: + proto.default_value.CopyFrom(self.default_value) + + return proto @classmethod def from_proto(cls, field_proto: FieldProto): @@ -132,15 +151,19 @@ def from_proto(cls, field_proto: FieldProto): vector_search_metric = getattr(field_proto, "vector_search_metric", "") vector_index = getattr(field_proto, "vector_index", False) vector_length = getattr(field_proto, "vector_length", 0) + default_value = None + if field_proto.HasField("default_value"): + default_value = field_proto.default_value return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): @@ -156,3 +179,31 @@ def from_feature(cls, feature: Feature): description=feature.description, tags=feature.labels, ) + + def _is_value_compatible_with_type( + value: ValueProto.Value, + value_type: ValueType + ) -> bool: + """Check if a Value proto matches a ValueType""" + val_case = value.WhichOneof('val') + + type_mapping = { + 'int32_val': ValueType.INT32, + 'int64_val': ValueType.INT64, + 'double_val': ValueType.DOUBLE, + 'float_val': ValueType.FLOAT, + 'string_val': ValueType.STRING, + 'bytes_val': ValueType.BYTES, + 'bool_val': ValueType.BOOL, + 'int32_list_val': ValueType.INT32_LIST, + 'int64_list_val': ValueType.INT64_LIST, + 'double_list_val': ValueType.DOUBLE_LIST, + 'float_list_val': ValueType.FLOAT_LIST, + 'string_list_val': ValueType.STRING_LIST, + 'bytes_list_val': ValueType.BYTES_LIST, + 'bool_list_val': ValueType.BOOL_LIST, + 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, + 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + } + + return type_mapping.get(val_case) == value_type From 789aa142d2d99aec919798817d4707d564632044 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Sat, 24 Jan 2026 10:53:49 -0800 Subject: [PATCH 02/18] fixing mypy issues --- sdk/python/feast/field.py | 122 +++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index 5b69dfd669d..a8678a4311c 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from pydantic import BaseModel, ConfigDict, field_validator from typeguard import check_type, typechecked @@ -21,7 +21,9 @@ from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType -from feast.protos.feast.types import Value_pb2 as ValueProto + +if TYPE_CHECKING: + from feast.protos.feast.types import Value_pb2 as ValueProto @typechecked @@ -51,16 +53,55 @@ class Field(BaseModel): default_value: Optional[ValueProto.Value] = None @field_validator("default_value") - def validate_default_value_type(cls, v, info): - """Validate that default_value matches dtype""" - if v is not None: - dtype = info.data.get('dtype') - if dtype is not None: - value_type = dtype.to_value_type() - if not _is_value_compatible_with_type(v, value_type): - raise ValueError( - f"default_value type does not match dtype {dtype}" - ) + @classmethod + def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) -> Optional["ValueProto.Value"]: + """ + Validate that default_value type matches the field's dtype. + """ + if v is None: + return v + + # Get dtype from the model data + dtype = info.data.get('dtype') + if dtype is None: + # dtype will be validated by its own validator, skip for now + return v + + # Validate type compatibility + value_type = dtype.to_value_type() + val_case = v.WhichOneof('val') + + if val_case is None: + # Empty Value proto + return v + + # Map proto value types to ValueType enums + type_mapping: Dict[str, ValueType] = { + 'int32_val': ValueType.INT32, + 'int64_val': ValueType.INT64, + 'double_val': ValueType.DOUBLE, + 'float_val': ValueType.FLOAT, + 'string_val': ValueType.STRING, + 'bytes_val': ValueType.BYTES, + 'bool_val': ValueType.BOOL, + 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, + 'int32_list_val': ValueType.INT32_LIST, + 'int64_list_val': ValueType.INT64_LIST, + 'double_list_val': ValueType.DOUBLE_LIST, + 'float_list_val': ValueType.FLOAT_LIST, + 'string_list_val': ValueType.STRING_LIST, + 'bytes_list_val': ValueType.BYTES_LIST, + 'bool_list_val': ValueType.BOOL_LIST, + 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + } + + expected_type = type_mapping.get(val_case) + if expected_type != value_type: + raise ValueError( + f"default_value type '{val_case}' does not match field dtype '{dtype}' " + f"(expected ValueType.{value_type.name})" + ) + return v @field_validator("dtype", mode="before") @@ -134,8 +175,9 @@ def to_proto(self) -> FieldProto: vector_length=self.vector_length, vector_search_metric=vector_search_metric, ) + # Add default_value if present (using type: ignore until proto is regenerated) if self.default_value is not None: - proto.default_value.CopyFrom(self.default_value) + proto.default_value.CopyFrom(self.default_value) # type: ignore[attr-defined] return proto @@ -151,19 +193,21 @@ def from_proto(cls, field_proto: FieldProto): vector_search_metric = getattr(field_proto, "vector_search_metric", "") vector_index = getattr(field_proto, "vector_index", False) vector_length = getattr(field_proto, "vector_length", 0) - default_value = None - if field_proto.HasField("default_value"): - default_value = field_proto.default_value + # Extract default_value if present + default_value = getattr(field_proto, "default_value", None) + if default_value is not None and not default_value.WhichOneof("val"): + # Empty Value proto, treat as None + default_value = None return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - default_value=default_value, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): @@ -179,31 +223,3 @@ def from_feature(cls, feature: Feature): description=feature.description, tags=feature.labels, ) - - def _is_value_compatible_with_type( - value: ValueProto.Value, - value_type: ValueType - ) -> bool: - """Check if a Value proto matches a ValueType""" - val_case = value.WhichOneof('val') - - type_mapping = { - 'int32_val': ValueType.INT32, - 'int64_val': ValueType.INT64, - 'double_val': ValueType.DOUBLE, - 'float_val': ValueType.FLOAT, - 'string_val': ValueType.STRING, - 'bytes_val': ValueType.BYTES, - 'bool_val': ValueType.BOOL, - 'int32_list_val': ValueType.INT32_LIST, - 'int64_list_val': ValueType.INT64_LIST, - 'double_list_val': ValueType.DOUBLE_LIST, - 'float_list_val': ValueType.FLOAT_LIST, - 'string_list_val': ValueType.STRING_LIST, - 'bytes_list_val': ValueType.BYTES_LIST, - 'bool_list_val': ValueType.BOOL_LIST, - 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, - 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, - } - - return type_mapping.get(val_case) == value_type From 46651f199b7193bf446009e98aacaa26fdf131c0 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Sat, 24 Jan 2026 22:37:55 -0800 Subject: [PATCH 03/18] fixed formatting --- sdk/python/feast/field.py | 64 +++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index a8678a4311c..e2bf5dda37b 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -12,19 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, field_validator from typeguard import check_type, typechecked from feast.feature import Feature from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType -if TYPE_CHECKING: - from feast.protos.feast.types import Value_pb2 as ValueProto - @typechecked class Field(BaseModel): @@ -54,7 +52,9 @@ class Field(BaseModel): @field_validator("default_value") @classmethod - def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) -> Optional["ValueProto.Value"]: + def validate_default_value_type( + cls, v: Optional[ValueProto.Value], info: Any + ) -> Optional[ValueProto.Value]: """ Validate that default_value type matches the field's dtype. """ @@ -62,14 +62,14 @@ def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) return v # Get dtype from the model data - dtype = info.data.get('dtype') + dtype = info.data.get("dtype") if dtype is None: # dtype will be validated by its own validator, skip for now return v # Validate type compatibility value_type = dtype.to_value_type() - val_case = v.WhichOneof('val') + val_case = v.WhichOneof("val") if val_case is None: # Empty Value proto @@ -77,22 +77,22 @@ def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) # Map proto value types to ValueType enums type_mapping: Dict[str, ValueType] = { - 'int32_val': ValueType.INT32, - 'int64_val': ValueType.INT64, - 'double_val': ValueType.DOUBLE, - 'float_val': ValueType.FLOAT, - 'string_val': ValueType.STRING, - 'bytes_val': ValueType.BYTES, - 'bool_val': ValueType.BOOL, - 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, - 'int32_list_val': ValueType.INT32_LIST, - 'int64_list_val': ValueType.INT64_LIST, - 'double_list_val': ValueType.DOUBLE_LIST, - 'float_list_val': ValueType.FLOAT_LIST, - 'string_list_val': ValueType.STRING_LIST, - 'bytes_list_val': ValueType.BYTES_LIST, - 'bool_list_val': ValueType.BOOL_LIST, - 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + "int32_val": ValueType.INT32, + "int64_val": ValueType.INT64, + "double_val": ValueType.DOUBLE, + "float_val": ValueType.FLOAT, + "string_val": ValueType.STRING, + "bytes_val": ValueType.BYTES, + "bool_val": ValueType.BOOL, + "unix_timestamp_val": ValueType.UNIX_TIMESTAMP, + "int32_list_val": ValueType.INT32_LIST, + "int64_list_val": ValueType.INT64_LIST, + "double_list_val": ValueType.DOUBLE_LIST, + "float_list_val": ValueType.FLOAT_LIST, + "string_list_val": ValueType.STRING_LIST, + "bytes_list_val": ValueType.BYTES_LIST, + "bool_list_val": ValueType.BOOL_LIST, + "unix_timestamp_list_val": ValueType.UNIX_TIMESTAMP_LIST, } expected_type = type_mapping.get(val_case) @@ -199,15 +199,15 @@ def from_proto(cls, field_proto: FieldProto): # Empty Value proto, treat as None default_value = None return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - default_value=default_value, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): From cefb1e1afa5231868eb2ec7948ac2079967fbde0 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 13:26:39 -0800 Subject: [PATCH 04/18] updated pydantic models --- .../expediagroup/pydantic_models/field_model.py | 4 ++++ sdk/python/feast/feature.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/expediagroup/pydantic_models/field_model.py b/sdk/python/feast/expediagroup/pydantic_models/field_model.py index b30c5862755..31fc6be6ca8 100644 --- a/sdk/python/feast/expediagroup/pydantic_models/field_model.py +++ b/sdk/python/feast/expediagroup/pydantic_models/field_model.py @@ -4,6 +4,7 @@ from typing_extensions import Self from feast.field import Field +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.types import Array, PrimitiveFeastType @@ -19,6 +20,7 @@ class FieldModel(BaseModel): vector_index: bool = False vector_length: int = 0 vector_search_metric: Optional[str] = None + default_value: Optional[ValueProto.Value] = None def to_field(self) -> Field: """ @@ -35,6 +37,7 @@ def to_field(self) -> Field: vector_index=self.vector_index, vector_length=self.vector_length, vector_search_metric=self.vector_search_metric, + default_value=self.default_value, ) @classmethod @@ -56,4 +59,5 @@ def from_field( vector_index=field.vector_index, vector_length=field.vector_length, vector_search_metric=field.vector_search_metric, + default_value=field.default_value, ) diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index db629d677a8..017aff1faad 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -15,6 +15,7 @@ from typing import Dict, Optional from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FeatureSpecProto +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.protos.feast.types.Value_pb2 import ValueType as ValueTypeProto from feast.value_type import ValueType @@ -35,6 +36,7 @@ def __init__( dtype: ValueType, description: str = "", labels: Optional[Dict[str, str]] = None, + default_value: Optional[ValueProto.Value] = None, ): """Creates a Feature object.""" self._name = name @@ -48,6 +50,7 @@ def __init__( self._labels = dict() else: self._labels = labels + self._default_value = default_value def __eq__(self, other): if self.name != other.name or self.dtype != other.dtype: @@ -64,6 +67,7 @@ def __repr__(self): f" dtype={self._dtype!r},\n" f" description={self._description!r},\n" f" labels={self._labels!r}\n" + f" default_value={self._default_value!r}\n" f")" ) @@ -108,12 +112,15 @@ def to_proto(self) -> FeatureSpecProto: """ value_type = ValueTypeProto.Enum.Value(self.dtype.name) - return FeatureSpecProto( + proto = FeatureSpecProto( name=self.name, value_type=value_type, description=self.description, tags=self.labels, ) + if self.default_value is not None: + proto.default_value.CopyFrom(self.default_value) # type: ignore[attr-defined] + return proto @classmethod def from_proto(cls, feature_proto: FeatureSpecProto): @@ -124,11 +131,16 @@ def from_proto(cls, feature_proto: FeatureSpecProto): Returns: Feature object """ + default_value = getattr(feature_proto, "default_value", None) + if default_value is not None and not default_value.WhichOneof("val"): + # Empty Value proto, treat as None + default_value = None feature = cls( name=feature_proto.name, dtype=ValueType(feature_proto.value_type), description=feature_proto.description, labels=dict(feature_proto.tags), + default_value=default_value, ) return feature From 2a8317f9089ab32fa901387a73aa0700a090c61f Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 13:39:00 -0800 Subject: [PATCH 05/18] add missing property --- sdk/python/feast/feature.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index 017aff1faad..320843f581e 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -103,6 +103,13 @@ def labels(self) -> Dict[str, str]: """ return self._labels + @property + def default_value(self) -> Dict[str, str]: + """ + Gets the default value of this feature. + """ + return self._default_value + def to_proto(self) -> FeatureSpecProto: """ Converts Feature object to its Protocol Buffer representation. From 4d75ee0e321d020550eefa688ed4c6b1588d57f8 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 16:20:36 -0800 Subject: [PATCH 06/18] fix test failueres --- sdk/python/feast/expediagroup/pydantic_models/field_model.py | 4 +++- sdk/python/feast/feature.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/expediagroup/pydantic_models/field_model.py b/sdk/python/feast/expediagroup/pydantic_models/field_model.py index 31fc6be6ca8..f5f2e9c09db 100644 --- a/sdk/python/feast/expediagroup/pydantic_models/field_model.py +++ b/sdk/python/feast/expediagroup/pydantic_models/field_model.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from typing_extensions import Self from feast.field import Field @@ -22,6 +22,8 @@ class FieldModel(BaseModel): vector_search_metric: Optional[str] = None default_value: Optional[ValueProto.Value] = None + model_config = ConfigDict(arbitrary_types_allowed=True) + def to_field(self) -> Field: """ Given a Pydantic FieldModel, create and return a Field. diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index 320843f581e..15904474df6 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -104,7 +104,7 @@ def labels(self) -> Dict[str, str]: return self._labels @property - def default_value(self) -> Dict[str, str]: + def default_value(self) -> Optional[ValueProto.Value]: """ Gets the default value of this feature. """ From e022c03eb971e87c74da39d3189df1d1bf5232ee Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Tue, 27 Jan 2026 16:36:22 -0800 Subject: [PATCH 07/18] added unit tests --- sdk/python/feast/field.py | 1 + sdk/python/tests/unit/test_feature.py | 156 +++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index e2bf5dda37b..d14d2d55c68 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -222,4 +222,5 @@ def from_feature(cls, feature: Feature): dtype=from_value_type(feature.dtype), description=feature.description, tags=feature.labels, + default_value=feature.default_value, ) diff --git a/sdk/python/tests/unit/test_feature.py b/sdk/python/tests/unit/test_feature.py index ca0dce44457..08c51e050cc 100644 --- a/sdk/python/tests/unit/test_feature.py +++ b/sdk/python/tests/unit/test_feature.py @@ -1,5 +1,9 @@ +import pytest +from pydantic_core import ValidationError + from feast.field import Feature, Field -from feast.types import Float32 +from feast.protos.feast.types import Value_pb2 as ValueProto +from feast.types import Array, Bool, Float32, Int32, Int64, String from feast.value_type import ValueType @@ -9,7 +13,6 @@ def test_feature_serialization_with_description(): name="avg_daily_trips", dtype=ValueType.FLOAT, description=expected_description ) serialized_feature = feature.to_proto() - assert serialized_feature.description == expected_description @@ -21,12 +24,155 @@ def test_field_serialization_with_description(): feature = Feature( name="avg_daily_trips", dtype=ValueType.FLOAT, description=expected_description ) - serialized_field = field.to_proto() field_from_feature = Field.from_feature(feature) - assert serialized_field.description == expected_description assert field_from_feature.description == expected_description - field = Field.from_proto(serialized_field) assert field.description == expected_description + + +def test_field_with_default_value_to_proto(): + default_val = ValueProto.Value(int32_val=42) + field = Field(name="age", dtype=Int32, default_value=default_val) + proto = field.to_proto() + assert proto.name == "age" + assert proto.HasField("default_value") + assert proto.default_value.int32_val == 42 + + +def test_field_without_default_value_to_proto(): + field = Field(name="age", dtype=Int32) + proto = field.to_proto() + assert proto.name == "age" + assert not proto.HasField("default_value") + + +def test_field_from_proto_with_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + default_val = ValueProto.Value(string_val="unknown") + proto = FeatureSpecV2( + name="country", + value_type=2, # STRING + default_value=default_val, + ) + field = Field.from_proto(proto) + assert field.name == "country" + assert field.default_value is not None + assert field.default_value.string_val == "unknown" + + +def test_field_from_proto_without_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + proto = FeatureSpecV2(name="country", value_type=2) + field = Field.from_proto(proto) + assert field.name == "country" + assert field.default_value is None + + +def test_field_roundtrip_with_default_value(): + default_val = ValueProto.Value(int64_val=9999) + original_field = Field(name="user_id", dtype=Int64, default_value=default_val) + proto = original_field.to_proto() + restored_field = Field.from_proto(proto) + assert restored_field.name == original_field.name + assert restored_field.dtype == original_field.dtype + assert restored_field.default_value.int64_val == 9999 + + +def test_field_default_value_type_validation(): + with pytest.raises(ValidationError, match="does not match field dtype"): + default_val = ValueProto.Value(string_val="not_an_int") + Field(name="age", dtype=Int32, default_value=default_val) + + +def test_field_with_list_default_value(): + default_val = ValueProto.Value(int32_list_val=ValueProto.Int32List(val=[1, 2, 3])) + field = Field(name="scores", dtype=Array(Int32), default_value=default_val) + assert list(field.default_value.int32_list_val.val) == [1, 2, 3] + + +def test_feature_with_default_value_to_proto(): + default_val = ValueProto.Value(int32_val=0) + feature = Feature(name="count", dtype=ValueType.INT32, default_value=default_val) + proto = feature.to_proto() + assert proto.name == "count" + assert proto.HasField("default_value") + assert proto.default_value.int32_val == 0 + + +def test_feature_without_default_value_to_proto(): + feature = Feature(name="count", dtype=ValueType.INT32) + proto = feature.to_proto() + assert proto.name == "count" + assert not proto.HasField("default_value") + + +def test_feature_from_proto_with_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + default_val = ValueProto.Value(bool_val=False) + proto = FeatureSpecV2( + name="is_active", + value_type=7, # BOOL + default_value=default_val, + ) + feature = Feature.from_proto(proto) + assert feature.name == "is_active" + assert feature.default_value is not None + assert feature.default_value.bool_val is False + + +def test_feature_from_proto_without_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + proto = FeatureSpecV2(name="is_active", value_type=7) + feature = Feature.from_proto(proto) + assert feature.name == "is_active" + assert feature.default_value is None + + +def test_feature_roundtrip_with_default_value(): + default_val = ValueProto.Value(string_val="US") + original_feature = Feature( + name="country", dtype=ValueType.STRING, default_value=default_val + ) + proto = original_feature.to_proto() + restored_feature = Feature.from_proto(proto) + assert restored_feature.name == original_feature.name + assert restored_feature.dtype == original_feature.dtype + assert restored_feature.default_value.string_val == "US" + + +def test_backward_compatibility_field_from_feature(): + default_val = ValueProto.Value(int32_val=18) + feature = Feature(name="age", dtype=ValueType.INT32, default_value=default_val) + field = Field.from_feature(feature) + assert field.name == "age" + assert field.default_value is not None + assert field.default_value.int32_val == 18 + + +def test_field_default_value_edge_cases(): + # Zero value + field1 = Field( + name="count", dtype=Int32, default_value=ValueProto.Value(int32_val=0) + ) + assert field1.default_value.int32_val == 0 + # Empty string + field2 = Field( + name="name", dtype=String, default_value=ValueProto.Value(string_val="") + ) + assert field2.default_value.string_val == "" + # False boolean + field3 = Field( + name="flag", dtype=Bool, default_value=ValueProto.Value(bool_val=False) + ) + assert field3.default_value.bool_val is False + # Negative number + field4 = Field( + name="error", dtype=Int64, default_value=ValueProto.Value(int64_val=-1) + ) + assert field4.default_value.int64_val == -1 From 377d8b06d2adf752ed658f2826183459e069de19 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Tue, 27 Jan 2026 18:56:28 -0800 Subject: [PATCH 08/18] adding compiled protos --- .../feast/protos/feast/core/Feature_pb2.py | 8 +-- .../feast/serving/ServingService_pb2.py | 70 ++++++++++--------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.py b/sdk/python/feast/protos/feast/core/Feature_pb2.py index a02bb7ff403..c7abf76d66e 100644 --- a/sdk/python/feast/protos/feast/core/Feature_pb2.py +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.py @@ -15,7 +15,7 @@ from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Feature.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\"\x8e\x02\n\rFeatureSpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x31\n\x04tags\x18\x03 \x03(\x0b\x32#.feast.core.FeatureSpecV2.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x14\n\x0cvector_index\x18\x05 \x01(\x08\x12\x1c\n\x14vector_search_metric\x18\x06 \x01(\t\x12\x15\n\rvector_length\x18\x07 \x01(\x05\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x10\x66\x65\x61st.proto.coreB\x0c\x46\x65\x61tureProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Feature.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\"\xb9\x02\n\rFeatureSpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x31\n\x04tags\x18\x03 \x03(\x0b\x32#.feast.core.FeatureSpecV2.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x14\n\x0cvector_index\x18\x05 \x01(\x08\x12\x1c\n\x14vector_search_metric\x18\x06 \x01(\t\x12\x15\n\rvector_length\x18\x07 \x01(\x05\x12)\n\rdefault_value\x18\x08 \x01(\x0b\x32\x12.feast.types.Value\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x10\x66\x65\x61st.proto.coreB\x0c\x46\x65\x61tureProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -26,7 +26,7 @@ _globals['_FEATURESPECV2_TAGSENTRY']._options = None _globals['_FEATURESPECV2_TAGSENTRY']._serialized_options = b'8\001' _globals['_FEATURESPECV2']._serialized_start=66 - _globals['_FEATURESPECV2']._serialized_end=336 - _globals['_FEATURESPECV2_TAGSENTRY']._serialized_start=293 - _globals['_FEATURESPECV2_TAGSENTRY']._serialized_end=336 + _globals['_FEATURESPECV2']._serialized_end=379 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_start=336 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_end=379 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py index 12c00e856b2..85e467ca447 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x17\n\x15GetVersionInfoRequest\"{\n\x16GetVersionInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\nbuild_time\x18\x02 \x01(\t\x12\x13\n\x0b\x63ommit_hash\x18\x03 \x01(\t\x12\x12\n\ngo_version\x18\x04 \x01(\t\x12\x13\n\x0bserver_type\x18\x05 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\xe2\x03\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\n \x01(\x08\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList\"A\n\x13RepeatedFieldStatus\x12*\n\x06status\x18\x01 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\"\x9e\x02\n\rSortKeyFilter\x12\x15\n\rsort_key_name\x18\x01 \x01(\t\x12\x38\n\x05range\x18\x02 \x01(\x0b\x32\'.feast.serving.SortKeyFilter.RangeQueryH\x00\x12$\n\x06\x65quals\x18\x03 \x01(\x0b\x32\x12.feast.types.ValueH\x00\x1a\x8c\x01\n\nRangeQuery\x12\'\n\x0brange_start\x18\x02 \x01(\x0b\x32\x12.feast.types.Value\x12%\n\trange_end\x18\x03 \x01(\x0b\x32\x12.feast.types.Value\x12\x17\n\x0fstart_inclusive\x18\x04 \x01(\x08\x12\x15\n\rend_inclusive\x18\x05 \x01(\x08\x42\x07\n\x05query\"\xd4\x04\n\x1dGetOnlineFeaturesRangeRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12L\n\x08\x65ntities\x18\x03 \x03(\x0b\x32:.feast.serving.GetOnlineFeaturesRangeRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12\x36\n\x10sort_key_filters\x18\x05 \x03(\x0b\x32\x1c.feast.serving.SortKeyFilter\x12\x1a\n\x12reverse_sort_order\x18\x06 \x01(\x08\x12\r\n\x05limit\x18\x07 \x01(\x05\x12Y\n\x0frequest_context\x18\x08 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\t \x01(\x08\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\x82\x04\n\x1eGetOnlineFeaturesRangeResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12M\n\x08\x65ntities\x18\x02 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRangeResponse.EntitiesEntry\x12Q\n\x07results\x18\x03 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeResponse.RangeFeatureVector\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1a\xac\x01\n\x12RangeFeatureVector\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.feast.types.RepeatedValue\x12\x34\n\x08statuses\x18\x02 \x03(\x0b\x32\".feast.serving.RepeatedFieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.feast.types.RepeatedValue*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xbc\x03\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12]\n\x0eGetVersionInfo\x12$.feast.serving.GetVersionInfoRequest\x1a%.feast.serving.GetVersionInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponse\x12u\n\x16GetOnlineFeaturesRange\x12,.feast.serving.GetOnlineFeaturesRangeRequest\x1a-.feast.serving.GetOnlineFeaturesRangeResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x17\n\x15GetVersionInfoRequest\"{\n\x16GetVersionInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\nbuild_time\x18\x02 \x01(\t\x12\x13\n\x0b\x63ommit_hash\x18\x03 \x01(\t\x12\x12\n\ngo_version\x18\x04 \x01(\t\x12\x13\n\x0bserver_type\x18\x05 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x98\x04\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\n \x01(\x08\x12\x34\n\x0cuse_defaults\x18\x0b \x01(\x0e\x32\x1e.feast.serving.UseDefaultsMode\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList\"A\n\x13RepeatedFieldStatus\x12*\n\x06status\x18\x01 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\"\x9e\x02\n\rSortKeyFilter\x12\x15\n\rsort_key_name\x18\x01 \x01(\t\x12\x38\n\x05range\x18\x02 \x01(\x0b\x32\'.feast.serving.SortKeyFilter.RangeQueryH\x00\x12$\n\x06\x65quals\x18\x03 \x01(\x0b\x32\x12.feast.types.ValueH\x00\x1a\x8c\x01\n\nRangeQuery\x12\'\n\x0brange_start\x18\x02 \x01(\x0b\x32\x12.feast.types.Value\x12%\n\trange_end\x18\x03 \x01(\x0b\x32\x12.feast.types.Value\x12\x17\n\x0fstart_inclusive\x18\x04 \x01(\x08\x12\x15\n\rend_inclusive\x18\x05 \x01(\x08\x42\x07\n\x05query\"\x8a\x05\n\x1dGetOnlineFeaturesRangeRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12L\n\x08\x65ntities\x18\x03 \x03(\x0b\x32:.feast.serving.GetOnlineFeaturesRangeRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12\x36\n\x10sort_key_filters\x18\x05 \x03(\x0b\x32\x1c.feast.serving.SortKeyFilter\x12\x1a\n\x12reverse_sort_order\x18\x06 \x01(\x08\x12\r\n\x05limit\x18\x07 \x01(\x05\x12Y\n\x0frequest_context\x18\x08 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\t \x01(\x08\x12\x34\n\x0cuse_defaults\x18\x0b \x01(\x0e\x32\x1e.feast.serving.UseDefaultsMode\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\x82\x04\n\x1eGetOnlineFeaturesRangeResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12M\n\x08\x65ntities\x18\x02 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRangeResponse.EntitiesEntry\x12Q\n\x07results\x18\x03 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeResponse.RangeFeatureVector\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1a\xac\x01\n\x12RangeFeatureVector\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.feast.types.RepeatedValue\x12\x34\n\x08statuses\x18\x02 \x03(\x0b\x32\".feast.serving.RepeatedFieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.feast.types.RepeatedValue*y\n\x0fUseDefaultsMode\x12\x1c\n\x18USE_DEFAULTS_UNSPECIFIED\x10\x00\x12\x14\n\x10USE_DEFAULTS_OFF\x10\x01\x12\x19\n\x15USE_DEFAULTS_FLEXIBLE\x10\x02\x12\x17\n\x13USE_DEFAULTS_STRICT\x10\x03*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xbc\x03\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12]\n\x0eGetVersionInfo\x12$.feast.serving.GetVersionInfoRequest\x1a%.feast.serving.GetVersionInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponse\x12u\n\x16GetOnlineFeaturesRange\x12,.feast.serving.GetOnlineFeaturesRangeRequest\x1a-.feast.serving.GetOnlineFeaturesRangeResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,8 +36,10 @@ _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_options = b'8\001' _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._options = None _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_options = b'8\001' - _globals['_FIELDSTATUS']._serialized_start=3208 - _globals['_FIELDSTATUS']._serialized_end=3299 + _globals['_USEDEFAULTSMODE']._serialized_start=3316 + _globals['_USEDEFAULTSMODE']._serialized_end=3437 + _globals['_FIELDSTATUS']._serialized_start=3439 + _globals['_FIELDSTATUS']._serialized_end=3530 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_start=111 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_end=139 _globals['_GETFEASTSERVINGINFORESPONSE']._serialized_start=141 @@ -57,35 +59,35 @@ _globals['_FEATURELIST']._serialized_start=794 _globals['_FEATURELIST']._serialized_end=820 _globals['_GETONLINEFEATURESREQUEST']._serialized_start=823 - _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1305 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1216 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1297 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1308 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1646 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1495 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1646 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1648 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1734 - _globals['_REPEATEDFIELDSTATUS']._serialized_start=1736 - _globals['_REPEATEDFIELDSTATUS']._serialized_end=1801 - _globals['_SORTKEYFILTER']._serialized_start=1804 - _globals['_SORTKEYFILTER']._serialized_end=2090 - _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_start=1941 - _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_end=2081 - _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_start=2093 - _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_end=2689 - _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1216 - _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1297 - _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_start=2692 - _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_end=3206 - _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_start=3034 - _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_end=3206 - _globals['_SERVINGSERVICE']._serialized_start=3302 - _globals['_SERVINGSERVICE']._serialized_end=3746 + _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1359 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1270 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1351 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1362 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1700 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1549 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1700 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1702 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1788 + _globals['_REPEATEDFIELDSTATUS']._serialized_start=1790 + _globals['_REPEATEDFIELDSTATUS']._serialized_end=1855 + _globals['_SORTKEYFILTER']._serialized_start=1858 + _globals['_SORTKEYFILTER']._serialized_end=2144 + _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_start=1995 + _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_end=2135 + _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_start=2147 + _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_end=2797 + _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1270 + _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1351 + _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_start=2800 + _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_end=3314 + _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_start=3142 + _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_end=3314 + _globals['_SERVINGSERVICE']._serialized_start=3533 + _globals['_SERVINGSERVICE']._serialized_end=3977 # @@protoc_insertion_point(module_scope) From ff9655c70b8b2f3a747f38249be426d2b83fa05a Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Thu, 29 Jan 2026 09:38:16 -0800 Subject: [PATCH 09/18] generated protos --- .../feast/protos/feast/core/Feature_pb2.pyi | 8 +++- .../feast/serving/ServingService_pb2.pyi | 39 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.pyi b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi index aa56630424f..734d5bc275e 100644 --- a/sdk/python/feast/protos/feast/core/Feature_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi @@ -56,6 +56,7 @@ class FeatureSpecV2(google.protobuf.message.Message): VECTOR_INDEX_FIELD_NUMBER: builtins.int VECTOR_SEARCH_METRIC_FIELD_NUMBER: builtins.int VECTOR_LENGTH_FIELD_NUMBER: builtins.int + DEFAULT_VALUE_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature. Not updatable.""" value_type: feast.types.Value_pb2.ValueType.Enum.ValueType @@ -71,6 +72,9 @@ class FeatureSpecV2(google.protobuf.message.Message): """Metric used for vector similarity search.""" vector_length: builtins.int """Field indicating the vector length""" + @property + def default_value(self) -> feast.types.Value_pb2.Value: + """Default value to be used for the feature when its value is missing/expired.""" def __init__( self, *, @@ -81,7 +85,9 @@ class FeatureSpecV2(google.protobuf.message.Message): vector_index: builtins.bool = ..., vector_search_metric: builtins.str = ..., vector_length: builtins.int = ..., + default_value: feast.types.Value_pb2.Value | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "name", b"name", "tags", b"tags", "value_type", b"value_type", "vector_index", b"vector_index", "vector_length", b"vector_length", "vector_search_metric", b"vector_search_metric"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["default_value", b"default_value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["default_value", b"default_value", "description", b"description", "name", b"name", "tags", b"tags", "value_type", b"value_type", "vector_index", b"vector_index", "vector_length", b"vector_length", "vector_search_metric", b"vector_search_metric"]) -> None: ... global___FeatureSpecV2 = FeatureSpecV2 diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi index 24c452620a7..f43d4333dce 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi @@ -34,6 +34,33 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +class _UseDefaultsMode: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _UseDefaultsModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_UseDefaultsMode.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + USE_DEFAULTS_UNSPECIFIED: _UseDefaultsMode.ValueType # 0 + """Field not set - use server default behavior (currently OFF)""" + USE_DEFAULTS_OFF: _UseDefaultsMode.ValueType # 1 + """Explicitly disable default replacement""" + USE_DEFAULTS_FLEXIBLE: _UseDefaultsMode.ValueType # 2 + """Ignore if default missing""" + USE_DEFAULTS_STRICT: _UseDefaultsMode.ValueType # 3 + """Fail if default is missing""" + +class UseDefaultsMode(_UseDefaultsMode, metaclass=_UseDefaultsModeEnumTypeWrapper): ... + +USE_DEFAULTS_UNSPECIFIED: UseDefaultsMode.ValueType # 0 +"""Field not set - use server default behavior (currently OFF)""" +USE_DEFAULTS_OFF: UseDefaultsMode.ValueType # 1 +"""Explicitly disable default replacement""" +USE_DEFAULTS_FLEXIBLE: UseDefaultsMode.ValueType # 2 +"""Ignore if default missing""" +USE_DEFAULTS_STRICT: UseDefaultsMode.ValueType # 3 +"""Fail if default is missing""" +global___UseDefaultsMode = UseDefaultsMode + class _FieldStatus: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType @@ -289,6 +316,7 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): FULL_FEATURE_NAMES_FIELD_NUMBER: builtins.int REQUEST_CONTEXT_FIELD_NUMBER: builtins.int INCLUDE_METADATA_FIELD_NUMBER: builtins.int + USE_DEFAULTS_FIELD_NUMBER: builtins.int feature_service: builtins.str @property def features(self) -> global___FeatureList: ... @@ -306,6 +334,8 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): """ include_metadata: builtins.bool """Whether to include the timestamp/status metadata in the response""" + use_defaults: global___UseDefaultsMode.ValueType + """Mode for handling features with default values when feature value is missing""" def __init__( self, *, @@ -315,9 +345,10 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): full_feature_names: builtins.bool = ..., request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., include_metadata: builtins.bool = ..., + use_defaults: global___UseDefaultsMode.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "request_context", b"request_context"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "request_context", b"request_context", "use_defaults", b"use_defaults"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... global___GetOnlineFeaturesRequest = GetOnlineFeaturesRequest @@ -499,6 +530,7 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): LIMIT_FIELD_NUMBER: builtins.int REQUEST_CONTEXT_FIELD_NUMBER: builtins.int INCLUDE_METADATA_FIELD_NUMBER: builtins.int + USE_DEFAULTS_FIELD_NUMBER: builtins.int feature_service: builtins.str @property def features(self) -> global___FeatureList: ... @@ -520,6 +552,8 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): """ include_metadata: builtins.bool """Whether to include the timestamp and status metadata in the response""" + use_defaults: global___UseDefaultsMode.ValueType + """Mode for handling features with default values when feature value is missing""" def __init__( self, *, @@ -532,9 +566,10 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): limit: builtins.int = ..., request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., include_metadata: builtins.bool = ..., + use_defaults: global___UseDefaultsMode.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "limit", b"limit", "request_context", b"request_context", "reverse_sort_order", b"reverse_sort_order", "sort_key_filters", b"sort_key_filters"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "limit", b"limit", "request_context", b"request_context", "reverse_sort_order", b"reverse_sort_order", "sort_key_filters", b"sort_key_filters", "use_defaults", b"use_defaults"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... global___GetOnlineFeaturesRangeRequest = GetOnlineFeaturesRangeRequest From df6f2712efb0e3fea9dc3cba0a77998a876b5d2f Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Fri, 23 Jan 2026 09:53:49 -0800 Subject: [PATCH 10/18] add default value field --- protos/feast/core/Feature.proto | 3 + protos/feast/serving/ServingService.proto | 14 +++++ sdk/python/feast/field.py | 69 ++++++++++++++++++++--- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/protos/feast/core/Feature.proto b/protos/feast/core/Feature.proto index 9f7708c65e7..472df8f25b8 100644 --- a/protos/feast/core/Feature.proto +++ b/protos/feast/core/Feature.proto @@ -45,4 +45,7 @@ message FeatureSpecV2 { // Field indicating the vector length int32 vector_length = 7; + + // Default value to be used for the feature when its value is missing/expired. + feast.types.Value default_value = 8; } diff --git a/protos/feast/serving/ServingService.proto b/protos/feast/serving/ServingService.proto index ebadeb6f7ff..36af202d23a 100644 --- a/protos/feast/serving/ServingService.proto +++ b/protos/feast/serving/ServingService.proto @@ -108,6 +108,16 @@ message GetOnlineFeaturesRequest { // Whether to include the timestamp/status metadata in the response bool include_metadata = 10; + + // Mode for handling features with default values when feature value is missing + UseDefaultsMode use_defaults = 11; +} + +enum UseDefaultsMode { + USE_DEFAULTS_UNSPECIFIED = 0; // Field not set - use server default behavior (currently OFF) + USE_DEFAULTS_OFF = 1; // Explicitly disable default replacement + USE_DEFAULTS_FLEXIBLE = 2; // Ignore if default missing + USE_DEFAULTS_STRICT = 3; // Fail if default is missing } message GetOnlineFeaturesResponse { @@ -200,6 +210,10 @@ message GetOnlineFeaturesRangeRequest { // Whether to include the timestamp and status metadata in the response bool include_metadata = 9; + + // Mode for handling features with default values when feature value is missing + UseDefaultsMode use_defaults = 11; + } message GetOnlineFeaturesRangeResponse { diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index d03a5ccdaac..5b69dfd669d 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -21,6 +21,7 @@ from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType +from feast.protos.feast.types import Value_pb2 as ValueProto @typechecked @@ -47,6 +48,20 @@ class Field(BaseModel): vector_index: bool = False vector_length: int = 0 vector_search_metric: Optional[str] = "" + default_value: Optional[ValueProto.Value] = None + + @field_validator("default_value") + def validate_default_value_type(cls, v, info): + """Validate that default_value matches dtype""" + if v is not None: + dtype = info.data.get('dtype') + if dtype is not None: + value_type = dtype.to_value_type() + if not _is_value_compatible_with_type(v, value_type): + raise ValueError( + f"default_value type does not match dtype {dtype}" + ) + return v @field_validator("dtype", mode="before") def dtype_is_feasttype_or_string_feasttype(cls, v): @@ -110,7 +125,7 @@ def to_proto(self) -> FieldProto: """Converts a Field object to its protobuf representation.""" value_type = self.dtype.to_value_type() vector_search_metric = self.vector_search_metric or "" - return FieldProto( + proto = FieldProto( name=self.name, value_type=value_type.value, description=self.description, @@ -119,6 +134,10 @@ def to_proto(self) -> FieldProto: vector_length=self.vector_length, vector_search_metric=vector_search_metric, ) + if self.default_value is not None: + proto.default_value.CopyFrom(self.default_value) + + return proto @classmethod def from_proto(cls, field_proto: FieldProto): @@ -132,15 +151,19 @@ def from_proto(cls, field_proto: FieldProto): vector_search_metric = getattr(field_proto, "vector_search_metric", "") vector_index = getattr(field_proto, "vector_index", False) vector_length = getattr(field_proto, "vector_length", 0) + default_value = None + if field_proto.HasField("default_value"): + default_value = field_proto.default_value return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): @@ -156,3 +179,31 @@ def from_feature(cls, feature: Feature): description=feature.description, tags=feature.labels, ) + + def _is_value_compatible_with_type( + value: ValueProto.Value, + value_type: ValueType + ) -> bool: + """Check if a Value proto matches a ValueType""" + val_case = value.WhichOneof('val') + + type_mapping = { + 'int32_val': ValueType.INT32, + 'int64_val': ValueType.INT64, + 'double_val': ValueType.DOUBLE, + 'float_val': ValueType.FLOAT, + 'string_val': ValueType.STRING, + 'bytes_val': ValueType.BYTES, + 'bool_val': ValueType.BOOL, + 'int32_list_val': ValueType.INT32_LIST, + 'int64_list_val': ValueType.INT64_LIST, + 'double_list_val': ValueType.DOUBLE_LIST, + 'float_list_val': ValueType.FLOAT_LIST, + 'string_list_val': ValueType.STRING_LIST, + 'bytes_list_val': ValueType.BYTES_LIST, + 'bool_list_val': ValueType.BOOL_LIST, + 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, + 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + } + + return type_mapping.get(val_case) == value_type From bc9e53794728b7243d309ced39c99f79a7ea8096 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Sat, 24 Jan 2026 10:53:49 -0800 Subject: [PATCH 11/18] fixing mypy issues --- sdk/python/feast/field.py | 122 +++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index 5b69dfd669d..a8678a4311c 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from pydantic import BaseModel, ConfigDict, field_validator from typeguard import check_type, typechecked @@ -21,7 +21,9 @@ from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType -from feast.protos.feast.types import Value_pb2 as ValueProto + +if TYPE_CHECKING: + from feast.protos.feast.types import Value_pb2 as ValueProto @typechecked @@ -51,16 +53,55 @@ class Field(BaseModel): default_value: Optional[ValueProto.Value] = None @field_validator("default_value") - def validate_default_value_type(cls, v, info): - """Validate that default_value matches dtype""" - if v is not None: - dtype = info.data.get('dtype') - if dtype is not None: - value_type = dtype.to_value_type() - if not _is_value_compatible_with_type(v, value_type): - raise ValueError( - f"default_value type does not match dtype {dtype}" - ) + @classmethod + def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) -> Optional["ValueProto.Value"]: + """ + Validate that default_value type matches the field's dtype. + """ + if v is None: + return v + + # Get dtype from the model data + dtype = info.data.get('dtype') + if dtype is None: + # dtype will be validated by its own validator, skip for now + return v + + # Validate type compatibility + value_type = dtype.to_value_type() + val_case = v.WhichOneof('val') + + if val_case is None: + # Empty Value proto + return v + + # Map proto value types to ValueType enums + type_mapping: Dict[str, ValueType] = { + 'int32_val': ValueType.INT32, + 'int64_val': ValueType.INT64, + 'double_val': ValueType.DOUBLE, + 'float_val': ValueType.FLOAT, + 'string_val': ValueType.STRING, + 'bytes_val': ValueType.BYTES, + 'bool_val': ValueType.BOOL, + 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, + 'int32_list_val': ValueType.INT32_LIST, + 'int64_list_val': ValueType.INT64_LIST, + 'double_list_val': ValueType.DOUBLE_LIST, + 'float_list_val': ValueType.FLOAT_LIST, + 'string_list_val': ValueType.STRING_LIST, + 'bytes_list_val': ValueType.BYTES_LIST, + 'bool_list_val': ValueType.BOOL_LIST, + 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + } + + expected_type = type_mapping.get(val_case) + if expected_type != value_type: + raise ValueError( + f"default_value type '{val_case}' does not match field dtype '{dtype}' " + f"(expected ValueType.{value_type.name})" + ) + return v @field_validator("dtype", mode="before") @@ -134,8 +175,9 @@ def to_proto(self) -> FieldProto: vector_length=self.vector_length, vector_search_metric=vector_search_metric, ) + # Add default_value if present (using type: ignore until proto is regenerated) if self.default_value is not None: - proto.default_value.CopyFrom(self.default_value) + proto.default_value.CopyFrom(self.default_value) # type: ignore[attr-defined] return proto @@ -151,19 +193,21 @@ def from_proto(cls, field_proto: FieldProto): vector_search_metric = getattr(field_proto, "vector_search_metric", "") vector_index = getattr(field_proto, "vector_index", False) vector_length = getattr(field_proto, "vector_length", 0) - default_value = None - if field_proto.HasField("default_value"): - default_value = field_proto.default_value + # Extract default_value if present + default_value = getattr(field_proto, "default_value", None) + if default_value is not None and not default_value.WhichOneof("val"): + # Empty Value proto, treat as None + default_value = None return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - default_value=default_value, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): @@ -179,31 +223,3 @@ def from_feature(cls, feature: Feature): description=feature.description, tags=feature.labels, ) - - def _is_value_compatible_with_type( - value: ValueProto.Value, - value_type: ValueType - ) -> bool: - """Check if a Value proto matches a ValueType""" - val_case = value.WhichOneof('val') - - type_mapping = { - 'int32_val': ValueType.INT32, - 'int64_val': ValueType.INT64, - 'double_val': ValueType.DOUBLE, - 'float_val': ValueType.FLOAT, - 'string_val': ValueType.STRING, - 'bytes_val': ValueType.BYTES, - 'bool_val': ValueType.BOOL, - 'int32_list_val': ValueType.INT32_LIST, - 'int64_list_val': ValueType.INT64_LIST, - 'double_list_val': ValueType.DOUBLE_LIST, - 'float_list_val': ValueType.FLOAT_LIST, - 'string_list_val': ValueType.STRING_LIST, - 'bytes_list_val': ValueType.BYTES_LIST, - 'bool_list_val': ValueType.BOOL_LIST, - 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, - 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, - } - - return type_mapping.get(val_case) == value_type From 8b987e181923504efda3424131e8119ec2e83fe3 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Sat, 24 Jan 2026 22:37:55 -0800 Subject: [PATCH 12/18] fixed formatting --- sdk/python/feast/field.py | 64 +++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index a8678a4311c..e2bf5dda37b 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -12,19 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, field_validator from typeguard import check_type, typechecked from feast.feature import Feature from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.types import FeastType, from_string, from_value_type from feast.value_type import ValueType -if TYPE_CHECKING: - from feast.protos.feast.types import Value_pb2 as ValueProto - @typechecked class Field(BaseModel): @@ -54,7 +52,9 @@ class Field(BaseModel): @field_validator("default_value") @classmethod - def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) -> Optional["ValueProto.Value"]: + def validate_default_value_type( + cls, v: Optional[ValueProto.Value], info: Any + ) -> Optional[ValueProto.Value]: """ Validate that default_value type matches the field's dtype. """ @@ -62,14 +62,14 @@ def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) return v # Get dtype from the model data - dtype = info.data.get('dtype') + dtype = info.data.get("dtype") if dtype is None: # dtype will be validated by its own validator, skip for now return v # Validate type compatibility value_type = dtype.to_value_type() - val_case = v.WhichOneof('val') + val_case = v.WhichOneof("val") if val_case is None: # Empty Value proto @@ -77,22 +77,22 @@ def validate_default_value_type(cls, v: Optional["ValueProto.Value"], info: Any) # Map proto value types to ValueType enums type_mapping: Dict[str, ValueType] = { - 'int32_val': ValueType.INT32, - 'int64_val': ValueType.INT64, - 'double_val': ValueType.DOUBLE, - 'float_val': ValueType.FLOAT, - 'string_val': ValueType.STRING, - 'bytes_val': ValueType.BYTES, - 'bool_val': ValueType.BOOL, - 'unix_timestamp_val': ValueType.UNIX_TIMESTAMP, - 'int32_list_val': ValueType.INT32_LIST, - 'int64_list_val': ValueType.INT64_LIST, - 'double_list_val': ValueType.DOUBLE_LIST, - 'float_list_val': ValueType.FLOAT_LIST, - 'string_list_val': ValueType.STRING_LIST, - 'bytes_list_val': ValueType.BYTES_LIST, - 'bool_list_val': ValueType.BOOL_LIST, - 'unix_timestamp_list_val': ValueType.UNIX_TIMESTAMP_LIST, + "int32_val": ValueType.INT32, + "int64_val": ValueType.INT64, + "double_val": ValueType.DOUBLE, + "float_val": ValueType.FLOAT, + "string_val": ValueType.STRING, + "bytes_val": ValueType.BYTES, + "bool_val": ValueType.BOOL, + "unix_timestamp_val": ValueType.UNIX_TIMESTAMP, + "int32_list_val": ValueType.INT32_LIST, + "int64_list_val": ValueType.INT64_LIST, + "double_list_val": ValueType.DOUBLE_LIST, + "float_list_val": ValueType.FLOAT_LIST, + "string_list_val": ValueType.STRING_LIST, + "bytes_list_val": ValueType.BYTES_LIST, + "bool_list_val": ValueType.BOOL_LIST, + "unix_timestamp_list_val": ValueType.UNIX_TIMESTAMP_LIST, } expected_type = type_mapping.get(val_case) @@ -199,15 +199,15 @@ def from_proto(cls, field_proto: FieldProto): # Empty Value proto, treat as None default_value = None return cls( - name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), - description=field_proto.description, - vector_index=vector_index, - vector_length=vector_length, - vector_search_metric=vector_search_metric, - default_value=default_value, - ) + name=field_proto.name, + dtype=from_value_type(value_type=value_type), + tags=dict(field_proto.tags), + description=field_proto.description, + vector_index=vector_index, + vector_length=vector_length, + vector_search_metric=vector_search_metric, + default_value=default_value, + ) @classmethod def from_feature(cls, feature: Feature): From 3395d2bd717f0d1c7ac4b9afeaa671619e409dda Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 13:26:39 -0800 Subject: [PATCH 13/18] updated pydantic models --- .../expediagroup/pydantic_models/field_model.py | 4 ++++ sdk/python/feast/feature.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/expediagroup/pydantic_models/field_model.py b/sdk/python/feast/expediagroup/pydantic_models/field_model.py index b30c5862755..31fc6be6ca8 100644 --- a/sdk/python/feast/expediagroup/pydantic_models/field_model.py +++ b/sdk/python/feast/expediagroup/pydantic_models/field_model.py @@ -4,6 +4,7 @@ from typing_extensions import Self from feast.field import Field +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.types import Array, PrimitiveFeastType @@ -19,6 +20,7 @@ class FieldModel(BaseModel): vector_index: bool = False vector_length: int = 0 vector_search_metric: Optional[str] = None + default_value: Optional[ValueProto.Value] = None def to_field(self) -> Field: """ @@ -35,6 +37,7 @@ def to_field(self) -> Field: vector_index=self.vector_index, vector_length=self.vector_length, vector_search_metric=self.vector_search_metric, + default_value=self.default_value, ) @classmethod @@ -56,4 +59,5 @@ def from_field( vector_index=field.vector_index, vector_length=field.vector_length, vector_search_metric=field.vector_search_metric, + default_value=field.default_value, ) diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index db629d677a8..017aff1faad 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -15,6 +15,7 @@ from typing import Dict, Optional from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FeatureSpecProto +from feast.protos.feast.types import Value_pb2 as ValueProto from feast.protos.feast.types.Value_pb2 import ValueType as ValueTypeProto from feast.value_type import ValueType @@ -35,6 +36,7 @@ def __init__( dtype: ValueType, description: str = "", labels: Optional[Dict[str, str]] = None, + default_value: Optional[ValueProto.Value] = None, ): """Creates a Feature object.""" self._name = name @@ -48,6 +50,7 @@ def __init__( self._labels = dict() else: self._labels = labels + self._default_value = default_value def __eq__(self, other): if self.name != other.name or self.dtype != other.dtype: @@ -64,6 +67,7 @@ def __repr__(self): f" dtype={self._dtype!r},\n" f" description={self._description!r},\n" f" labels={self._labels!r}\n" + f" default_value={self._default_value!r}\n" f")" ) @@ -108,12 +112,15 @@ def to_proto(self) -> FeatureSpecProto: """ value_type = ValueTypeProto.Enum.Value(self.dtype.name) - return FeatureSpecProto( + proto = FeatureSpecProto( name=self.name, value_type=value_type, description=self.description, tags=self.labels, ) + if self.default_value is not None: + proto.default_value.CopyFrom(self.default_value) # type: ignore[attr-defined] + return proto @classmethod def from_proto(cls, feature_proto: FeatureSpecProto): @@ -124,11 +131,16 @@ def from_proto(cls, feature_proto: FeatureSpecProto): Returns: Feature object """ + default_value = getattr(feature_proto, "default_value", None) + if default_value is not None and not default_value.WhichOneof("val"): + # Empty Value proto, treat as None + default_value = None feature = cls( name=feature_proto.name, dtype=ValueType(feature_proto.value_type), description=feature_proto.description, labels=dict(feature_proto.tags), + default_value=default_value, ) return feature From fe771a77b9886ad08312394515f94b4d4e646a2e Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 13:39:00 -0800 Subject: [PATCH 14/18] add missing property --- sdk/python/feast/feature.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index 017aff1faad..320843f581e 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -103,6 +103,13 @@ def labels(self) -> Dict[str, str]: """ return self._labels + @property + def default_value(self) -> Dict[str, str]: + """ + Gets the default value of this feature. + """ + return self._default_value + def to_proto(self) -> FeatureSpecProto: """ Converts Feature object to its Protocol Buffer representation. From cde8f1ea62c69d5cf030b5727df1d171b7758340 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Mon, 26 Jan 2026 16:20:36 -0800 Subject: [PATCH 15/18] fix test failueres --- sdk/python/feast/expediagroup/pydantic_models/field_model.py | 4 +++- sdk/python/feast/feature.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/expediagroup/pydantic_models/field_model.py b/sdk/python/feast/expediagroup/pydantic_models/field_model.py index 31fc6be6ca8..f5f2e9c09db 100644 --- a/sdk/python/feast/expediagroup/pydantic_models/field_model.py +++ b/sdk/python/feast/expediagroup/pydantic_models/field_model.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from typing_extensions import Self from feast.field import Field @@ -22,6 +22,8 @@ class FieldModel(BaseModel): vector_search_metric: Optional[str] = None default_value: Optional[ValueProto.Value] = None + model_config = ConfigDict(arbitrary_types_allowed=True) + def to_field(self) -> Field: """ Given a Pydantic FieldModel, create and return a Field. diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index 320843f581e..15904474df6 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -104,7 +104,7 @@ def labels(self) -> Dict[str, str]: return self._labels @property - def default_value(self) -> Dict[str, str]: + def default_value(self) -> Optional[ValueProto.Value]: """ Gets the default value of this feature. """ From c8dc514e038e580eeac8ebc5e40baf99c214d966 Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Tue, 27 Jan 2026 16:36:22 -0800 Subject: [PATCH 16/18] added unit tests --- sdk/python/feast/field.py | 1 + sdk/python/tests/unit/test_feature.py | 156 +++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index e2bf5dda37b..d14d2d55c68 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -222,4 +222,5 @@ def from_feature(cls, feature: Feature): dtype=from_value_type(feature.dtype), description=feature.description, tags=feature.labels, + default_value=feature.default_value, ) diff --git a/sdk/python/tests/unit/test_feature.py b/sdk/python/tests/unit/test_feature.py index ca0dce44457..08c51e050cc 100644 --- a/sdk/python/tests/unit/test_feature.py +++ b/sdk/python/tests/unit/test_feature.py @@ -1,5 +1,9 @@ +import pytest +from pydantic_core import ValidationError + from feast.field import Feature, Field -from feast.types import Float32 +from feast.protos.feast.types import Value_pb2 as ValueProto +from feast.types import Array, Bool, Float32, Int32, Int64, String from feast.value_type import ValueType @@ -9,7 +13,6 @@ def test_feature_serialization_with_description(): name="avg_daily_trips", dtype=ValueType.FLOAT, description=expected_description ) serialized_feature = feature.to_proto() - assert serialized_feature.description == expected_description @@ -21,12 +24,155 @@ def test_field_serialization_with_description(): feature = Feature( name="avg_daily_trips", dtype=ValueType.FLOAT, description=expected_description ) - serialized_field = field.to_proto() field_from_feature = Field.from_feature(feature) - assert serialized_field.description == expected_description assert field_from_feature.description == expected_description - field = Field.from_proto(serialized_field) assert field.description == expected_description + + +def test_field_with_default_value_to_proto(): + default_val = ValueProto.Value(int32_val=42) + field = Field(name="age", dtype=Int32, default_value=default_val) + proto = field.to_proto() + assert proto.name == "age" + assert proto.HasField("default_value") + assert proto.default_value.int32_val == 42 + + +def test_field_without_default_value_to_proto(): + field = Field(name="age", dtype=Int32) + proto = field.to_proto() + assert proto.name == "age" + assert not proto.HasField("default_value") + + +def test_field_from_proto_with_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + default_val = ValueProto.Value(string_val="unknown") + proto = FeatureSpecV2( + name="country", + value_type=2, # STRING + default_value=default_val, + ) + field = Field.from_proto(proto) + assert field.name == "country" + assert field.default_value is not None + assert field.default_value.string_val == "unknown" + + +def test_field_from_proto_without_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + proto = FeatureSpecV2(name="country", value_type=2) + field = Field.from_proto(proto) + assert field.name == "country" + assert field.default_value is None + + +def test_field_roundtrip_with_default_value(): + default_val = ValueProto.Value(int64_val=9999) + original_field = Field(name="user_id", dtype=Int64, default_value=default_val) + proto = original_field.to_proto() + restored_field = Field.from_proto(proto) + assert restored_field.name == original_field.name + assert restored_field.dtype == original_field.dtype + assert restored_field.default_value.int64_val == 9999 + + +def test_field_default_value_type_validation(): + with pytest.raises(ValidationError, match="does not match field dtype"): + default_val = ValueProto.Value(string_val="not_an_int") + Field(name="age", dtype=Int32, default_value=default_val) + + +def test_field_with_list_default_value(): + default_val = ValueProto.Value(int32_list_val=ValueProto.Int32List(val=[1, 2, 3])) + field = Field(name="scores", dtype=Array(Int32), default_value=default_val) + assert list(field.default_value.int32_list_val.val) == [1, 2, 3] + + +def test_feature_with_default_value_to_proto(): + default_val = ValueProto.Value(int32_val=0) + feature = Feature(name="count", dtype=ValueType.INT32, default_value=default_val) + proto = feature.to_proto() + assert proto.name == "count" + assert proto.HasField("default_value") + assert proto.default_value.int32_val == 0 + + +def test_feature_without_default_value_to_proto(): + feature = Feature(name="count", dtype=ValueType.INT32) + proto = feature.to_proto() + assert proto.name == "count" + assert not proto.HasField("default_value") + + +def test_feature_from_proto_with_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + default_val = ValueProto.Value(bool_val=False) + proto = FeatureSpecV2( + name="is_active", + value_type=7, # BOOL + default_value=default_val, + ) + feature = Feature.from_proto(proto) + assert feature.name == "is_active" + assert feature.default_value is not None + assert feature.default_value.bool_val is False + + +def test_feature_from_proto_without_default_value(): + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + + proto = FeatureSpecV2(name="is_active", value_type=7) + feature = Feature.from_proto(proto) + assert feature.name == "is_active" + assert feature.default_value is None + + +def test_feature_roundtrip_with_default_value(): + default_val = ValueProto.Value(string_val="US") + original_feature = Feature( + name="country", dtype=ValueType.STRING, default_value=default_val + ) + proto = original_feature.to_proto() + restored_feature = Feature.from_proto(proto) + assert restored_feature.name == original_feature.name + assert restored_feature.dtype == original_feature.dtype + assert restored_feature.default_value.string_val == "US" + + +def test_backward_compatibility_field_from_feature(): + default_val = ValueProto.Value(int32_val=18) + feature = Feature(name="age", dtype=ValueType.INT32, default_value=default_val) + field = Field.from_feature(feature) + assert field.name == "age" + assert field.default_value is not None + assert field.default_value.int32_val == 18 + + +def test_field_default_value_edge_cases(): + # Zero value + field1 = Field( + name="count", dtype=Int32, default_value=ValueProto.Value(int32_val=0) + ) + assert field1.default_value.int32_val == 0 + # Empty string + field2 = Field( + name="name", dtype=String, default_value=ValueProto.Value(string_val="") + ) + assert field2.default_value.string_val == "" + # False boolean + field3 = Field( + name="flag", dtype=Bool, default_value=ValueProto.Value(bool_val=False) + ) + assert field3.default_value.bool_val is False + # Negative number + field4 = Field( + name="error", dtype=Int64, default_value=ValueProto.Value(int64_val=-1) + ) + assert field4.default_value.int64_val == -1 From af0b9bae191daec25be6c92602bb7cfd01e0adda Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Tue, 27 Jan 2026 18:56:28 -0800 Subject: [PATCH 17/18] adding compiled protos --- .../feast/protos/feast/core/Feature_pb2.py | 8 +-- .../feast/serving/ServingService_pb2.py | 70 ++++++++++--------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.py b/sdk/python/feast/protos/feast/core/Feature_pb2.py index a02bb7ff403..c7abf76d66e 100644 --- a/sdk/python/feast/protos/feast/core/Feature_pb2.py +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.py @@ -15,7 +15,7 @@ from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Feature.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\"\x8e\x02\n\rFeatureSpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x31\n\x04tags\x18\x03 \x03(\x0b\x32#.feast.core.FeatureSpecV2.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x14\n\x0cvector_index\x18\x05 \x01(\x08\x12\x1c\n\x14vector_search_metric\x18\x06 \x01(\t\x12\x15\n\rvector_length\x18\x07 \x01(\x05\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x10\x66\x65\x61st.proto.coreB\x0c\x46\x65\x61tureProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Feature.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\"\xb9\x02\n\rFeatureSpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x31\n\x04tags\x18\x03 \x03(\x0b\x32#.feast.core.FeatureSpecV2.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x14\n\x0cvector_index\x18\x05 \x01(\x08\x12\x1c\n\x14vector_search_metric\x18\x06 \x01(\t\x12\x15\n\rvector_length\x18\x07 \x01(\x05\x12)\n\rdefault_value\x18\x08 \x01(\x0b\x32\x12.feast.types.Value\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x10\x66\x65\x61st.proto.coreB\x0c\x46\x65\x61tureProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -26,7 +26,7 @@ _globals['_FEATURESPECV2_TAGSENTRY']._options = None _globals['_FEATURESPECV2_TAGSENTRY']._serialized_options = b'8\001' _globals['_FEATURESPECV2']._serialized_start=66 - _globals['_FEATURESPECV2']._serialized_end=336 - _globals['_FEATURESPECV2_TAGSENTRY']._serialized_start=293 - _globals['_FEATURESPECV2_TAGSENTRY']._serialized_end=336 + _globals['_FEATURESPECV2']._serialized_end=379 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_start=336 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_end=379 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py index 12c00e856b2..85e467ca447 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x17\n\x15GetVersionInfoRequest\"{\n\x16GetVersionInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\nbuild_time\x18\x02 \x01(\t\x12\x13\n\x0b\x63ommit_hash\x18\x03 \x01(\t\x12\x12\n\ngo_version\x18\x04 \x01(\t\x12\x13\n\x0bserver_type\x18\x05 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\xe2\x03\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\n \x01(\x08\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList\"A\n\x13RepeatedFieldStatus\x12*\n\x06status\x18\x01 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\"\x9e\x02\n\rSortKeyFilter\x12\x15\n\rsort_key_name\x18\x01 \x01(\t\x12\x38\n\x05range\x18\x02 \x01(\x0b\x32\'.feast.serving.SortKeyFilter.RangeQueryH\x00\x12$\n\x06\x65quals\x18\x03 \x01(\x0b\x32\x12.feast.types.ValueH\x00\x1a\x8c\x01\n\nRangeQuery\x12\'\n\x0brange_start\x18\x02 \x01(\x0b\x32\x12.feast.types.Value\x12%\n\trange_end\x18\x03 \x01(\x0b\x32\x12.feast.types.Value\x12\x17\n\x0fstart_inclusive\x18\x04 \x01(\x08\x12\x15\n\rend_inclusive\x18\x05 \x01(\x08\x42\x07\n\x05query\"\xd4\x04\n\x1dGetOnlineFeaturesRangeRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12L\n\x08\x65ntities\x18\x03 \x03(\x0b\x32:.feast.serving.GetOnlineFeaturesRangeRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12\x36\n\x10sort_key_filters\x18\x05 \x03(\x0b\x32\x1c.feast.serving.SortKeyFilter\x12\x1a\n\x12reverse_sort_order\x18\x06 \x01(\x08\x12\r\n\x05limit\x18\x07 \x01(\x05\x12Y\n\x0frequest_context\x18\x08 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\t \x01(\x08\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\x82\x04\n\x1eGetOnlineFeaturesRangeResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12M\n\x08\x65ntities\x18\x02 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRangeResponse.EntitiesEntry\x12Q\n\x07results\x18\x03 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeResponse.RangeFeatureVector\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1a\xac\x01\n\x12RangeFeatureVector\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.feast.types.RepeatedValue\x12\x34\n\x08statuses\x18\x02 \x03(\x0b\x32\".feast.serving.RepeatedFieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.feast.types.RepeatedValue*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xbc\x03\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12]\n\x0eGetVersionInfo\x12$.feast.serving.GetVersionInfoRequest\x1a%.feast.serving.GetVersionInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponse\x12u\n\x16GetOnlineFeaturesRange\x12,.feast.serving.GetOnlineFeaturesRangeRequest\x1a-.feast.serving.GetOnlineFeaturesRangeResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x17\n\x15GetVersionInfoRequest\"{\n\x16GetVersionInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\nbuild_time\x18\x02 \x01(\t\x12\x13\n\x0b\x63ommit_hash\x18\x03 \x01(\t\x12\x12\n\ngo_version\x18\x04 \x01(\t\x12\x13\n\x0bserver_type\x18\x05 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x98\x04\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\n \x01(\x08\x12\x34\n\x0cuse_defaults\x18\x0b \x01(\x0e\x32\x1e.feast.serving.UseDefaultsMode\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList\"A\n\x13RepeatedFieldStatus\x12*\n\x06status\x18\x01 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\"\x9e\x02\n\rSortKeyFilter\x12\x15\n\rsort_key_name\x18\x01 \x01(\t\x12\x38\n\x05range\x18\x02 \x01(\x0b\x32\'.feast.serving.SortKeyFilter.RangeQueryH\x00\x12$\n\x06\x65quals\x18\x03 \x01(\x0b\x32\x12.feast.types.ValueH\x00\x1a\x8c\x01\n\nRangeQuery\x12\'\n\x0brange_start\x18\x02 \x01(\x0b\x32\x12.feast.types.Value\x12%\n\trange_end\x18\x03 \x01(\x0b\x32\x12.feast.types.Value\x12\x17\n\x0fstart_inclusive\x18\x04 \x01(\x08\x12\x15\n\rend_inclusive\x18\x05 \x01(\x08\x42\x07\n\x05query\"\x8a\x05\n\x1dGetOnlineFeaturesRangeRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12L\n\x08\x65ntities\x18\x03 \x03(\x0b\x32:.feast.serving.GetOnlineFeaturesRangeRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12\x36\n\x10sort_key_filters\x18\x05 \x03(\x0b\x32\x1c.feast.serving.SortKeyFilter\x12\x1a\n\x12reverse_sort_order\x18\x06 \x01(\x08\x12\r\n\x05limit\x18\x07 \x01(\x05\x12Y\n\x0frequest_context\x18\x08 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeRequest.RequestContextEntry\x12\x18\n\x10include_metadata\x18\t \x01(\x08\x12\x34\n\x0cuse_defaults\x18\x0b \x01(\x0e\x32\x1e.feast.serving.UseDefaultsMode\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\x82\x04\n\x1eGetOnlineFeaturesRangeResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12M\n\x08\x65ntities\x18\x02 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRangeResponse.EntitiesEntry\x12Q\n\x07results\x18\x03 \x03(\x0b\x32@.feast.serving.GetOnlineFeaturesRangeResponse.RangeFeatureVector\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1a\xac\x01\n\x12RangeFeatureVector\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.feast.types.RepeatedValue\x12\x34\n\x08statuses\x18\x02 \x03(\x0b\x32\".feast.serving.RepeatedFieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.feast.types.RepeatedValue*y\n\x0fUseDefaultsMode\x12\x1c\n\x18USE_DEFAULTS_UNSPECIFIED\x10\x00\x12\x14\n\x10USE_DEFAULTS_OFF\x10\x01\x12\x19\n\x15USE_DEFAULTS_FLEXIBLE\x10\x02\x12\x17\n\x13USE_DEFAULTS_STRICT\x10\x03*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xbc\x03\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12]\n\x0eGetVersionInfo\x12$.feast.serving.GetVersionInfoRequest\x1a%.feast.serving.GetVersionInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponse\x12u\n\x16GetOnlineFeaturesRange\x12,.feast.serving.GetOnlineFeaturesRangeRequest\x1a-.feast.serving.GetOnlineFeaturesRangeResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,8 +36,10 @@ _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_options = b'8\001' _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._options = None _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_options = b'8\001' - _globals['_FIELDSTATUS']._serialized_start=3208 - _globals['_FIELDSTATUS']._serialized_end=3299 + _globals['_USEDEFAULTSMODE']._serialized_start=3316 + _globals['_USEDEFAULTSMODE']._serialized_end=3437 + _globals['_FIELDSTATUS']._serialized_start=3439 + _globals['_FIELDSTATUS']._serialized_end=3530 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_start=111 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_end=139 _globals['_GETFEASTSERVINGINFORESPONSE']._serialized_start=141 @@ -57,35 +59,35 @@ _globals['_FEATURELIST']._serialized_start=794 _globals['_FEATURELIST']._serialized_end=820 _globals['_GETONLINEFEATURESREQUEST']._serialized_start=823 - _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1305 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1216 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1297 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1308 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1646 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1495 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1646 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1648 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1734 - _globals['_REPEATEDFIELDSTATUS']._serialized_start=1736 - _globals['_REPEATEDFIELDSTATUS']._serialized_end=1801 - _globals['_SORTKEYFILTER']._serialized_start=1804 - _globals['_SORTKEYFILTER']._serialized_end=2090 - _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_start=1941 - _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_end=2081 - _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_start=2093 - _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_end=2689 - _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1216 - _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1297 - _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_start=2692 - _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_end=3206 - _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_start=1139 - _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_end=1214 - _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_start=3034 - _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_end=3206 - _globals['_SERVINGSERVICE']._serialized_start=3302 - _globals['_SERVINGSERVICE']._serialized_end=3746 + _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1359 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1270 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1351 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1362 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1700 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1549 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1700 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1702 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1788 + _globals['_REPEATEDFIELDSTATUS']._serialized_start=1790 + _globals['_REPEATEDFIELDSTATUS']._serialized_end=1855 + _globals['_SORTKEYFILTER']._serialized_start=1858 + _globals['_SORTKEYFILTER']._serialized_end=2144 + _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_start=1995 + _globals['_SORTKEYFILTER_RANGEQUERY']._serialized_end=2135 + _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_start=2147 + _globals['_GETONLINEFEATURESRANGEREQUEST']._serialized_end=2797 + _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESRANGEREQUEST_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1270 + _globals['_GETONLINEFEATURESRANGEREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1351 + _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_start=2800 + _globals['_GETONLINEFEATURESRANGERESPONSE']._serialized_end=3314 + _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_start=1193 + _globals['_GETONLINEFEATURESRANGERESPONSE_ENTITIESENTRY']._serialized_end=1268 + _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_start=3142 + _globals['_GETONLINEFEATURESRANGERESPONSE_RANGEFEATUREVECTOR']._serialized_end=3314 + _globals['_SERVINGSERVICE']._serialized_start=3533 + _globals['_SERVINGSERVICE']._serialized_end=3977 # @@protoc_insertion_point(module_scope) From ce5ca901e53feae2d6360352a5636f3f7bebc32d Mon Sep 17 00:00:00 2001 From: vbhagwat Date: Thu, 29 Jan 2026 09:38:16 -0800 Subject: [PATCH 18/18] generated protos --- .../feast/protos/feast/core/Feature_pb2.pyi | 8 +++- .../feast/serving/ServingService_pb2.pyi | 39 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.pyi b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi index aa56630424f..734d5bc275e 100644 --- a/sdk/python/feast/protos/feast/core/Feature_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi @@ -56,6 +56,7 @@ class FeatureSpecV2(google.protobuf.message.Message): VECTOR_INDEX_FIELD_NUMBER: builtins.int VECTOR_SEARCH_METRIC_FIELD_NUMBER: builtins.int VECTOR_LENGTH_FIELD_NUMBER: builtins.int + DEFAULT_VALUE_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature. Not updatable.""" value_type: feast.types.Value_pb2.ValueType.Enum.ValueType @@ -71,6 +72,9 @@ class FeatureSpecV2(google.protobuf.message.Message): """Metric used for vector similarity search.""" vector_length: builtins.int """Field indicating the vector length""" + @property + def default_value(self) -> feast.types.Value_pb2.Value: + """Default value to be used for the feature when its value is missing/expired.""" def __init__( self, *, @@ -81,7 +85,9 @@ class FeatureSpecV2(google.protobuf.message.Message): vector_index: builtins.bool = ..., vector_search_metric: builtins.str = ..., vector_length: builtins.int = ..., + default_value: feast.types.Value_pb2.Value | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "name", b"name", "tags", b"tags", "value_type", b"value_type", "vector_index", b"vector_index", "vector_length", b"vector_length", "vector_search_metric", b"vector_search_metric"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["default_value", b"default_value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["default_value", b"default_value", "description", b"description", "name", b"name", "tags", b"tags", "value_type", b"value_type", "vector_index", b"vector_index", "vector_length", b"vector_length", "vector_search_metric", b"vector_search_metric"]) -> None: ... global___FeatureSpecV2 = FeatureSpecV2 diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi index 24c452620a7..f43d4333dce 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi @@ -34,6 +34,33 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +class _UseDefaultsMode: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _UseDefaultsModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_UseDefaultsMode.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + USE_DEFAULTS_UNSPECIFIED: _UseDefaultsMode.ValueType # 0 + """Field not set - use server default behavior (currently OFF)""" + USE_DEFAULTS_OFF: _UseDefaultsMode.ValueType # 1 + """Explicitly disable default replacement""" + USE_DEFAULTS_FLEXIBLE: _UseDefaultsMode.ValueType # 2 + """Ignore if default missing""" + USE_DEFAULTS_STRICT: _UseDefaultsMode.ValueType # 3 + """Fail if default is missing""" + +class UseDefaultsMode(_UseDefaultsMode, metaclass=_UseDefaultsModeEnumTypeWrapper): ... + +USE_DEFAULTS_UNSPECIFIED: UseDefaultsMode.ValueType # 0 +"""Field not set - use server default behavior (currently OFF)""" +USE_DEFAULTS_OFF: UseDefaultsMode.ValueType # 1 +"""Explicitly disable default replacement""" +USE_DEFAULTS_FLEXIBLE: UseDefaultsMode.ValueType # 2 +"""Ignore if default missing""" +USE_DEFAULTS_STRICT: UseDefaultsMode.ValueType # 3 +"""Fail if default is missing""" +global___UseDefaultsMode = UseDefaultsMode + class _FieldStatus: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType @@ -289,6 +316,7 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): FULL_FEATURE_NAMES_FIELD_NUMBER: builtins.int REQUEST_CONTEXT_FIELD_NUMBER: builtins.int INCLUDE_METADATA_FIELD_NUMBER: builtins.int + USE_DEFAULTS_FIELD_NUMBER: builtins.int feature_service: builtins.str @property def features(self) -> global___FeatureList: ... @@ -306,6 +334,8 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): """ include_metadata: builtins.bool """Whether to include the timestamp/status metadata in the response""" + use_defaults: global___UseDefaultsMode.ValueType + """Mode for handling features with default values when feature value is missing""" def __init__( self, *, @@ -315,9 +345,10 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): full_feature_names: builtins.bool = ..., request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., include_metadata: builtins.bool = ..., + use_defaults: global___UseDefaultsMode.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "request_context", b"request_context"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "request_context", b"request_context", "use_defaults", b"use_defaults"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... global___GetOnlineFeaturesRequest = GetOnlineFeaturesRequest @@ -499,6 +530,7 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): LIMIT_FIELD_NUMBER: builtins.int REQUEST_CONTEXT_FIELD_NUMBER: builtins.int INCLUDE_METADATA_FIELD_NUMBER: builtins.int + USE_DEFAULTS_FIELD_NUMBER: builtins.int feature_service: builtins.str @property def features(self) -> global___FeatureList: ... @@ -520,6 +552,8 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): """ include_metadata: builtins.bool """Whether to include the timestamp and status metadata in the response""" + use_defaults: global___UseDefaultsMode.ValueType + """Mode for handling features with default values when feature value is missing""" def __init__( self, *, @@ -532,9 +566,10 @@ class GetOnlineFeaturesRangeRequest(google.protobuf.message.Message): limit: builtins.int = ..., request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., include_metadata: builtins.bool = ..., + use_defaults: global___UseDefaultsMode.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "limit", b"limit", "request_context", b"request_context", "reverse_sort_order", b"reverse_sort_order", "sort_key_filters", b"sort_key_filters"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_metadata", b"include_metadata", "kind", b"kind", "limit", b"limit", "request_context", b"request_context", "reverse_sort_order", b"reverse_sort_order", "sort_key_filters", b"sort_key_filters", "use_defaults", b"use_defaults"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... global___GetOnlineFeaturesRangeRequest = GetOnlineFeaturesRangeRequest