1313# limitations under the License.
1414
1515import os
16+ import tempfile
1617import time
17-
18+ from datetime import datetime
19+ import pytz
20+ import fastavro
1821import pandas as pd
22+ from google .cloud import bigquery
1923from google .cloud .bigquery .client import Client as BQClient
2024from google .cloud .bigquery .job import ExtractJobConfig , DestinationFormat
2125from google .cloud .bigquery .table import Table
26+ from google .cloud .exceptions import NotFound
2227from google .cloud .storage import Client as GCSClient
28+ from google .cloud import storage
2329
2430from feast .sdk .utils .gs_utils import is_gs_path , split_gs_path , gcs_to_df
2531
2632
2733def head (client , table , max_rows = 10 ):
28- ''' Get the head of the table. Retrieves rows from the given table at
34+ """ Get the head of the table. Retrieves rows from the given table at
2935 minimum cost
3036
3137 Args:
@@ -37,12 +43,13 @@ def head(client, table, max_rows=10):
3743
3844 Returns:
3945 pandas.DataFrame: dataframe containing the head of rows
40- '''
46+ """
4147
4248 rows = client .list_rows (table , max_results = max_rows )
4349 rows = [x for x in rows ]
4450 return pd .DataFrame (
45- data = [list (x .values ()) for x in rows ], columns = list (rows [0 ].keys ()))
51+ data = [list (x .values ()) for x in rows ], columns = list (rows [0 ].keys ())
52+ )
4653
4754
4855def get_table_name (feature_id , storage_spec ):
@@ -71,6 +78,108 @@ def get_table_name(feature_id, storage_spec):
7178 return "." .join ([project , dataset , table_name ])
7279
7380
81+ def get_default_templocation (bigquery_client , project = None ):
82+ if project is None :
83+ project = bigquery_client .project
84+ assert isinstance (project , str )
85+ assert len (project ) > 0
86+ storage_client = storage .Client ()
87+ default_bucket_name = f"feast-templocation-{ project } "
88+ try :
89+ storage_client .get_bucket (default_bucket_name )
90+ except NotFound :
91+ print (
92+ f'Default bucket "{ default_bucket_name } " not found. Attempting to create it.'
93+ )
94+ storage_client .create_bucket (bucket_name = default_bucket_name , project = project )
95+ return f"gs://{ default_bucket_name } "
96+
97+
98+ def query_to_dataframe (
99+ query : str ,
100+ bigquery_client : bigquery .Client = None ,
101+ storage_client : storage .Client = None ,
102+ project : str = None ,
103+ templocation : str = None ,
104+ ) -> pd .DataFrame :
105+ """
106+ Run a query job on BigQuery and return the result in Pandas DataFrame format
107+
108+ Args:
109+ query: BigQuery query e.g. "SELECT * FROM dataset.table"
110+ bigquery_client:
111+ storage_client:
112+ project: Google Cloud project id
113+ templocation: Google Cloud Storage location to store intermediate files, must start with "gs://"
114+
115+ Returns: Pandas DataFrame of the query result
116+
117+ """
118+ if isinstance (templocation , str ) and not templocation .startswith ("gs://" ):
119+ raise RuntimeError ('templocation must start with "gs://"' )
120+
121+ if bigquery_client is None :
122+ bigquery_client = bigquery .Client (project = project )
123+
124+ if project is None :
125+ project = bigquery_client .project
126+
127+ query_job = bigquery_client .query (query , project = project )
128+ query_job_state = ""
129+
130+ while not query_job .done ():
131+ if query_job .state != query_job_state :
132+ print (f"Query status: { query_job .state } " )
133+ query_job_state = query_job .state
134+ time .sleep (5 )
135+
136+ if query_job .state != query_job_state :
137+ print (f"Query status: { query_job .state } " )
138+
139+ if query_job .exception ():
140+ raise query_job .exception ()
141+
142+ if not templocation :
143+ templocation = get_default_templocation (bigquery_client , project = project )
144+
145+ if templocation .endswith ("/" ):
146+ templocation += templocation [:- 1 ]
147+
148+ destination_uri = (
149+ f"{ templocation } /bq-{ datetime .now (pytz .utc ).strftime ('%Y%m%dT%H%M%SZ' )} .avro"
150+ )
151+ extract_job_config = bigquery .job .ExtractJobConfig (destination_format = "AVRO" )
152+ extract_job = bigquery_client .extract_table (
153+ query_job .destination , destination_uri , job_config = extract_job_config
154+ )
155+
156+ while not extract_job .done ():
157+ time .sleep (5 )
158+
159+ if extract_job .exception ():
160+ raise extract_job .exception ()
161+
162+ if not storage_client :
163+ storage_client = storage .Client (project = project )
164+
165+ print ("Reading query result into DataFrame" )
166+
167+ bucket_name , blob_name = (
168+ destination_uri .split ("/" )[2 ],
169+ "/" .join (destination_uri .split ("/" )[3 :]),
170+ )
171+ bucket = storage_client .get_bucket (bucket_name )
172+ blob = bucket .get_blob (blob_name )
173+ downloaded_avro_filename = tempfile .NamedTemporaryFile ().name
174+ blob .download_to_filename (downloaded_avro_filename )
175+
176+ with open (downloaded_avro_filename , "rb" ) as avro_file :
177+ avro_reader = fastavro .reader (avro_file )
178+ df = pd .DataFrame .from_records (avro_reader )
179+
180+ return df
181+
182+
74183class TableDownloader :
75184 def __init__ (self ):
76185 self ._bq = None
@@ -88,8 +197,7 @@ def bq(self):
88197 self ._bq = BQClient ()
89198 return self ._bq
90199
91- def download_table_as_file (self , table_id , dest , staging_location ,
92- file_type ):
200+ def download_table_as_file (self , table_id , dest , staging_location , file_type ):
93201 """
94202 Download a bigquery table as file
95203 Args:
@@ -105,14 +213,13 @@ def download_table_as_file(self, table_id, dest, staging_location,
105213 if not is_gs_path (staging_location ):
106214 raise ValueError ("staging_uri must be a directory in GCS" )
107215
108- temp_file_name = ' temp_{}' .format (int (round (time .time () * 1000 )))
216+ temp_file_name = " temp_{}" .format (int (round (time .time () * 1000 )))
109217 staging_file_path = os .path .join (staging_location , temp_file_name )
110218
111219 job_config = ExtractJobConfig ()
112220 job_config .destination_format = file_type
113221 src_table = Table .from_string (table_id )
114- job = self .bq .extract_table (
115- src_table , staging_file_path , job_config = job_config )
222+ job = self .bq .extract_table (src_table , staging_file_path , job_config = job_config )
116223
117224 # await completion
118225 job .result ()
@@ -137,15 +244,14 @@ def download_table_as_df(self, table_id, staging_location):
137244 if not is_gs_path (staging_location ):
138245 raise ValueError ("staging_uri must be a directory in GCS" )
139246
140- temp_file_name = ' temp_{}' .format (int (round (time .time () * 1000 )))
247+ temp_file_name = " temp_{}" .format (int (round (time .time () * 1000 )))
141248 staging_file_path = os .path .join (staging_location , temp_file_name )
142249
143250 job_config = ExtractJobConfig ()
144251 job_config .destination_format = DestinationFormat .CSV
145252 job = self .bq .extract_table (
146- Table .from_string (table_id ),
147- staging_file_path ,
148- job_config = job_config )
253+ Table .from_string (table_id ), staging_file_path , job_config = job_config
254+ )
149255
150256 # await completion
151257 job .result ()
0 commit comments