@@ -74,6 +74,7 @@ class OnDemandFeatureView(BaseFeatureView):
7474 tags : dict [str , str ]
7575 owner : str
7676 write_to_online_store : bool
77+ singleton : bool
7778
7879 def __init__ ( # noqa: C901
7980 self ,
@@ -98,6 +99,7 @@ def __init__( # noqa: C901
9899 tags : Optional [dict [str , str ]] = None ,
99100 owner : str = "" ,
100101 write_to_online_store : bool = False ,
102+ singleton : bool = False ,
101103 ):
102104 """
103105 Creates an OnDemandFeatureView object.
@@ -121,6 +123,8 @@ def __init__( # noqa: C901
121123 of the primary maintainer.
122124 write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to
123125 the online store for faster retrieval.
126+ singleton (optional): A boolean that indicates whether the transformation is executed on a singleton
127+ (only applicable when mode="python").
124128 """
125129 super ().__init__ (
126130 name = name ,
@@ -204,6 +208,9 @@ def __init__( # noqa: C901
204208 self .features = features
205209 self .feature_transformation = feature_transformation
206210 self .write_to_online_store = write_to_online_store
211+ self .singleton = singleton
212+ if self .singleton and self .mode != "python" :
213+ raise ValueError ("Singleton is only supported for Python mode." )
207214
208215 @property
209216 def proto_class (self ) -> type [OnDemandFeatureViewProto ]:
@@ -221,6 +228,7 @@ def __copy__(self):
221228 tags = self .tags ,
222229 owner = self .owner ,
223230 write_to_online_store = self .write_to_online_store ,
231+ singleton = self .singleton ,
224232 )
225233 fv .entities = self .entities
226234 fv .features = self .features
@@ -247,6 +255,7 @@ def __eq__(self, other):
247255 or self .feature_transformation != other .feature_transformation
248256 or self .write_to_online_store != other .write_to_online_store
249257 or sorted (self .entity_columns ) != sorted (other .entity_columns )
258+ or self .singleton != other .singleton
250259 ):
251260 return False
252261
@@ -328,6 +337,7 @@ def to_proto(self) -> OnDemandFeatureViewProto:
328337 tags = self .tags ,
329338 owner = self .owner ,
330339 write_to_online_store = self .write_to_online_store ,
340+ singleton = self .singleton if self .singleton else False ,
331341 )
332342
333343 return OnDemandFeatureViewProto (spec = spec , meta = meta )
@@ -434,6 +444,9 @@ def from_proto(
434444 ]
435445 else :
436446 entity_columns = []
447+ singleton = False
448+ if hasattr (on_demand_feature_view_proto .spec , "singleton" ):
449+ singleton = on_demand_feature_view_proto .spec .singleton
437450
438451 on_demand_feature_view_obj = cls (
439452 name = on_demand_feature_view_proto .spec .name ,
@@ -451,6 +464,7 @@ def from_proto(
451464 tags = dict (on_demand_feature_view_proto .spec .tags ),
452465 owner = on_demand_feature_view_proto .spec .owner ,
453466 write_to_online_store = write_to_online_store ,
467+ singleton = singleton ,
454468 )
455469
456470 on_demand_feature_view_obj .entities = entities
@@ -614,17 +628,19 @@ def transform_dict(
614628 feature_dict [full_feature_ref ] = feature_dict [feature .name ]
615629 columns_to_cleanup .append (str (full_feature_ref ))
616630
617- output_dict : dict [str , Any ] = self .feature_transformation .transform (
618- feature_dict
619- )
631+ if self .singleton and self .mode == "python" :
632+ output_dict : dict [str , Any ] = (
633+ self .feature_transformation .transform_singleton (feature_dict )
634+ )
635+ else :
636+ output_dict = self .feature_transformation .transform (feature_dict )
620637 for feature_name in columns_to_cleanup :
621638 del output_dict [feature_name ]
622639 return output_dict
623640
624641 def infer_features (self ) -> None :
625- inferred_features = self .feature_transformation .infer_features (
626- self ._construct_random_input ()
627- )
642+ random_input = self ._construct_random_input (singleton = self .singleton )
643+ inferred_features = self .feature_transformation .infer_features (random_input )
628644
629645 if self .features :
630646 missing_features = []
@@ -644,8 +660,10 @@ def infer_features(self) -> None:
644660 f"Could not infer Features for the feature view '{ self .name } '." ,
645661 )
646662
647- def _construct_random_input (self ) -> dict [str , list [Any ]]:
648- rand_dict_value : dict [ValueType , list [Any ]] = {
663+ def _construct_random_input (
664+ self , singleton : bool = False
665+ ) -> dict [str , Union [list [Any ], Any ]]:
666+ rand_dict_value : dict [ValueType , Union [list [Any ], Any ]] = {
649667 ValueType .BYTES : [str .encode ("hello world" )],
650668 ValueType .STRING : ["hello world" ],
651669 ValueType .INT32 : [1 ],
@@ -663,20 +681,25 @@ def _construct_random_input(self) -> dict[str, list[Any]]:
663681 ValueType .BOOL_LIST : [[True ]],
664682 ValueType .UNIX_TIMESTAMP_LIST : [[_utc_now ()]],
665683 }
684+ if singleton :
685+ rand_dict_value = {k : rand_dict_value [k ][0 ] for k in rand_dict_value }
666686
687+ rand_missing_value = [None ] if singleton else None
667688 feature_dict = {}
668689 for feature_view_projection in self .source_feature_view_projections .values ():
669690 for feature in feature_view_projection .features :
670691 feature_dict [f"{ feature_view_projection .name } __{ feature .name } " ] = (
671- rand_dict_value .get (feature .dtype .to_value_type (), [None ])
692+ rand_dict_value .get (
693+ feature .dtype .to_value_type (), rand_missing_value
694+ )
672695 )
673696 feature_dict [f"{ feature .name } " ] = rand_dict_value .get (
674- feature .dtype .to_value_type (), [ None ]
697+ feature .dtype .to_value_type (), rand_missing_value
675698 )
676699 for request_data in self .source_request_sources .values ():
677700 for field in request_data .schema :
678701 feature_dict [f"{ field .name } " ] = rand_dict_value .get (
679- field .dtype .to_value_type (), [ None ]
702+ field .dtype .to_value_type (), rand_missing_value
680703 )
681704
682705 return feature_dict
@@ -713,6 +736,7 @@ def on_demand_feature_view(
713736 tags : Optional [dict [str , str ]] = None ,
714737 owner : str = "" ,
715738 write_to_online_store : bool = False ,
739+ singleton : bool = False ,
716740):
717741 """
718742 Creates an OnDemandFeatureView object with the given user function as udf.
@@ -731,6 +755,8 @@ def on_demand_feature_view(
731755 of the primary maintainer.
732756 write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to
733757 the online store for faster retrieval.
758+ singleton (optional): A boolean that indicates whether the transformation is executed on a singleton
759+ (only applicable when mode="python").
734760 """
735761
736762 def mainify (obj ) -> None :
@@ -775,6 +801,7 @@ def decorator(user_function):
775801 owner = owner ,
776802 write_to_online_store = write_to_online_store ,
777803 entities = entities ,
804+ singleton = singleton ,
778805 )
779806 functools .update_wrapper (
780807 wrapper = on_demand_feature_view_obj , wrapped = user_function
0 commit comments