diff options
-rw-r--r-- | lib/gdata_lib.py | 24 | ||||
-rw-r--r-- | scripts/sync_package_status.py | 38 | ||||
-rw-r--r-- | scripts/sync_package_status_unittest.py | 1 |
3 files changed, 58 insertions, 5 deletions
diff --git a/lib/gdata_lib.py b/lib/gdata_lib.py index 8f71b7630..77452a79f 100644 --- a/lib/gdata_lib.py +++ b/lib/gdata_lib.py @@ -16,6 +16,7 @@ import xml.dom.minidom # pylint: disable=W0404 import gdata.projecthosting.client +import gdata.service import gdata.spreadsheet.service from chromite.lib import operation @@ -361,6 +362,20 @@ class SpreadsheetRow(dict): raise TypeError('deleting item in SpreadsheetRow not supported') +class SpreadsheetError(RuntimeError): + """Base class for spreadsheet communication errors.""" + +def ReadWriteDecorator(func): + """Raise SpreadsheetWriteError if appropriate.""" + def f(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except gdata.service.RequestError as ex: + raise SpreadsheetError(str(ex)) + + f.__name__ = func.__name__ + return f + class SpreadsheetComm(object): """Class to manage communication with one Google Spreadsheet worksheet.""" @@ -487,6 +502,7 @@ class SpreadsheetComm(object): oper.Die('Unable to find worksheet "%s" in spreadsheet "%s"' % (ws_name, ss_key)) + @ReadWriteDecorator def GetColumns(self): """Return tuple of column names in worksheet. @@ -494,6 +510,7 @@ class SpreadsheetComm(object): """ return self.columns + @ReadWriteDecorator def GetColumnIndex(self, colName): """Get the column index (starting at 1) for column |colName|""" try: @@ -502,10 +519,12 @@ class SpreadsheetComm(object): except ValueError: return None + @ReadWriteDecorator def GetRows(self): """Return tuple of SpreadsheetRow objects in order.""" return self.rows + @ReadWriteDecorator def GetRowCacheByCol(self, column): """Return a dict for looking up rows by value in |column|. @@ -530,11 +549,13 @@ class SpreadsheetComm(object): return row_cache + @ReadWriteDecorator def InsertRow(self, row): """Insert |row| at end of spreadsheet.""" self.gd_client.InsertRow(row, self.ss_key, self.ws_key) self._ClearCache(keep_columns=True) + @ReadWriteDecorator def UpdateRowCellByCell(self, rowIx, row): """Replace cell values in row at |rowIx| with those in |row| dict.""" for colName in row: @@ -543,16 +564,19 @@ class SpreadsheetComm(object): self.ReplaceCellValue(rowIx, colIx, row[colName]) self._ClearCache(keep_columns=True) + @ReadWriteDecorator def DeleteRow(self, ss_row): """Delete the given |ss_row| (must be original spreadsheet row object.""" self.gd_client.DeleteRow(ss_row) self._ClearCache(keep_columns=True) + @ReadWriteDecorator def ReplaceCellValue(self, rowIx, colIx, val): """Replace cell value at |rowIx| and |colIx| with |val|""" self.gd_client.UpdateCell(rowIx, colIx, val, self.ss_key, self.ws_key) self._ClearCache(keep_columns=True) + @ReadWriteDecorator def ClearCellValue(self, rowIx, colIx): """Clear cell value at |rowIx| and |colIx|""" self.ReplaceCellValue(rowIx, colIx, None) diff --git a/scripts/sync_package_status.py b/scripts/sync_package_status.py index 8033c1a68..539240f37 100644 --- a/scripts/sync_package_status.py +++ b/scripts/sync_package_status.py @@ -293,14 +293,35 @@ class Syncer(object): if not self.pretend: oper.Info('Creating Tracker issue for package %s with details:\n%s' % (pkg, issue)) + + # Before actually creating the Tracker issue, confirm that writing + # to this spreadsheet row is going to work. + try: + self.scomm.ClearCellValue(rowIx, self.tracker_col_ix) + except gdata_lib.SpreadsheetError as ex: + oper.Error('Unable to write to row %d, package %r. Aborting issue' + ' creation. Error was:\n%s' % (rowIx, pkg, ex)) + raise SyncError + issue_id = self.tcomm.CreateTrackerIssue(issue) oper.Info('Inserting new Tracker issue %d for package %s' % (issue_id, pkg)) ss_issue_val = self._GenSSLinkToIssue(issue_id) - self.scomm.ReplaceCellValue(rowIx, self.tracker_col_ix, ss_issue_val) - oper.Notice('Created Tracker issue %d for row %d, package %r' % - (issue_id, rowIx, pkg)) + # This really should not fail since write access was checked before. + try: + self.scomm.ReplaceCellValue(rowIx, self.tracker_col_ix, ss_issue_val) + oper.Notice('Created Tracker issue %d for row %d, package %r' % + (issue_id, rowIx, pkg)) + except gdata_lib.SpreadsheetError as ex: + oper.Error('Failed to write link to new issue %d into' + ' row %d, package %r:\n%s' % + (issue_id, rowIx, pkg, ex)) + oper.Error('This means that the spreadsheet will have no record of' + ' this Tracker Issue and will create one again next time' + ' unless the spreadsheet is edited by hand!') + raise SyncError + else: oper.Notice('Would create and insert issue for row %d, package %r' % (rowIx, pkg)) @@ -315,8 +336,15 @@ class Syncer(object): pkg = row[COL_PACKAGE] if not self.pretend: - oper.Notice('Clearing Tracker issue for package %s' % pkg) - self.scomm.ClearCellValue(rowIx, self.tracker_col_ix) + try: + self.scomm.ClearCellValue(rowIx, self.tracker_col_ix) + oper.Notice('Cleared Tracker issue from row %d, package %r' % + (rowIx, pkg)) + except gdata_lib.SpreadsheetError as ex: + oper.Error('Error while clearing Tracker issue for' + ' row %d, package %r:\n%s' % (rowIx, pkg, ex)) + raise SyncError + else: oper.Notice('Would clear Tracker issue from row %d, package %r' % (rowIx, pkg)) diff --git a/scripts/sync_package_status_unittest.py b/scripts/sync_package_status_unittest.py index 5f14b3487..5e3ab2091 100644 --- a/scripts/sync_package_status_unittest.py +++ b/scripts/sync_package_status_unittest.py @@ -467,6 +467,7 @@ class SyncerTest(test_lib.MoxTestCase): ss_issue_val = 'Hyperlink%d' % issue_id # Replay script + mocked_scomm.ClearCellValue(row_ix, mocked_syncer.tracker_col_ix) mocked_tcomm.CreateTrackerIssue(issue).AndReturn(issue_id) mocked_syncer._GenSSLinkToIssue(issue_id).AndReturn(ss_issue_val) mocked_scomm.ReplaceCellValue(row_ix, mocked_syncer.tracker_col_ix, |