blob: e614962d73c254ff3a5681bfd4346eb52295fa0b [file] [log] [blame]
Matt Tennant92f732c2011-06-17 10:40:23 -07001#!/usr/bin/python2.6
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Support uploading a csv file to a Google Docs spreadsheet."""
7
8import optparse
9import os
Matt Tennant92f732c2011-06-17 10:40:23 -070010
Brian Harring503f3ab2012-03-09 21:39:41 -080011from chromite.lib import gdata_lib
12from chromite.lib import table
13from chromite.lib import operation
14from chromite.lib import upgrade_table as utable
15from chromite.scripts import merge_package_status as mps
Matt Tennant639b6f22011-07-15 17:11:22 -070016
Matt Tennantb5c2b572011-12-15 16:33:32 -080017REAL_SS_KEY = '0AsXDKtaHikmcdEp1dVN1SG1yRU1xZEw1Yjhka2dCSUE'
18TEST_SS_KEY = '0AsXDKtaHikmcdDlQMjI3ZDdPVGc4Rkl3Yk5OLWxjR1E'
Matt Tennantc0efbd62011-10-28 16:31:29 -070019PKGS_WS_NAME = 'Packages'
20DEPS_WS_NAME = 'Dependencies'
Matt Tennant92f732c2011-06-17 10:40:23 -070021
Matt Tennantffed1d52011-07-21 10:01:13 -070022oper = operation.Operation('upload_package_status')
Matt Tennant92f732c2011-06-17 10:40:23 -070023
Matt Tennant3dcd7d32012-01-26 09:39:39 -080024
Matt Tennant92f732c2011-06-17 10:40:23 -070025class Uploader(object):
26 """Uploads portage package status data from csv file to Google spreadsheet."""
27
Matt Tennant3dcd7d32012-01-26 09:39:39 -080028 __slots__ = ('_creds', # gdata_lib.Creds object
29 '_scomm', # gdata_lib.SpreadsheetComm object
30 '_ss_row_cache', # dict with key=pkg, val=SpreadsheetRow obj
31 '_csv_table', # table.Table of csv rows
32 )
Matt Tennant92f732c2011-06-17 10:40:23 -070033
Matt Tennant639b6f22011-07-15 17:11:22 -070034 ID_COL = utable.UpgradeTable.COL_PACKAGE
Matt Tennant3dcd7d32012-01-26 09:39:39 -080035 SS_ID_COL = gdata_lib.PrepColNameForSS(ID_COL)
Matt Tennante3cf99c2012-02-06 11:15:54 -080036 SOURCE = 'Uploaded from CSV'
Matt Tennant92f732c2011-06-17 10:40:23 -070037
Matt Tennantb5c2b572011-12-15 16:33:32 -080038 def __init__(self, creds, table_obj):
39 self._creds = creds
Matt Tennant3dcd7d32012-01-26 09:39:39 -080040 self._csv_table = table_obj
41 self._scomm = None
42 self._ss_row_cache = None
Matt Tennant92f732c2011-06-17 10:40:23 -070043
44 def _GetSSRowForPackage(self, package):
Matt Tennant3dcd7d32012-01-26 09:39:39 -080045 """Return the SpreadsheetRow corresponding to Package=|package|."""
46 if package in self._ss_row_cache:
47 row = self._ss_row_cache[package]
Matt Tennant92f732c2011-06-17 10:40:23 -070048
Matt Tennant3dcd7d32012-01-26 09:39:39 -080049 if isinstance(row, list):
Matt Tennante3cf99c2012-02-06 11:15:54 -080050 raise LookupError('More than one row in spreadsheet with Package=%s' %
Matt Tennant3dcd7d32012-01-26 09:39:39 -080051 package)
Matt Tennant92f732c2011-06-17 10:40:23 -070052
Matt Tennant3dcd7d32012-01-26 09:39:39 -080053 return row
54
55 return None
Matt Tennant92f732c2011-06-17 10:40:23 -070056
Matt Tennant82ea9092011-10-06 18:08:01 -070057 def Upload(self, ss_key, ws_name):
Matt Tennant3dcd7d32012-01-26 09:39:39 -080058 """Upload |_csv_table| to the given Google Spreadsheet.
Matt Tennant92f732c2011-06-17 10:40:23 -070059
60 The spreadsheet is identified the spreadsheet key |ss_key|.
Matt Tennant82ea9092011-10-06 18:08:01 -070061 The worksheet within that spreadsheet is identified by the
62 worksheet name |ws_name|.
Matt Tennant92f732c2011-06-17 10:40:23 -070063 """
Matt Tennant3dcd7d32012-01-26 09:39:39 -080064 if self._scomm:
65 self._scomm.SetCurrentWorksheet(ws_name)
66 else:
67 self._scomm = gdata_lib.SpreadsheetComm()
68 self._scomm.Connect(self._creds, ss_key, ws_name,
69 source='Upload Package Status')
70
71 oper.Notice('Caching rows for worksheet %r.' % self._scomm.ws_name)
72 self._ss_row_cache = self._scomm.GetRowCacheByCol(self.SS_ID_COL)
73
Matt Tennante3cf99c2012-02-06 11:15:54 -080074 oper.Notice('Uploading changes to worksheet "%s" of spreadsheet "%s" now.' %
Matt Tennant3dcd7d32012-01-26 09:39:39 -080075 (self._scomm.ws_name, self._scomm.ss_key))
Matt Tennant92f732c2011-06-17 10:40:23 -070076
Matt Tennante3cf99c2012-02-06 11:15:54 -080077 oper.Info('Details by package: S=Same, C=Changed, A=Added, D=Deleted')
Matt Tennant92f732c2011-06-17 10:40:23 -070078 rows_unchanged, rows_updated, rows_inserted = self._UploadChangedRows()
Matt Tennant92f732c2011-06-17 10:40:23 -070079 rows_deleted, rows_with_owner_deleted = self._DeleteOldRows()
80
Matt Tennante3cf99c2012-02-06 11:15:54 -080081 oper.Notice('Final row stats for worksheet "%s"'
82 ': %d changed, %d added, %d deleted, %d same.' %
Matt Tennant3dcd7d32012-01-26 09:39:39 -080083 (self._scomm.ws_name, rows_updated, rows_inserted,
84 rows_deleted, rows_unchanged))
Matt Tennant92f732c2011-06-17 10:40:23 -070085 if rows_with_owner_deleted:
Matt Tennante3cf99c2012-02-06 11:15:54 -080086 oper.Warning('%d rows with owner entry deleted, see above warnings.' %
Matt Tennantb5c2b572011-12-15 16:33:32 -080087 rows_with_owner_deleted)
Matt Tennant92f732c2011-06-17 10:40:23 -070088 else:
Matt Tennante3cf99c2012-02-06 11:15:54 -080089 oper.Notice('No rows with owner entry were deleted.')
Matt Tennant92f732c2011-06-17 10:40:23 -070090
91 def _UploadChangedRows(self):
92 """Upload all rows in table that need to be changed in spreadsheet."""
Matt Tennant92f732c2011-06-17 10:40:23 -070093 rows_unchanged, rows_updated, rows_inserted = (0, 0, 0)
94
95 # Go over all rows in csv table. Identify existing row by the 'Package'
96 # column. Either update existing row or create new one.
Matt Tennant3dcd7d32012-01-26 09:39:39 -080097 for csv_row in self._csv_table:
Matt Tennant92f732c2011-06-17 10:40:23 -070098 # Seed new row values from csv_row values, with column translation.
Matt Tennantb5c2b572011-12-15 16:33:32 -080099 new_row = dict((gdata_lib.PrepColNameForSS(key),
100 csv_row[key]) for key in csv_row)
Matt Tennant92f732c2011-06-17 10:40:23 -0700101
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800102 # Retrieve row values already in spreadsheet, along with row index.
103 csv_package = csv_row[self.ID_COL]
104 ss_row = self._GetSSRowForPackage(csv_package)
105
Matt Tennant92f732c2011-06-17 10:40:23 -0700106 if ss_row:
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800107 changed = [] # Gather changes for log message.
Matt Tennant92f732c2011-06-17 10:40:23 -0700108
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800109 # Check each key/value in new_row to see if it is different from what
110 # is already in spreadsheet (ss_row). Keep only differences to get
111 # the row delta.
112 row_delta = {}
113 for col in new_row:
114 if col in ss_row:
115 ss_val = ss_row[col]
116 new_val = new_row[col]
117 if (ss_val or new_val) and ss_val != new_val:
Matt Tennante3cf99c2012-02-06 11:15:54 -0800118 changed.append('%s="%s"->"%s"' % (col, ss_val, new_val))
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800119 row_delta[col] = new_val
Matt Tennant92f732c2011-06-17 10:40:23 -0700120
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800121 if row_delta:
122 self._scomm.UpdateRowCellByCell(ss_row.ss_row_num,
123 gdata_lib.PrepRowForSS(row_delta))
Matt Tennant92f732c2011-06-17 10:40:23 -0700124 rows_updated += 1
Matt Tennante3cf99c2012-02-06 11:15:54 -0800125 oper.Info('C %-30s: %s' % (csv_package, ', '.join(changed)))
Matt Tennant92f732c2011-06-17 10:40:23 -0700126 else:
127 rows_unchanged += 1
Matt Tennante3cf99c2012-02-06 11:15:54 -0800128 oper.Info('S %-30s:' % csv_package)
Matt Tennant92f732c2011-06-17 10:40:23 -0700129 else:
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800130 self._scomm.InsertRow(gdata_lib.PrepRowForSS(new_row))
Matt Tennant92f732c2011-06-17 10:40:23 -0700131 rows_inserted += 1
132 row_descr_list = []
133 for col in sorted(new_row.keys()):
134 if col != self.ID_COL:
Matt Tennante3cf99c2012-02-06 11:15:54 -0800135 row_descr_list.append('%s="%s"' % (col, new_row[col]))
136 oper.Info('A %-30s: %s' % (csv_package, ', '.join(row_descr_list)))
Matt Tennant92f732c2011-06-17 10:40:23 -0700137
138 return (rows_unchanged, rows_updated, rows_inserted)
139
140 def _DeleteOldRows(self):
141 """Delete all rows from spreadsheet that not found in table."""
Matt Tennante3cf99c2012-02-06 11:15:54 -0800142 oper.Notice('Checking for rows in worksheet that should be deleted now.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700143
144 rows_deleted, rows_with_owner_deleted = (0, 0)
145
146 # Also need to delete rows in spreadsheet that are not in csv table.
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800147 ss_rows = self._scomm.GetRows()
148 for ss_row in ss_rows:
149 ss_package = gdata_lib.ScrubValFromSS(ss_row[self.SS_ID_COL])
Matt Tennant92f732c2011-06-17 10:40:23 -0700150
151 # See whether this row is in csv table.
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800152 csv_rows = self._csv_table.GetRowsByValue({ self.ID_COL: ss_package })
Matt Tennant92f732c2011-06-17 10:40:23 -0700153 if not csv_rows:
154 # Row needs to be deleted from spreadsheet.
155 owner_val = None
156 owner_notes_val = None
157 row_descr_list = []
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800158 for col in sorted(ss_row.keys()):
Matt Tennant92f732c2011-06-17 10:40:23 -0700159 if col == 'owner':
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800160 owner_val = ss_row[col]
Matt Tennant92f732c2011-06-17 10:40:23 -0700161 if col == 'ownernotes':
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800162 owner_notes_val = ss_row[col]
Matt Tennant92f732c2011-06-17 10:40:23 -0700163
164 # Don't include ID_COL value in description, it is in prefix already.
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800165 if col != self.SS_ID_COL:
166 val = ss_row[col]
Matt Tennante3cf99c2012-02-06 11:15:54 -0800167 row_descr_list.append('%s="%s"' % (col, val))
Matt Tennant92f732c2011-06-17 10:40:23 -0700168
Matt Tennante3cf99c2012-02-06 11:15:54 -0800169 oper.Info('D %-30s: %s' % (ss_package, ', '.join(row_descr_list)))
Matt Tennant92f732c2011-06-17 10:40:23 -0700170 if owner_val or owner_notes_val:
171 rows_with_owner_deleted += 1
Matt Tennante3cf99c2012-02-06 11:15:54 -0800172 oper.Notice('WARNING: Deleting spreadsheet row with owner entry:\n' +
173 ' %-30s: Owner=%s, Owner Notes=%s' %
Matt Tennantb5c2b572011-12-15 16:33:32 -0800174 (ss_package, owner_val, owner_notes_val))
Matt Tennant92f732c2011-06-17 10:40:23 -0700175
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800176 self._scomm.DeleteRow(ss_row.ss_row_obj)
Matt Tennant92f732c2011-06-17 10:40:23 -0700177 rows_deleted += 1
178
179 return (rows_deleted, rows_with_owner_deleted)
180
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800181
Matt Tennant92f732c2011-06-17 10:40:23 -0700182def LoadTable(table_file):
183 """Load csv |table_file| into a table. Return table."""
Matt Tennante3cf99c2012-02-06 11:15:54 -0800184 oper.Notice('Loading csv table from "%s".' % (table_file))
Matt Tennant92f732c2011-06-17 10:40:23 -0700185 csv_table = table.Table.LoadFromCSV(table_file)
186 return csv_table
187
Matt Tennant3dcd7d32012-01-26 09:39:39 -0800188
Matt Tennante3cf99c2012-02-06 11:15:54 -0800189def PrepareCreds(cred_file, token_file, email, password):
190 """Return a Creds object from given credentials.
191
192 If |email| is given, the Creds object will contain that |email|
193 and either the given |password| or one entered at a prompt.
194
195 Otherwise, if |token_file| is given then the Creds object will have
196 the auth_token from that file.
197
198 Otherwise, if |cred_file| is given then the Creds object will have
199 the email/password from that file.
200 """
201
202 creds = gdata_lib.Creds()
203
204 if email:
205 creds.SetCreds(email, password)
206 elif token_file and os.path.exists(token_file):
207 creds.LoadAuthToken(token_file)
208 elif cred_file and os.path.exists(cred_file):
209 creds.LoadCreds(cred_file)
210
211 return creds
212
Brian Harring30675052012-02-29 12:18:22 -0800213def main(argv):
Matt Tennant92f732c2011-06-17 10:40:23 -0700214 """Main function."""
215 usage = 'Usage: %prog [options] csv_file'
216 parser = optparse.OptionParser(usage=usage)
Matt Tennante3cf99c2012-02-06 11:15:54 -0800217 parser.add_option('--auth-token-file', dest='token_file', type='string',
218 action='store', default=None,
219 help='File for reading/writing Docs auth token.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700220 parser.add_option('--cred-file', dest='cred_file', type='string',
221 action='store', default=None,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800222 help='File for reading/writing Docs login email/password.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700223 parser.add_option('--email', dest='email', type='string',
224 action='store', default=None,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800225 help='Email for Google Doc user')
Matt Tennant92f732c2011-06-17 10:40:23 -0700226 parser.add_option('--password', dest='password', type='string',
227 action='store', default=None,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800228 help='Password for Google Doc user')
Matt Tennant639b6f22011-07-15 17:11:22 -0700229 parser.add_option('--ss-key', dest='ss_key', type='string',
230 action='store', default=None,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800231 help='Key of spreadsheet to upload to')
Matt Tennant92f732c2011-06-17 10:40:23 -0700232 parser.add_option('--test-spreadsheet', dest='test_ss',
233 action='store_true', default=False,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800234 help='Upload to the testing spreadsheet.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700235 parser.add_option('--verbose', dest='verbose',
236 action='store_true', default=False,
Matt Tennante3cf99c2012-02-06 11:15:54 -0800237 help='Show details about packages.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700238
Brian Harring30675052012-02-29 12:18:22 -0800239 (options, args) = parser.parse_args(argv)
Matt Tennant92f732c2011-06-17 10:40:23 -0700240
Matt Tennantb5c2b572011-12-15 16:33:32 -0800241 oper.verbose = options.verbose
242
Matt Tennant92f732c2011-06-17 10:40:23 -0700243 if len(args) < 1:
244 parser.print_help()
Matt Tennante3cf99c2012-02-06 11:15:54 -0800245 oper.Die('One csv_file is required.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700246
247 # If email or password provided, the other is required. If neither is
Matt Tennante3cf99c2012-02-06 11:15:54 -0800248 # provided, then either token_file or cred_file must be provided and
249 # be a real file.
Matt Tennant92f732c2011-06-17 10:40:23 -0700250 if options.email or options.password:
251 if not (options.email and options.password):
252 parser.print_help()
Matt Tennante3cf99c2012-02-06 11:15:54 -0800253 oper.Die('The email/password options must be used together.')
254 elif not ((options.cred_file and os.path.exists(options.cred_file)) or
255 (options.token_file and os.path.exists(options.token_file))):
Matt Tennant92f732c2011-06-17 10:40:23 -0700256 parser.print_help()
Matt Tennante3cf99c2012-02-06 11:15:54 -0800257 oper.Die('Without email/password, cred-file or auth-token-file'
258 'must exist.')
Matt Tennant92f732c2011-06-17 10:40:23 -0700259
Matt Tennant639b6f22011-07-15 17:11:22 -0700260 # --ss-key and --test-spreadsheet are mutually exclusive.
261 if options.ss_key and options.test_ss:
262 parser.print_help()
Matt Tennante3cf99c2012-02-06 11:15:54 -0800263 oper.Die('Cannot specify --ss-key and --test-spreadsheet together.')
Matt Tennant639b6f22011-07-15 17:11:22 -0700264
Matt Tennantb5c2b572011-12-15 16:33:32 -0800265 # Prepare credentials for spreadsheet access.
Matt Tennante3cf99c2012-02-06 11:15:54 -0800266 creds = PrepareCreds(options.cred_file, options.token_file,
267 options.email, options.password)
Matt Tennantb5c2b572011-12-15 16:33:32 -0800268
Matt Tennant92f732c2011-06-17 10:40:23 -0700269 # Load the given csv file.
270 csv_table = LoadTable(args[0])
271
Matt Tennant639b6f22011-07-15 17:11:22 -0700272 # Prepare table for upload.
273 mps.FinalizeTable(csv_table)
274
Matt Tennant92f732c2011-06-17 10:40:23 -0700275 # Prepare the Google Doc client for uploading.
Matt Tennantb5c2b572011-12-15 16:33:32 -0800276 uploader = Uploader(creds, csv_table)
Matt Tennant92f732c2011-06-17 10:40:23 -0700277
Matt Tennant639b6f22011-07-15 17:11:22 -0700278 ss_key = options.ss_key
Matt Tennant82ea9092011-10-06 18:08:01 -0700279 ws_names = [PKGS_WS_NAME, DEPS_WS_NAME]
Matt Tennant639b6f22011-07-15 17:11:22 -0700280 if not ss_key:
281 if options.test_ss:
282 ss_key = TEST_SS_KEY # For testing with backup spreadsheet
283 else:
284 ss_key = REAL_SS_KEY
Matt Tennant82ea9092011-10-06 18:08:01 -0700285
286 for ws_name in ws_names:
287 uploader.Upload(ss_key, ws_name=ws_name)
Matt Tennant92f732c2011-06-17 10:40:23 -0700288
Matt Tennantf6e9f1f2012-03-01 17:23:05 -0800289 # If cred_file given and new credentials were used then write
Matt Tennantb5c2b572011-12-15 16:33:32 -0800290 # credentials out to that location.
Matt Tennantf6e9f1f2012-03-01 17:23:05 -0800291 if options.cred_file:
292 creds.StoreCredsIfNeeded(options.cred_file)
Matt Tennantb5c2b572011-12-15 16:33:32 -0800293
Matt Tennantf6e9f1f2012-03-01 17:23:05 -0800294 # If token_file path given and new auth token was used then
295 # write auth_token out to that location.
296 if options.token_file:
297 creds.StoreAuthTokenIfNeeded(options.token_file)