1414 python export_to_google_sheets.py [--spreadsheet-id SPREADSHEET_ID] [--sheet-name SHEET_NAME]
1515"""
1616
17+ import argparse
18+ import json
1719import os
1820import sys
19- import json
20- import argparse
2121from datetime import datetime
2222from pathlib import Path
2323
@@ -39,7 +39,7 @@ def get_google_credentials():
3939 """Get Google Sheets credentials from environment or file."""
4040 # Check environment variable first
4141 creds_path = os .getenv ("GOOGLE_SHEETS_CREDENTIALS" )
42-
42+
4343 # Fall back to credentials.json in project root
4444 if not creds_path :
4545 creds_path = Path (__file__ ).parent / "credentials.json"
@@ -48,17 +48,17 @@ def get_google_credentials():
4848 "Google Sheets credentials not found. "
4949 "Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root."
5050 )
51-
51+
5252 creds_path = Path (creds_path )
5353 if not creds_path .exists ():
5454 raise FileNotFoundError (f"Credentials file not found: { creds_path } " )
55-
55+
5656 # Load credentials
5757 scope = [
5858 "https://spreadsheets.google.com/feeds" ,
5959 "https://www.googleapis.com/auth/drive" ,
6060 ]
61-
61+
6262 creds = Credentials .from_service_account_file (str (creds_path ), scopes = scope )
6363 return creds
6464
@@ -79,13 +79,13 @@ def create_or_get_spreadsheet(gc, spreadsheet_id=None, title=None):
7979
8080def export_devices_to_sheet (devices , worksheet , factory_name ):
8181 """Export devices data to Google Sheet.
82-
82+
8383 SECURITY NOTE: This export only includes non-sensitive device metadata:
8484 - Device names, status, targets, apps
8585 - VPN IP addresses (not secrets)
8686 - Creation dates
8787 - Production flags
88-
88+
8989 NO SECRETS EXPORTED:
9090 - No API keys, tokens, or passwords
9191 - No private keys or certificates
@@ -94,7 +94,7 @@ def export_devices_to_sheet(devices, worksheet, factory_name):
9494 if not devices :
9595 print ("No devices to export" )
9696 return
97-
97+
9898 # Define headers - ordered by importance/usefulness
9999 # SECURITY: Only include non-sensitive metadata
100100 headers = [
@@ -117,13 +117,13 @@ def export_devices_to_sheet(devices, worksheet, factory_name):
117117 "Days Since Created" , # Calculated field
118118 "Days Since Last Seen" , # Calculated field
119119 ]
120-
120+
121121 # Prepare data rows
122122 rows = [headers ]
123-
123+
124124 # Calculate days since created for each device
125125 from datetime import datetime as dt
126-
126+
127127 def parse_date (date_str ):
128128 """Parse date string from various formats."""
129129 if not date_str :
@@ -134,18 +134,18 @@ def parse_date(date_str):
134134 except ValueError :
135135 continue
136136 return None
137-
137+
138138 def days_since (date_str ):
139139 """Calculate days since a date string."""
140140 date_obj = parse_date (date_str )
141141 if date_obj :
142142 return str ((dt .now () - date_obj ).days )
143143 return ""
144-
144+
145145 for device in devices :
146146 created_at = device .get ("created_at" , "" )
147147 last_seen = device .get ("last_seen" , "" )
148-
148+
149149 row = [
150150 device .get ("name" , "" ),
151151 device .get ("status" , "" ),
@@ -167,11 +167,11 @@ def days_since(date_str):
167167 days_since (last_seen ),
168168 ]
169169 rows .append (row )
170-
170+
171171 # Clear existing content and add new data
172172 worksheet .clear ()
173173 worksheet .update (values = rows , range_name = "A1" )
174-
174+
175175 # Calculate column letter for last column
176176 def col_letter (col_num ):
177177 """Convert column number (1-based) to letter (A, B, C, ..., Z, AA, AB, ...)"""
@@ -181,74 +181,83 @@ def col_letter(col_num):
181181 result = chr (65 + (col_num % 26 )) + result
182182 col_num //= 26
183183 return result
184-
184+
185185 last_col = col_letter (len (headers ))
186186 header_range = f"A1:{ last_col } 1"
187-
187+
188188 # Format header row (bold, colored background)
189- worksheet .format (header_range , {
190- "textFormat" : {"bold" : True },
191- "backgroundColor" : {"red" : 0.2 , "green" : 0.6 , "blue" : 0.9 },
192- "horizontalAlignment" : "CENTER" ,
193- })
194-
189+ worksheet .format (
190+ header_range ,
191+ {
192+ "textFormat" : {"bold" : True },
193+ "backgroundColor" : {"red" : 0.2 , "green" : 0.6 , "blue" : 0.9 },
194+ "horizontalAlignment" : "CENTER" ,
195+ },
196+ )
197+
195198 # Freeze header row for scrolling
196199 try :
197200 worksheet .freeze (rows = 1 )
198201 except Exception :
199202 pass # Freeze might not be available in all gspread versions
200-
203+
201204 # Add filters to header row (makes spreadsheet filterable and sortable)
202205 # Filters automatically enable sorting on all columns
203206 try :
204207 spreadsheet = worksheet .spreadsheet
205208 # Use the correct API format for setBasicFilter
206- requests = [{
207- "setBasicFilter" : {
208- "filter" : {
209- "range" : {
210- "sheetId" : worksheet .id ,
211- "startRowIndex" : 0 ,
212- "endRowIndex" : len (devices ) + 1 , # Include all data rows
213- "startColumnIndex" : 0 ,
214- "endColumnIndex" : len (headers ),
209+ requests = [
210+ {
211+ "setBasicFilter" : {
212+ "filter" : {
213+ "range" : {
214+ "sheetId" : worksheet .id ,
215+ "startRowIndex" : 0 ,
216+ "endRowIndex" : len (devices ) + 1 , # Include all data rows
217+ "startColumnIndex" : 0 ,
218+ "endColumnIndex" : len (headers ),
219+ }
215220 }
216221 }
217222 }
218- } ]
223+ ]
219224 spreadsheet .batch_update ({"requests" : requests })
220225 print ("✅ Filters added - columns are now sortable and filterable" )
221226 except Exception as e :
222227 print (f"⚠️ Warning: Could not add filters automatically: { e } " )
223228 print (" You can add filters manually: Select header row > Data > Create a filter" )
224229 print (" This will enable sorting and filtering on all columns" )
225-
230+
226231 # Auto-resize columns
227232 try :
228233 worksheet .columns_auto_resize (0 , len (headers ))
229234 except Exception :
230235 pass # Auto-resize might not be available in all versions
231-
236+
232237 # Format specific columns for better readability
233238 if len (devices ) > 0 :
234239 # Status column - bold
235- worksheet .format (f"B2:B{ len (devices ) + 1 } " , {
236- "textFormat" : {"bold" : True },
237- })
238-
240+ worksheet .format (
241+ f"B2:B{ len (devices ) + 1 } " ,
242+ {
243+ "textFormat" : {"bold" : True },
244+ },
245+ )
246+
239247 # Up-to-Date column - bold
240- worksheet .format (f"F2:F{ len (devices ) + 1 } " , {
241- "textFormat" : {"bold" : True },
242- })
243-
248+ worksheet .format (
249+ f"F2:F{ len (devices ) + 1 } " ,
250+ {
251+ "textFormat" : {"bold" : True },
252+ },
253+ )
254+
244255 print (f"✅ Exported { len (devices )} devices to sheet '{ worksheet .title } '" )
245- print (f "📊 Filters enabled on header row - click filter icons to filter/sort" )
256+ print ("📊 Filters enabled on header row - click filter icons to filter/sort" )
246257
247258
248259def main ():
249- parser = argparse .ArgumentParser (
250- description = "Export Foundries devices to Google Sheets"
251- )
260+ parser = argparse .ArgumentParser (description = "Export Foundries devices to Google Sheets" )
252261 parser .add_argument (
253262 "--spreadsheet-id" ,
254263 help = "Google Spreadsheet ID (if not provided, creates a new spreadsheet)" ,
@@ -267,24 +276,24 @@ def main():
267276 "--title" ,
268277 help = "Title for new spreadsheet (if creating new)" ,
269278 )
270-
279+
271280 args = parser .parse_args ()
272-
281+
273282 # Get devices data
274283 print (f"Fetching devices from factory '{ args .factory } '..." )
275284 result = list_foundries_devices (factory = args .factory )
276-
285+
277286 if not result .get ("success" ):
278287 print (f"ERROR: Failed to fetch devices: { result .get ('error' , 'Unknown error' )} " )
279288 sys .exit (1 )
280-
289+
281290 devices = result .get ("devices" , [])
282291 print (f"✅ Retrieved { len (devices )} devices" )
283-
292+
284293 if not devices :
285294 print ("No devices to export" )
286295 return
287-
296+
288297 # Authenticate with Google Sheets
289298 print ("Authenticating with Google Sheets..." )
290299 try :
@@ -298,36 +307,40 @@ def main():
298307 print ("2. Create a project and enable Google Sheets API" )
299308 print ("3. Create a service account and download credentials JSON" )
300309 print ("4. Share your spreadsheet with the service account email" )
301- print ("5. Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root" )
310+ print (
311+ "5. Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root"
312+ )
302313 sys .exit (1 )
303-
314+
304315 # Create or get spreadsheet
305316 if args .spreadsheet_id :
306317 print (f"Opening spreadsheet with ID: { args .spreadsheet_id } " )
307318 spreadsheet = create_or_get_spreadsheet (gc , spreadsheet_id = args .spreadsheet_id )
308319 else :
309- title = args .title or f"Foundries Devices - { args .factory } - { datetime .now ().strftime ('%Y-%m-%d' )} "
320+ title = (
321+ args .title
322+ or f"Foundries Devices - { args .factory } - { datetime .now ().strftime ('%Y-%m-%d' )} "
323+ )
310324 print (f"Creating new spreadsheet: { title } " )
311325 spreadsheet = create_or_get_spreadsheet (gc , title = title )
312326 print (f"✅ Created spreadsheet: { spreadsheet .url } " )
313-
327+
314328 # Get or create worksheet
315329 try :
316330 worksheet = spreadsheet .worksheet (args .sheet_name )
317331 print (f"Using existing sheet: { args .sheet_name } " )
318332 except gspread .exceptions .WorksheetNotFound :
319333 worksheet = spreadsheet .add_worksheet (title = args .sheet_name , rows = 1000 , cols = 20 )
320334 print (f"Created new sheet: { args .sheet_name } " )
321-
335+
322336 # Export data
323337 print (f"Exporting { len (devices )} devices to sheet..." )
324338 export_devices_to_sheet (devices , worksheet , args .factory )
325-
326- print (f "\n ✅ Export complete!" )
339+
340+ print ("\n ✅ Export complete!" )
327341 print (f"📊 Spreadsheet URL: { spreadsheet .url } " )
328342 print (f"📋 Sheet name: { worksheet .title } " )
329343
330344
331345if __name__ == "__main__" :
332346 main ()
333-
0 commit comments