1+ import os
2+ import logging
3+ from databricks import sql
4+ from fastapi import FastAPI , HTTPException , Request
5+ from fastapi .staticfiles import StaticFiles
6+ from fastapi .responses import FileResponse
7+
8+ host = ""
9+ http_path = ""
10+ access_token = ""
11+
12+ logging .basicConfig (level = logging .INFO , format = "%(asctime)s [%(levelname)s] %(message)s" )
13+ logger = logging .getLogger (__name__ )
14+
15+ app = FastAPI (title = "Simple FastAPI + React App" )
16+
17+ def get_databricks_connection ():
18+ """Create and return a Databricks SQL connection"""
19+ if not access_token :
20+ raise ValueError ("DATABRICKS_TOKEN environment variable not set" )
21+
22+ return sql .connect (
23+ server_hostname = host ,
24+ http_path = http_path ,
25+ access_token = access_token
26+ )
27+
28+ @app .get ("/api/health" )
29+ async def health_check ():
30+ logger .info ("Health check at /api/health" )
31+ return {"status" : "healthy" }
32+
33+ @app .get ("/api/classification" )
34+ async def get_classification ():
35+ connection = get_databricks_connection ()
36+ cursor = connection .cursor ()
37+ cursor .execute ('SELECT resource_category, resource_type, total_count FROM cq_catalog.cloudquery.cloud_assets_counts' )
38+ result = cursor .fetchall ()
39+ cursor .close ()
40+ connection .close ()
41+
42+ data = {}
43+ for row in result :
44+ resource_category = row [0 ]
45+ resource_type = row [1 ]
46+ total_count = row [2 ]
47+
48+ if resource_category not in data :
49+ data [resource_category ] = {
50+ "resource_category" : resource_category ,
51+ "total_count" : 0 ,
52+ "types" : []
53+ }
54+
55+ # Add the type to the category
56+ data [resource_category ]["types" ].append ({
57+ "resource_type" : resource_type ,
58+ "total_count" : total_count
59+ })
60+
61+ data [resource_category ]["total_count" ] += total_count
62+
63+
64+ return {
65+ "data" : list (data .values ()),
66+ "title" : "Resource Classification in Databricks"
67+ }
68+
69+ @app .get ("/api/data" )
70+ async def get_data (request : Request ):
71+ connection = get_databricks_connection ()
72+ cursor = connection .cursor ()
73+
74+ filter_param = request .query_params .get ("filter" , "" )
75+ sorting_param = request .query_params .get ("sorting" , "" )
76+ page = int (request .query_params .get ("page" , 0 ))
77+ page_size = int (request .query_params .get ("pageSize" , 25 ))
78+ offset = page * page_size
79+
80+ def compose_where_clause (filter_array ):
81+ if not filter_array :
82+ return ""
83+
84+ conditions = []
85+ for filter_item in filter_array :
86+ field = filter_item .get ("field" , "" )
87+ operator = filter_item .get ("operator" , "" )
88+ value = filter_item .get ("value" , "" )
89+
90+ if not field or not operator or value == "" :
91+ continue
92+
93+ # Handle different operators
94+ if operator == "equals" :
95+ conditions .append (f"`{ field } ` = '{ value } '" )
96+ elif operator == "doesNotEqual" :
97+ conditions .append (f"`{ field } ` != '{ value } '" )
98+ elif operator == "contains" :
99+ conditions .append (f"`{ field } ` LIKE '%{ value } %'" )
100+ # Add more operators as needed
101+
102+ if conditions :
103+ return f"WHERE { ' AND ' .join (conditions )} "
104+ return ""
105+
106+ def compose_order_by_clause (sort_array ):
107+ if not sort_array :
108+ return ""
109+
110+ order_clauses = []
111+ for sort_item in sort_array :
112+ field = sort_item .get ("field" , "" )
113+ sort_direction = sort_item .get ("sort" , "" ).upper ()
114+
115+ if not field or sort_direction not in ["ASC" , "DESC" ]:
116+ continue
117+
118+ order_clauses .append (f"`{ field } ` { sort_direction } " )
119+
120+ if order_clauses :
121+ return f"ORDER BY { ', ' .join (order_clauses )} "
122+ return ""
123+
124+ # Parse filter parameter (assuming it's JSON string)
125+ try :
126+ import json
127+ filter_array = json .loads (filter_param ) if filter_param else []
128+ where_clause = compose_where_clause (filter_array )
129+ except (json .JSONDecodeError , TypeError ):
130+ # Fallback to empty filter if parsing fails
131+ where_clause = ""
132+
133+ # Parse sort parameter (assuming it's JSON string)
134+ try :
135+ import json
136+ sort_array = json .loads (sorting_param ) if sorting_param else []
137+ order_by_clause = compose_order_by_clause (sort_array )
138+ except (json .JSONDecodeError , TypeError ):
139+ # Fallback to empty sort if parsing fails
140+ order_by_clause = ""
141+
142+ # Get total count first with filter
143+ count_query = f'SELECT COUNT(*) as total FROM cq_catalog.cloudquery.cloud_assets { where_clause } '
144+ cursor .execute (count_query )
145+ total_count = cursor .fetchone ()[0 ]
146+
147+ # Get paginated data with filter and sorting
148+ data_query = f'SELECT * FROM cq_catalog.cloudquery.cloud_assets { where_clause } { order_by_clause } LIMIT { page_size } OFFSET { offset } '
149+ cursor .execute (data_query )
150+ result = cursor .fetchall ()
151+
152+ # Get column names
153+ columns = [desc [0 ] for desc in cursor .description ]
154+
155+ # Convert to list of dictionaries for JSON serialization
156+ data = []
157+ for row in result :
158+ row_dict = {}
159+ for i , value in enumerate (row ):
160+ # Convert non-serializable types to strings
161+ if value is not None :
162+ try :
163+ # Handle different data types more comprehensively
164+ if hasattr (value , '__dict__' ):
165+ row_dict [columns [i ]] = str (value )
166+ elif isinstance (value , (dict , list , tuple )):
167+ row_dict [columns [i ]] = str (value )
168+ elif isinstance (value , (int , float , str , bool )):
169+ row_dict [columns [i ]] = value
170+ else :
171+ # For any other type, convert to string
172+ row_dict [columns [i ]] = str (value )
173+ except (TypeError , ValueError , AttributeError ):
174+ row_dict [columns [i ]] = str (value )
175+ else :
176+ row_dict [columns [i ]] = None
177+ data .append (row_dict )
178+
179+ cursor .close ()
180+ connection .close ()
181+
182+ return {
183+ "data" : data ,
184+ "rowCount" : total_count ,
185+ "title" : "cloud_assets from Databricks"
186+ }
187+
188+
189+ # --- Static Files Setup ---
190+ static_dir = os .path .join (os .path .dirname (os .path .abspath (__file__ )), "static" )
191+ os .makedirs (static_dir , exist_ok = True )
192+ app .mount ("/" , StaticFiles (directory = static_dir , html = True ), name = "static" )
193+
194+ # --- Catch-all for React Routes ---
195+ @app .get ("/{full_path:path}" )
196+ async def serve_react (full_path : str ):
197+ index_html = os .path .join (static_dir , "index.html" )
198+ if os .path .exists (index_html ):
199+ logger .info (f"Serving React frontend for path: /{ full_path } " )
200+ return FileResponse (index_html )
201+ logger .error ("Frontend not built. index.html missing." )
202+ raise HTTPException (
203+ status_code = 404 ,
204+ detail = "Frontend not built. Please run 'npm run build' first."
205+ )
0 commit comments