11import functools
22from abc import ABC
3- from typing import Any , Callable , Dict , Optional , Union
3+ from typing import Any , Callable , Dict , List , Optional , Union
44
55import dill
66
1414 TRANSFORMATION_CLASS_FOR_TYPE ,
1515 get_transformation_class_from_type ,
1616)
17- from feast .transformation .mode import TransformationMode
17+ from feast .transformation .mode import TransformationMode , TransformationTiming
18+ from feast .entity import Entity
19+ from feast .field import Field
20+
21+ # Online compatibility constants
22+ ONLINE_COMPATIBLE_MODES = {"python" , "pandas" }
23+ BATCH_ONLY_MODES = {"sql" , "spark_sql" , "spark" , "ray" , "substrait" }
24+
25+
26+ def is_online_compatible (mode : str ) -> bool :
27+ """
28+ Check if a transformation mode can run online in Feature Server.
29+
30+ Args:
31+ mode: The transformation mode string
32+
33+ Returns:
34+ True if the mode can run in Feature Server, False if batch-only
35+ """
36+ return mode .lower () in ONLINE_COMPATIBLE_MODES
1837
1938
2039class Transformation (ABC ):
@@ -117,7 +136,12 @@ def infer_features(self, *args, **kwargs) -> Any:
117136
118137
119138def transformation (
120- mode : Union [TransformationMode , str ],
139+ mode : Union [TransformationMode , str ], # Support both enum and string
140+ when : Optional [str ] = None ,
141+ online : Optional [bool ] = None ,
142+ sources : Optional [List [Union ["FeatureView" , "FeatureViewProjection" , "RequestSource" ]]] = None ,
143+ schema : Optional [List [Field ]] = None ,
144+ entities : Optional [List [Entity ]] = None ,
121145 name : Optional [str ] = None ,
122146 tags : Optional [Dict [str , str ]] = None ,
123147 description : Optional [str ] = "" ,
@@ -130,18 +154,95 @@ def mainify(obj):
130154 obj .__module__ = "__main__"
131155
132156 def decorator (user_function ):
157+ # Validate mode (handle both enum and string)
158+ if isinstance (mode , TransformationMode ):
159+ mode_str = mode .value
160+ else :
161+ mode_str = mode .lower () # Normalize to lowercase
162+ try :
163+ mode_enum = TransformationMode (mode_str )
164+ except ValueError :
165+ valid_modes = [m .value for m in TransformationMode ]
166+ raise ValueError (f"Invalid mode '{ mode } '. Valid options: { valid_modes } " )
167+
168+ # Validate timing if provided
169+ timing_enum = None
170+ if when is not None :
171+ try :
172+ timing_enum = TransformationTiming (when .lower ())
173+ except ValueError :
174+ valid_timings = [t .value for t in TransformationTiming ]
175+ raise ValueError (f"Invalid timing '{ when } '. Valid options: { valid_timings } " )
176+
177+ # Validate online compatibility
178+ if online and not is_online_compatible (mode_str ):
179+ compatible_modes = list (ONLINE_COMPATIBLE_MODES )
180+ raise ValueError (
181+ f"Mode '{ mode_str } ' cannot run online in Feature Server. "
182+ f"Use { compatible_modes } for online transformations."
183+ )
184+
185+ # Create transformation object
133186 udf_string = dill .source .getsource (user_function )
134187 mainify (user_function )
135188 transformation_obj = Transformation (
136- mode = mode ,
189+ mode = mode_str ,
137190 name = name or user_function .__name__ ,
138191 tags = tags ,
139192 description = description ,
140193 owner = owner ,
141194 udf = user_function ,
142195 udf_string = udf_string ,
143196 )
144- functools .update_wrapper (wrapper = transformation_obj , wrapped = user_function )
145- return transformation_obj
197+
198+ # If FeatureView parameters are provided, create and return FeatureView
199+ if any (param is not None for param in [when , online , sources , schema , entities ]):
200+ # Import FeatureView here to avoid circular imports
201+ from feast .feature_view import FeatureView
202+
203+ # Validate required parameters when creating FeatureView
204+ if when is None :
205+ raise ValueError ("'when' parameter is required when creating FeatureView" )
206+ if online is None :
207+ raise ValueError ("'online' parameter is required when creating FeatureView" )
208+ if sources is None :
209+ raise ValueError ("'sources' parameter is required when creating FeatureView" )
210+ if schema is None :
211+ raise ValueError ("'schema' parameter is required when creating FeatureView" )
212+
213+ # Handle source parameter correctly for FeatureView constructor
214+ if not sources :
215+ raise ValueError ("At least one source must be provided for FeatureView" )
216+ elif len (sources ) == 1 :
217+ # Single source - pass directly (works for DataSource or FeatureView)
218+ source_param = sources [0 ]
219+ else :
220+ # Multiple sources - pass as list (must be List[FeatureView])
221+ from feast .feature_view import FeatureView as FV
222+ for src in sources :
223+ if not isinstance (src , (FV , type (src ).__name__ == 'FeatureView' )):
224+ raise ValueError ("Multiple sources must be FeatureViews, not DataSources" )
225+ source_param = sources
226+
227+ # Create FeatureView with transformation
228+ fv = FeatureView (
229+ name = name or user_function .__name__ ,
230+ source = source_param ,
231+ entities = entities or [],
232+ schema = schema ,
233+ feature_transformation = transformation_obj ,
234+ when = when ,
235+ online_enabled = online ,
236+ description = description ,
237+ tags = tags ,
238+ owner = owner ,
239+ mode = mode_str ,
240+ )
241+ functools .update_wrapper (wrapper = fv , wrapped = user_function )
242+ return fv
243+ else :
244+ # Backward compatibility: return Transformation object
245+ functools .update_wrapper (wrapper = transformation_obj , wrapped = user_function )
246+ return transformation_obj
146247
147248 return decorator
0 commit comments