Skip to content

Commit a44fe66

Browse files
committedAug 15, 2022
fix: Fix on demand feature view output in feast plan + Web UI crash (#3057)
* fix: Fix on demand feature view output in feast plan + Web UI crash with ODFV Signed-off-by: Danny Chiao <danny@tecton.ai> * lint Signed-off-by: Danny Chiao <danny@tecton.ai> * fix tests Signed-off-by: Danny Chiao <danny@tecton.ai> Signed-off-by: Danny Chiao <danny@tecton.ai>
1 parent f06874a commit a44fe66

File tree

6 files changed

+57
-19
lines changed

6 files changed

+57
-19
lines changed
 

‎protos/feast/core/OnDemandFeatureView.proto

+3
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,7 @@ message UserDefinedFunction {
8383

8484
// The python-syntax function body (serialized by dill)
8585
bytes body = 2;
86+
87+
// The string representation of the udf
88+
string body_text = 3;
8689
}

‎sdk/python/feast/diff/registry_diff.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from feast.protos.feast.core.OnDemandFeatureView_pb2 import (
1818
OnDemandFeatureView as OnDemandFeatureViewProto,
1919
)
20+
from feast.protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec
2021
from feast.protos.feast.core.RequestFeatureView_pb2 import (
2122
RequestFeatureView as RequestFeatureViewProto,
2223
)
@@ -137,19 +138,39 @@ def diff_registry_objects(
137138
else:
138139
current_spec = current_proto.spec
139140
new_spec = new_proto.spec
140-
if current_spec != new_spec:
141+
if current != new:
141142
for _field in current_spec.DESCRIPTOR.fields:
142143
if _field.name in FIELDS_TO_IGNORE:
143144
continue
144-
if getattr(current_spec, _field.name) != getattr(new_spec, _field.name):
145-
transition = TransitionType.UPDATE
146-
property_diffs.append(
147-
PropertyDiff(
148-
_field.name,
149-
getattr(current_spec, _field.name),
150-
getattr(new_spec, _field.name),
145+
elif getattr(current_spec, _field.name) != getattr(new_spec, _field.name):
146+
if _field.name == "user_defined_function":
147+
current_spec = cast(OnDemandFeatureViewSpec, current_proto)
148+
new_spec = cast(OnDemandFeatureViewSpec, new_proto)
149+
current_udf = current_spec.user_defined_function
150+
new_udf = new_spec.user_defined_function
151+
for _udf_field in current_udf.DESCRIPTOR.fields:
152+
if _udf_field.name == "body":
153+
continue
154+
if getattr(current_udf, _udf_field.name) != getattr(
155+
new_udf, _udf_field.name
156+
):
157+
transition = TransitionType.UPDATE
158+
property_diffs.append(
159+
PropertyDiff(
160+
_field.name + "." + _udf_field.name,
161+
getattr(current_udf, _udf_field.name),
162+
getattr(new_udf, _udf_field.name),
163+
)
164+
)
165+
else:
166+
transition = TransitionType.UPDATE
167+
property_diffs.append(
168+
PropertyDiff(
169+
_field.name,
170+
getattr(current_spec, _field.name),
171+
getattr(new_spec, _field.name),
172+
)
151173
)
152-
)
153174
return FeastObjectDiff(
154175
name=new_spec.name,
155176
feast_object_type=object_type,

‎sdk/python/feast/on_demand_feature_view.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ class OnDemandFeatureView(BaseFeatureView):
6161
maintainer.
6262
"""
6363

64-
# TODO(adchia): remove inputs from proto and declaration
6564
name: str
6665
features: List[Field]
6766
source_feature_view_projections: Dict[str, FeatureViewProjection]
6867
source_request_sources: Dict[str, RequestSource]
6968
udf: FunctionType
69+
udf_string: str
7070
description: str
7171
tags: Dict[str, str]
7272
owner: str
@@ -81,6 +81,7 @@ def __init__( # noqa: C901
8181
List[Any]
8282
] = None, # Typed as Any because @typechecked can't deal with the List[Union]
8383
udf: Optional[FunctionType] = None,
84+
udf_string: str = "",
8485
inputs: Optional[
8586
Dict[str, Union[FeatureView, FeatureViewProjection, RequestSource]]
8687
] = None,
@@ -99,8 +100,9 @@ def __init__( # noqa: C901
99100
sources (optional): A map from input source names to the actual input sources,
100101
which may be feature views, or request data sources.
101102
These sources serve as inputs to the udf, which will refer to them by name.
102-
udf (optional): The user defined transformation function, which must take pandas
103+
udf: The user defined transformation function, which must take pandas
103104
dataframes as inputs.
105+
udf_string: The source code version of the udf (for diffing and displaying in Web UI)
104106
inputs (optional): (Deprecated) A map from input source names to the actual input sources,
105107
which may be feature views, feature view projections, or request data sources.
106108
These sources serve as inputs to the udf, which will refer to them by name.
@@ -233,9 +235,8 @@ def __init__( # noqa: C901
233235
odfv_source.name
234236
] = odfv_source.projection
235237

236-
if _udf is None:
237-
raise ValueError("The `udf` parameter must be specified.")
238-
self.udf = _udf # type: ignore
238+
self.udf = udf # type: ignore
239+
self.udf_string = udf_string
239240

240241
@property
241242
def proto_class(self) -> Type[OnDemandFeatureViewProto]:
@@ -249,6 +250,7 @@ def __copy__(self):
249250
sources=list(self.source_feature_view_projections.values())
250251
+ list(self.source_request_sources.values()),
251252
udf=self.udf,
253+
udf_string=self.udf_string,
252254
description=self.description,
253255
tags=self.tags,
254256
owner=self.owner,
@@ -269,6 +271,7 @@ def __eq__(self, other):
269271
self.source_feature_view_projections
270272
!= other.source_feature_view_projections
271273
or self.source_request_sources != other.source_request_sources
274+
or self.udf_string != other.udf_string
272275
or self.udf.__code__.co_code != other.udf.__code__.co_code
273276
):
274277
return False
@@ -305,7 +308,9 @@ def to_proto(self) -> OnDemandFeatureViewProto:
305308
features=[feature.to_proto() for feature in self.features],
306309
sources=sources,
307310
user_defined_function=UserDefinedFunctionProto(
308-
name=self.udf.__name__, body=dill.dumps(self.udf, recurse=True),
311+
name=self.udf.__name__,
312+
body=dill.dumps(self.udf, recurse=True),
313+
body_text=self.udf_string,
309314
),
310315
description=self.description,
311316
tags=self.tags,
@@ -354,6 +359,7 @@ def from_proto(cls, on_demand_feature_view_proto: OnDemandFeatureViewProto):
354359
udf=dill.loads(
355360
on_demand_feature_view_proto.spec.user_defined_function.body
356361
),
362+
udf_string=on_demand_feature_view_proto.spec.user_defined_function.body_text,
357363
description=on_demand_feature_view_proto.spec.description,
358364
tags=dict(on_demand_feature_view_proto.spec.tags),
359365
owner=on_demand_feature_view_proto.spec.owner,
@@ -641,6 +647,7 @@ def mainify(obj):
641647
obj.__module__ = "__main__"
642648

643649
def decorator(user_function):
650+
udf_string = dill.source.getsource(user_function)
644651
mainify(user_function)
645652
on_demand_feature_view_obj = OnDemandFeatureView(
646653
name=user_function.__name__,
@@ -650,6 +657,7 @@ def decorator(user_function):
650657
description=description,
651658
tags=tags,
652659
owner=owner,
660+
udf_string=udf_string,
653661
)
654662
functools.update_wrapper(
655663
wrapper=on_demand_feature_view_obj, wrapped=user_function

‎sdk/python/feast/registry.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from typing import Any, Dict, List, Optional
2525
from urllib.parse import urlparse
2626

27-
import dill
2827
from google.protobuf.internal.containers import RepeatedCompositeFieldContainer
2928
from google.protobuf.json_format import MessageToJson
3029
from proto import Message
@@ -729,9 +728,10 @@ def to_dict(self, project: str) -> Dict[str, List[Any]]:
729728
key=lambda on_demand_feature_view: on_demand_feature_view.name,
730729
):
731730
odfv_dict = self._message_to_sorted_dict(on_demand_feature_view.to_proto())
732-
odfv_dict["spec"]["userDefinedFunction"]["body"] = dill.source.getsource(
733-
on_demand_feature_view.udf
734-
)
731+
732+
odfv_dict["spec"]["userDefinedFunction"][
733+
"body"
734+
] = on_demand_feature_view.udf_string
735735
registry_dict["onDemandFeatureViews"].append(odfv_dict)
736736
for request_feature_view in sorted(
737737
self.list_request_feature_views(project=project),

‎sdk/python/tests/integration/feature_repos/universal/feature_views.py

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def conv_rate_plus_100_feature_view(
8888
schema=[] if infer_features else _features,
8989
sources=sources,
9090
udf=conv_rate_plus_100,
91+
udf_string="raw udf source",
9192
)
9293

9394

@@ -125,6 +126,7 @@ def similarity_feature_view(
125126
sources=sources,
126127
schema=[] if infer_features else _fields,
127128
udf=similarity,
129+
udf_string="similarity raw udf",
128130
)
129131

130132

‎sdk/python/tests/unit/test_on_demand_feature_view.py

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def test_hash():
5757
Field(name="output2", dtype=Float32),
5858
],
5959
udf=udf1,
60+
udf_string="udf1 source code",
6061
)
6162
on_demand_feature_view_2 = OnDemandFeatureView(
6263
name="my-on-demand-feature-view",
@@ -66,6 +67,7 @@ def test_hash():
6667
Field(name="output2", dtype=Float32),
6768
],
6869
udf=udf1,
70+
udf_string="udf1 source code",
6971
)
7072
on_demand_feature_view_3 = OnDemandFeatureView(
7173
name="my-on-demand-feature-view",
@@ -75,6 +77,7 @@ def test_hash():
7577
Field(name="output2", dtype=Float32),
7678
],
7779
udf=udf2,
80+
udf_string="udf2 source code",
7881
)
7982
on_demand_feature_view_4 = OnDemandFeatureView(
8083
name="my-on-demand-feature-view",
@@ -84,6 +87,7 @@ def test_hash():
8487
Field(name="output2", dtype=Float32),
8588
],
8689
udf=udf2,
90+
udf_string="udf2 source code",
8791
description="test",
8892
)
8993

0 commit comments

Comments
 (0)