11"""BoilingData Client"""
2- import os , json , uuid , time
3- import rel
4- import duckdb
2+ import os , json , uuid
3+ import logging
54import threading
5+ import duckdb
66import websocket
77import boto3
88import asyncio
99import botocore .auth
10- from pprint import pprint
1110from botocore .exceptions import NoCredentialsError
1211from botocore .awsrequest import AWSRequest
1312from botocore .credentials import Credentials
3938class BoilingData :
4039 """Run SQL with BoilingData and local DuckDB"""
4140
42- def __init__ (self ):
43- self .bd_conn = BoilingDataConnection ()
41+ def __init__ (self , log_level = logging .ERROR ):
42+ self .log_level = log_level
43+ self .logger = logging .getLogger ("BoilingData" )
44+ self .logger .setLevel (self .log_level )
45+ self .bd_conn = BoilingDataConnection (log_level = self .log_level )
4446 self .conn = duckdb .connect (":memory:" )
4547
46- async def populate (self ):
48+ async def _populate (self ):
4749 self .conn .execute ("ATTACH ':memory:' AS boilingdata;" )
4850 self .conn .execute ("SET search_path='memory,boilingdata';" )
49- # Boiling specific "information_schema" table
51+ # Boiling specific table, contains data shares
5052 q = "SELECT * FROM information_schema.create_tables_statements"
51-
52- def cb (bd_tables ):
53- if bd_tables :
54- for table in bd_tables :
55- self .conn .execute (table )
56-
57- await self .bd_conn .bd_execute (q , cb )
53+ tables = await self .execute (q , None , True )
54+ if tables :
55+ for table in tables :
56+ self .conn .execute (table )
5857
5958 def _is_boiling_execute (self , sql ):
6059 ## 1) Get all Boiling tables so we know what to intercept
@@ -91,22 +90,32 @@ def _is_boiling_execute(self, sql):
9190 async def connect (self ):
9291 """Connect to BoilingData"""
9392 await self .bd_conn .connect ()
93+ await self ._populate () # get catalog entries
9494
9595 async def close (self ):
9696 """Close WebSocket connection to Boiling"""
9797 await self .bd_conn .close ()
9898
99- async def execute (self , sql , cb ):
99+ async def execute (self , sql , cb = None , force_boiling = False ):
100100 """Send SQL Query to Boiling or run locally"""
101- if not self ._is_boiling_execute (sql ):
101+ if not force_boiling and not self ._is_boiling_execute (sql ):
102102 return self .conn .execute (sql ).fetchall ()
103- return await self .bd_conn .bd_execute (sql , cb )
103+ fut = await self .bd_conn .bd_execute (sql , cb )
104+ if cb is not None :
105+ return
106+ # TODO: Get rid of this while loop?!
107+ while not fut .done ():
108+ await asyncio .sleep (0.005 )
109+ return fut .result ()
104110
105111
106112class BoilingDataConnection :
107113 """Create authenticated WebSocket connection to BoilingData"""
108114
109- def __init__ (self , region = AWS_REGION ):
115+ def __init__ (self , region = AWS_REGION , log_level = logging .ERROR ):
116+ self .log_level = log_level
117+ self .logger = logging .getLogger ("BoilingDataConnection" )
118+ self .logger .setLevel (self .log_level )
110119 self .region = region
111120 self .username = os .getenv ("BD_USERNAME" , "" )
112121 self .password = os .getenv ("BD_PASSWORD" , "" )
@@ -115,6 +124,7 @@ def __init__(self, region=AWS_REGION):
115124 "Missing username (BD_USERNAME) and/or "
116125 + "password (BD_PASSWORD) environment variable(s)"
117126 )
127+ self .wsConnectTimeoutS = 10
118128 self .websocket = None
119129 self .aws_creds = None
120130 self .ws_app = None
@@ -150,10 +160,10 @@ def _get_cognito_tokens(self, username, password):
150160 )
151161 return response ["AuthenticationResult" ]
152162 except self .idp_client .exceptions .NotAuthorizedException as e :
153- print ("The username or password is incorrect." )
163+ self . logger . error ("The username or password is incorrect." )
154164 raise e
155165 except NoCredentialsError as e :
156- print ("Credentials not available." )
166+ self . logger . error ("Credentials not available." )
157167 raise e
158168
159169 def _get_credentials (self ):
@@ -175,20 +185,21 @@ def _get_credentials(self):
175185 return self .aws_creds
176186
177187 async def _ws_send (self , msg ):
178- # print (f"> {msg}")
188+ self . logger . debug (f"> { msg } " )
179189 return self .ws_app .send (msg )
180190
181191 def _on_open (self , ws_app ):
182- print ("WS OPEN" )
192+ self . logger . info ("WS OPEN" )
183193 self .bd_is_open = True
184194
185195 def _on_msg (self , ws_app , data ):
186- # print (f"< {data}")
196+ self . logger . debug (f"< { data } " )
187197 msg = json .loads (data )
188198 reqId = msg .get ("requestId" )
189199 if not reqId :
190200 return
191201 msg_type = msg .get ("messageType" )
202+ # TODO: Store statistics sent from Boiling (INFO messages)
192203 if msg_type != "DATA" :
193204 return
194205 req = self .requests .get (reqId )
@@ -201,32 +212,41 @@ def _on_msg(self, ws_app, data):
201212 del self .requests [reqId ]
202213
203214 def _on_error (self , ws_app , error ):
204- print (f"WS ERROR: { error } " )
215+ self . logger . error (f"WS ERROR: { error } " )
205216
206217 def _on_close (self , ws_app , code , msg ):
207- print (f"WS CLOSE: { code } { msg } " )
208-
209- ##
210- ## public
211- ##
218+ self .logger .info (f"WS CLOSE: { code } { msg } " )
219+ self .is_open = False
212220
213221 def _all_messages_received (self , event ):
214222 requestId = event ["requestId" ]
215223 data = event ["data" ]
216224 cb = self .requests .get (requestId )
217- cb ["callback" ](data )
225+ cb .get ("callback" )(data ) if cb .get ("callback" ) else None
226+ cb .get ("fut" ).set_result (data ) if cb .get ("fut" ) else None
227+
228+ ##
229+ ## public
230+ ##
218231
219232 async def bd_execute (self , sql , cb ):
220- if self .bd_is_open is not True :
233+ if not self .bd_is_open :
234+ await self .connect ()
235+ if not self .bd_is_open :
221236 raise Exception ("No Boiling connection" )
222237 reqId = uuid .uuid4 ().hex
223238 body = '{"sql":"' + sql + '","requestId":"' + reqId + '"}'
239+ loop = asyncio .get_running_loop ()
240+ fut = loop .create_future ()
224241 self .requests [reqId ] = {
225- "q" : DataQueue (reqId , self ._all_messages_received ),
242+ "q" : DataQueue (reqId , self ._all_messages_received , fut ),
243+ "sql" : sql ,
226244 "reqId" : reqId ,
227245 "callback" : cb ,
246+ "future" : fut ,
228247 }
229248 await self ._ws_send (body )
249+ return fut
230250
231251 async def connect (self ):
232252 """Connect to BoilingData WebSocket API"""
@@ -247,7 +267,7 @@ async def connect(self):
247267 wst .daemon = True
248268 wst .start ()
249269 timeoutS = 1
250- while self .bd_is_open is not True and timeoutS < 10 :
270+ while not self .bd_is_open and timeoutS < self . wsConnectTimeoutS :
251271 await asyncio .sleep (1 )
252272 timeoutS = timeoutS + 1
253273
0 commit comments