2525from feast import OnDemandFeatureView
2626from feast .data_source import DataSource
2727from feast .errors import InvalidEntityType
28- from feast .feature_logging import LoggingConfig , LoggingSource , LoggingDestination
28+ from feast .feature_logging import LoggingConfig , LoggingDestination , LoggingSource
2929from feast .feature_view import DUMMY_ENTITY_ID , DUMMY_ENTITY_VAL , FeatureView
30+ from feast .infra .offline_stores import offline_utils
31+ from feast .infra .offline_stores .contrib .athena_offline_store .athena_source import (
32+ AthenaLoggingDestination ,
33+ AthenaSource ,
34+ SavedDatasetAthenaStorage ,
35+ )
3036from feast .infra .offline_stores .offline_store import (
3137 OfflineStore ,
3238 RetrievalJob ,
3339 RetrievalMetadata ,
3440)
35-
36- from feast .infra .offline_stores .contrib .athena_offline_store .athena_source import (
37- AthenaSource ,
38- AthenaLoggingDestination ,
39- SavedDatasetAthenaStorage ,
40- )
4141from feast .infra .utils import aws_utils
42- from feast .infra .offline_stores import offline_utils
43-
44- from feast .registry import Registry
42+ from feast .registry import Registry , BaseRegistry
4543from feast .repo_config import FeastConfigBaseModel , RepoConfig
4644from feast .saved_dataset import SavedDatasetStorage
4745from feast .usage import log_exceptions_and_usage
@@ -82,7 +80,7 @@ def pull_latest_from_table_or_query(
8280 assert isinstance (data_source , AthenaSource )
8381 assert isinstance (config .offline_store , AthenaOfflineStoreConfig )
8482
85- from_expression = data_source .get_table_query_string ()
83+ from_expression = data_source .get_table_query_string (config )
8684
8785 partition_by_join_key_string = ", " .join (join_key_columns )
8886 if partition_by_join_key_string != "" :
@@ -99,9 +97,7 @@ def pull_latest_from_table_or_query(
9997
10098 date_partition_column = data_source .date_partition_column
10199
102- athena_client = aws_utils .get_athena_data_client (
103- config .offline_store .region
104- )
100+ athena_client = aws_utils .get_athena_data_client (config .offline_store .region )
105101 s3_resource = aws_utils .get_s3_resource (config .offline_store .region )
106102
107103 start_date = start_date .astimezone (tz = utc )
@@ -142,15 +138,13 @@ def pull_all_from_table_or_query(
142138 end_date : datetime ,
143139 ) -> RetrievalJob :
144140 assert isinstance (data_source , AthenaSource )
145- from_expression = data_source .get_table_query_string ()
141+ from_expression = data_source .get_table_query_string (config )
146142
147143 field_string = ", " .join (
148144 join_key_columns + feature_name_columns + [timestamp_field ]
149145 )
150146
151- athena_client = aws_utils .get_athena_data_client (
152- config .offline_store .region
153- )
147+ athena_client = aws_utils .get_athena_data_client (config .offline_store .region )
154148 s3_resource = aws_utils .get_s3_resource (config .offline_store .region )
155149
156150 date_partition_column = data_source .date_partition_column
@@ -186,9 +180,7 @@ def get_historical_features(
186180 ) -> RetrievalJob :
187181 assert isinstance (config .offline_store , AthenaOfflineStoreConfig )
188182
189- athena_client = aws_utils .get_athena_data_client (
190- config .offline_store .region
191- )
183+ athena_client = aws_utils .get_athena_data_client (config .offline_store .region )
192184 s3_resource = aws_utils .get_s3_resource (config .offline_store .region )
193185
194186 # get pandas dataframe consisting of 1 row (LIMIT 1) and generate the schema out of it
@@ -197,23 +189,24 @@ def get_historical_features(
197189 )
198190
199191 # find timestamp column of entity df.(default = "event_timestamp"). Exception occurs if there are more than two timestamp columns.
200- entity_df_event_timestamp_col = offline_utils . infer_event_timestamp_from_entity_df (
201- entity_schema
192+ entity_df_event_timestamp_col = (
193+ offline_utils . infer_event_timestamp_from_entity_df ( entity_schema )
202194 )
203195
204196 # get min,max of event_timestamp.
205197 entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range (
206- entity_df , entity_df_event_timestamp_col , athena_client , config ,
198+ entity_df ,
199+ entity_df_event_timestamp_col ,
200+ athena_client ,
201+ config ,
207202 )
208203
209204 @contextlib .contextmanager
210205 def query_generator () -> Iterator [str ]:
211206
212207 table_name = offline_utils .get_temp_entity_table_name ()
213208
214- _upload_entity_df (
215- entity_df , athena_client , config , s3_resource , table_name
216- )
209+ _upload_entity_df (entity_df , athena_client , config , s3_resource , table_name )
217210
218211 expected_join_keys = offline_utils .get_expected_join_keys (
219212 project , feature_views , registry
@@ -232,7 +225,6 @@ def query_generator() -> Iterator[str]:
232225 entity_df_event_timestamp_range ,
233226 )
234227
235-
236228 # Generate the Athena SQL query from the query context
237229 query = offline_utils .build_point_in_time_query (
238230 query_context ,
@@ -247,17 +239,20 @@ def query_generator() -> Iterator[str]:
247239 yield query
248240 finally :
249241
250- #Always clean up the temp Athena table
242+ # Always clean up the temp Athena table
251243 aws_utils .execute_athena_query (
252244 athena_client ,
253245 config .offline_store .data_source ,
254246 config .offline_store .database ,
255247 f"DROP TABLE IF EXISTS { config .offline_store .database } .{ table_name } " ,
256248 )
257249
258- bucket = config .offline_store .s3_staging_location .replace ("s3://" , "" ).split ("/" , 1 )[0 ]
259- aws_utils .delete_s3_directory (s3_resource ,bucket , "entity_df/" + table_name + "/" )
260-
250+ bucket = config .offline_store .s3_staging_location .replace (
251+ "s3://" , ""
252+ ).split ("/" , 1 )[0 ]
253+ aws_utils .delete_s3_directory (
254+ s3_resource , bucket , "entity_df/" + table_name + "/"
255+ )
261256
262257 return AthenaRetrievalJob (
263258 query = query_generator ,
@@ -276,21 +271,18 @@ def query_generator() -> Iterator[str]:
276271 ),
277272 )
278273
279-
280274 @staticmethod
281275 def write_logged_features (
282276 config : RepoConfig ,
283277 data : Union [pyarrow .Table , Path ],
284278 source : LoggingSource ,
285279 logging_config : LoggingConfig ,
286- registry : Registry ,
280+ registry : BaseRegistry ,
287281 ):
288282 destination = logging_config .destination
289283 assert isinstance (destination , AthenaLoggingDestination )
290284
291- athena_client = aws_utils .get_athena_data_client (
292- config .offline_store .region
293- )
285+ athena_client = aws_utils .get_athena_data_client (config .offline_store .region )
294286 s3_resource = aws_utils .get_s3_resource (config .offline_store .region )
295287 if isinstance (data , Path ):
296288 s3_path = f"{ config .offline_store .s3_staging_location } /logged_features/{ uuid .uuid4 ()} "
@@ -299,7 +291,7 @@ def write_logged_features(
299291
300292 aws_utils .upload_arrow_table_to_athena (
301293 table = data ,
302- athena_data_client = athena_client ,
294+ athena_client = athena_client ,
303295 data_source = config .offline_store .data_source ,
304296 database = config .offline_store .database ,
305297 s3_resource = s3_resource ,
@@ -332,7 +324,6 @@ def __init__(
332324 on_demand_feature_views (optional): A list of on demand transforms to apply at retrieval time
333325 """
334326
335-
336327 if not isinstance (query , str ):
337328 self ._query_generator = query
338329 else :
@@ -352,7 +343,6 @@ def query_generator() -> Iterator[str]:
352343 )
353344 self ._metadata = metadata
354345
355-
356346 @property
357347 def full_feature_names (self ) -> bool :
358348 return self ._full_feature_names
@@ -362,9 +352,15 @@ def on_demand_feature_views(self) -> Optional[List[OnDemandFeatureView]]:
362352 return self ._on_demand_feature_views
363353
364354 def get_temp_s3_path (self ) -> str :
365- return self ._config .offline_store .s3_staging_location + "/unload/" + str (uuid .uuid4 ())
355+ return (
356+ self ._config .offline_store .s3_staging_location
357+ + "/unload/"
358+ + str (uuid .uuid4 ())
359+ )
366360
367- def get_temp_table_dml_header (self , temp_table_name :str , temp_external_location :str ) -> str :
361+ def get_temp_table_dml_header (
362+ self , temp_table_name : str , temp_external_location : str
363+ ) -> str :
368364 temp_table_dml_header = f"""
369365 CREATE TABLE { temp_table_name }
370366 WITH (
@@ -387,7 +383,8 @@ def _to_df_internal(self) -> pd.DataFrame:
387383 self ._config .offline_store .database ,
388384 self ._s3_resource ,
389385 temp_external_location ,
390- self .get_temp_table_dml_header (temp_table_name , temp_external_location ) + query ,
386+ self .get_temp_table_dml_header (temp_table_name , temp_external_location )
387+ + query ,
391388 temp_table_name ,
392389 )
393390
@@ -402,7 +399,8 @@ def _to_arrow_internal(self) -> pa.Table:
402399 self ._config .offline_store .database ,
403400 self ._s3_resource ,
404401 temp_external_location ,
405- self .get_temp_table_dml_header (temp_table_name , temp_external_location ) + query ,
402+ self .get_temp_table_dml_header (temp_table_name , temp_external_location )
403+ + query ,
406404 temp_table_name ,
407405 )
408406
@@ -412,7 +410,33 @@ def metadata(self) -> Optional[RetrievalMetadata]:
412410
413411 def persist (self , storage : SavedDatasetStorage ):
414412 assert isinstance (storage , SavedDatasetAthenaStorage )
415- # self.to_athena(table_name=storage.athena_options.table)
413+ self .to_athena (table_name = storage .athena_options .table )
414+
415+ @log_exceptions_and_usage
416+ def to_athena (self , table_name : str ) -> None :
417+
418+ if self .on_demand_feature_views :
419+ transformed_df = self .to_df ()
420+
421+ _upload_entity_df (
422+ transformed_df ,
423+ self ._athena_client ,
424+ self ._config ,
425+ self ._s3_resource ,
426+ table_name ,
427+ )
428+
429+ return
430+
431+ with self ._query_generator () as query :
432+ query = f'CREATE TABLE "{ table_name } " AS ({ query } );\n '
433+
434+ aws_utils .execute_athena_query (
435+ self ._athena_client ,
436+ self ._config .offline_store .data_source ,
437+ self ._config .offline_store .database ,
438+ query ,
439+ )
416440
417441
418442def _upload_entity_df (
@@ -496,12 +520,14 @@ def _get_entity_df_event_timestamp_range(
496520 f"SELECT MIN({ entity_df_event_timestamp_col } ) AS min, MAX({ entity_df_event_timestamp_col } ) AS max "
497521 f"FROM ({ entity_df } )" ,
498522 )
499- res = aws_utils .get_athena_query_result (athena_client , statement_id )[
500- "Records"
501- ][0 ]
523+ res = aws_utils .get_athena_query_result (athena_client , statement_id )
502524 entity_df_event_timestamp_range = (
503- res .parse (res [0 ]["stringValue" ]),
504- res .parse (res [1 ]["stringValue" ]),
525+ datetime .strptime (
526+ res ["Rows" ][1 ]["Data" ][0 ]["VarCharValue" ], "%Y-%m-%d %H:%M:%S.%f"
527+ ),
528+ datetime .strptime (
529+ res ["Rows" ][1 ]["Data" ][1 ]["VarCharValue" ], "%Y-%m-%d %H:%M:%S.%f"
530+ ),
505531 )
506532 else :
507533 raise InvalidEntityType (type (entity_df ))
0 commit comments