Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(321)

Side by Side Diff: tracker_spreadsheet_sync

Issue 6287010: crosutils:Remove dead scripts (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/crosutils.git@master
Patch Set: Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « save_pinned_deps ('k') | upgrade.d/1_init » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
6
7 # For Spreadsheets:
8 try:
9 from xml.etree import ElementTree
10 except ImportError:
11 from elementtree import ElementTree
12 import gdata.spreadsheet.service
13 import gdata.service
14 import atom.service
15 import gdata.spreadsheet
16 import atom
17
18 # For Issue Tracker:
19 import gdata.projecthosting.client
20 import gdata.projecthosting.data
21 import gdata.gauth
22 import gdata.client
23 import gdata.data
24 import atom.http_core
25 import atom.core
26
27 # For this script:
28 import getpass
29 from optparse import OptionParser
30 import pickle
31 from sets import Set
32
33 # Settings
34 credentials_store = 'creds.dat'
35
36 class Merger(object):
37 def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project,
38 debug, pretend):
39 self.ss_key = ss_key
40 self.ss_ws_key = ss_ws_key
41 self.tracker_message = tracker_message
42 self.tracker_project = tracker_project
43 self.debug_enabled = debug
44 self.pretend = pretend
45 self.user_agent = 'adlr-tracker-spreadsheet-merger'
46 self.it_keys = ['id', 'owner', 'status', 'title']
47
48 def debug(self, message):
49 """Prints message if debug mode is set."""
50 if self.debug_enabled:
51 print message
52
53 def print_feed(self, feed):
54 'Handy for debugging'
55 for i, entry in enumerate(feed.entry):
56 print 'id:', entry.id
57 if isinstance(feed, gdata.spreadsheet.SpreadsheetsCellsFeed):
58 print '%s %s\n' % (entry.title.text, entry.content.text)
59 elif isinstance(feed, gdata.spreadsheet.SpreadsheetsListFeed):
60 print '%s %s %s' % (i, entry.title.text, entry.content.text)
61 # Print this row's value for each column (the custom dictionary is
62 # built using the gsx: elements in the entry.)
63 print 'Contents:'
64 for key in entry.custom:
65 print ' %s: %s' % (key, entry.custom[key].text)
66 print '\n',
67 else:
68 print '%s %s\n' % (i, entry.title.text)
69
70
71 def tracker_login(self):
72 """Logs user into Tracker, using cached credentials if possible.
73 Saves credentials after login."""
74 self.it_client = gdata.projecthosting.client.ProjectHostingClient()
75 self.it_client.source = self.user_agent
76
77 self.load_creds()
78
79 if self.tracker_token and self.tracker_user:
80 print 'Using existing credential for tracker login'
81 self.it_client.auth_token = self.tracker_token
82 else:
83 self.tracker_user = raw_input('Issue Tracker Login:')
84 password = getpass.getpass('Password:')
85 self.it_client.ClientLogin(self.tracker_user, password,
86 source=self.user_agent, service='code',
87 account_type='GOOGLE')
88 self.tracker_token = self.it_client.auth_token
89 self.store_creds()
90
91 def spreadsheet_login(self):
92 """Logs user into Google Spreadsheets, using cached credentials if possible.
93 Saves credentials after login."""
94 self.gd_client = gdata.spreadsheet.service.SpreadsheetsService()
95 self.gd_client.source = self.user_agent
96
97 self.load_creds()
98 if self.docs_token:
99 print 'Using existing credential for docs login'
100 self.gd_client.SetClientLoginToken(self.docs_token)
101 else:
102 self.gd_client.email = raw_input('Google Docs Login:')
103 self.gd_client.password = getpass.getpass('Password:')
104 self.gd_client.ProgrammaticLogin()
105 self.docs_token = self.gd_client.GetClientLoginToken()
106 self.store_creds()
107
108 def fetch_spreadsheet_issues(self):
109 """Fetches all issues from the user-specified spreadsheet. Returns
110 them as an array or dictionaries."""
111 feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key)
112 issues = []
113 for entry in feed.entry:
114 issue = {}
115 for key in entry.custom:
116 issue[key] = entry.custom[key].text
117 issue['__raw_entry'] = entry
118 issues.append(issue)
119 return issues
120
121 def ids_for_spreadsheet_issues(self, ss_issues):
122 """Returns a Set of strings, each string an id from ss_issues"""
123 ret = Set()
124 for ss_issue in ss_issues:
125 ret.add(ss_issue['id'])
126 return ret
127
128 def tracker_issues_for_query_feed(self, feed):
129 """Converts a feed object from a query to a list of tracker issue
130 dictionaries."""
131 issues = []
132 for issue in feed.entry:
133 issue_dict = {}
134 issue_dict['labels'] = [label.text for label in issue.label]
135 issue_dict['id'] = issue.id.text.split('/')[-1]
136 issue_dict['title'] = issue.title.text
137 issue_dict['status'] = issue.status.text
138 if issue.owner:
139 issue_dict['owner'] = issue.owner.username.text
140 issues.append(issue_dict)
141 return issues
142
143 def fetch_tracker_issues(self, ss_issues):
144 """Fetches all relevant issues from traacker and returns them as an array
145 of dictionaries. Relevance is:
146 - has an ID that's in ss_issues, OR
147 - (is Area=Installer AND status is open).
148 Open status is one of: Unconfirmed, Untriaged, Available, Assigned,
149 Started, Upstream"""
150 issues = []
151 got_results = True
152 index = 1
153 while got_results:
154 query = gdata.projecthosting.client.Query(label='Area-Installer',
155 max_results=50,
156 start_index=index)
157 feed = self.it_client.get_issues('chromium-os', query=query)
158 if not feed.entry:
159 got_results = False
160 index = index + len(feed.entry)
161 issues.extend(self.tracker_issues_for_query_feed(feed))
162 # Now, remove issues that are open or in ss_issues.
163 ss_ids = self.ids_for_spreadsheet_issues(ss_issues)
164 open_statuses = ['Unconfirmed', 'Untriaged', 'Available', 'Assigned',
165 'Started', 'Upstream']
166 new_issues = []
167 for issue in issues:
168 if issue['status'] in open_statuses or issue['id'] in ss_ids:
169 new_issues.append(issue)
170 # Remove id from ss_ids, if it's there
171 ss_ids.discard(issue['id'])
172 issues = new_issues
173
174 # Now, for each ss_id that didn't turn up in the query, explicitly add it
175 for id_ in ss_ids:
176 query = gdata.projecthosting.client.Query(issue_id=id_,
177 max_results=50,
178 start_index=index)
179 feed = self.it_client.get_issues('chromium-os', query=query)
180 if not feed.entry:
181 print 'No result for id', id_
182 continue
183 issues.extend(self.tracker_issues_for_query_feed(feed))
184
185 return issues
186
187 def store_creds(self):
188 """Stores login credentials to disk."""
189 obj = {}
190 if self.docs_token:
191 obj['docs_token'] = self.docs_token
192 if self.tracker_token:
193 obj['tracker_token'] = self.tracker_token
194 if self.tracker_user:
195 obj['tracker_user'] = self.tracker_user
196 try:
197 f = open(credentials_store, 'w')
198 pickle.dump(obj, f)
199 f.close()
200 except IOError:
201 print 'Unable to store credentials'
202
203 def load_creds(self):
204 """Loads login credentials from disk."""
205 self.docs_token = None
206 self.tracker_token = None
207 self.tracker_user = None
208 try:
209 f = open(credentials_store, 'r')
210 obj = pickle.load(f)
211 f.close()
212 if obj.has_key('docs_token'):
213 self.docs_token = obj['docs_token']
214 if obj.has_key('tracker_token'):
215 self.tracker_token = obj['tracker_token']
216 if obj.has_key('tracker_user'):
217 self.tracker_user = obj['tracker_user']
218 except IOError:
219 print 'Unable to load credentials'
220
221 def browse(self):
222 """Browses Spreadsheets to help the user find the spreadsheet and
223 worksheet keys"""
224 print 'Browsing spreadsheets...'
225
226 if self.ss_key and self.ss_ws_key:
227 print 'You already passed in --ss_key and --ss_ws_key. No need to browse.'
228 return
229
230 print 'Logging in...'
231 self.spreadsheet_login()
232
233 if not self.ss_key:
234 print 'Fetching spreadsheets...'
235 feed = self.gd_client.GetSpreadsheetsFeed()
236 print ''
237 print 'Spreadsheet key - Title'
238 for entry in feed.entry:
239 key = entry.id.text.split('/')[-1]
240 title = entry.title.text
241 print '"%s" - "%s"' % (key, title)
242 print ''
243 print 'Done. Rerun with --ss_key=KEY to browse a list of worksheet keys.'
244 else:
245 print 'Fetching worksheets for spreadsheet', self.ss_key
246 feed = self.gd_client.GetWorksheetsFeed(self.ss_key)
247 for entry in feed.entry:
248 key = entry.id.text.split('/')[-1]
249 title = entry.title.text
250 print ''
251 print 'Worksheet key - Title'
252 print '"%s" - "%s"' % (key, title)
253 print ''
254 print 'Done. You now have keys for --ss_key and --ss_ws_key.'
255
256 def tracker_issue_for_id(self, issues, id_):
257 """Returns the element of issues which has id_ for the key 'id'"""
258 for issue in issues:
259 if issue['id'] == id_:
260 return issue
261 return None
262
263 def spreadsheet_issue_to_tracker_dict(self, ss_issue):
264 """Converts a spreadsheet issue to the dict format that is used to
265 represent a tracker issue."""
266 ret = {}
267 ret['project'] = self.tracker_project
268 ret['title'] = ss_issue['title']
269 ret['summary'] = self.tracker_message
270 ret['owner'] = ss_issue['owner']
271 if ss_issue.get('status') is not None:
272 ret['status'] = ss_issue['status']
273 ret['labels'] = []
274 for (key, value) in ss_issue.items():
275 if key.endswith('-') and (value is not None):
276 ret['labels'].append(key.title() + value)
277 return ret
278
279 def label_from_prefix(self, prefix, corpus):
280 """Given a corpus (array of lable strings), return the first label
281 that begins with the specified prefix."""
282 for label in corpus:
283 if label.startswith(prefix):
284 return label
285 return None
286
287 def update_spreadsheet_issue_to_tracker_dict(self, ss_issue, t_issue):
288 """Updates a given tracker issue with data from the spreadsheet issue."""
289 ret = {}
290 ret['title'] = ss_issue['title']
291 ret['id'] = ss_issue['id']
292 ret['summary'] = self.tracker_message
293 if ss_issue['status'] != t_issue['status']:
294 ret['status'] = ss_issue['status']
295
296 if ss_issue.get('owner'):
297 if (not t_issue.has_key('owner')) or \
298 (ss_issue['owner'] != t_issue['owner']):
299 ret['owner'] = ss_issue['owner']
300 # labels
301 ret['labels'] = []
302 for (key, value) in ss_issue.items():
303 caps_key = key.title()
304 if not caps_key.endswith('-'):
305 continue
306 ss_label = None
307 if value:
308 ss_label = caps_key + value.title()
309 t_label = self.label_from_prefix(caps_key, t_issue['labels'])
310
311 if t_label is None and ss_label is None:
312 # Nothing
313 continue
314
315 if (t_label is not None) and \
316 ((ss_label is None) or (ss_label != t_label)):
317 ret['labels'].append('-' + t_label)
318
319 if (ss_label is not None) and \
320 ((t_label is None) or (t_label != ss_label)):
321 ret['labels'].append(ss_label)
322 return ret
323
324 def tracker_issue_has_changed(self, t_issue, ss_issue):
325 """Returns True iff ss_issue indicates changes in t_issue that need to be
326 committed up to the Issue Tracker."""
327 if t_issue is None:
328 return True
329 potential_commit = \
330 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue)
331
332 if potential_commit.has_key('status') or \
333 potential_commit.has_key('owner') or \
334 (len(potential_commit['labels']) > 0):
335 return True
336 if potential_commit['title'] != t_issue['title']:
337 return True
338 return False
339
340 def spreadsheet_to_tracker_commits(self, ss_issues, t_issues):
341 """Given the current state of all spreadsheet issues and tracker issues,
342 returns a list of all commits that need to go to tracker to get it in
343 line with the spreadsheet."""
344 ret = []
345 for ss_issue in ss_issues:
346 t_issue = self.tracker_issue_for_id(t_issues, ss_issue['id'])
347 commit = {}
348 # TODO see if an update is needed at all
349 if t_issue is None:
350 commit['type'] = 'append'
351 commit['dict'] = self.spreadsheet_issue_to_tracker_dict(ss_issue)
352 commit['__ss_issue'] = ss_issue
353 else:
354 if not self.tracker_issue_has_changed(t_issue, ss_issue):
355 continue
356 commit['type'] = 'update'
357 commit['dict'] = \
358 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue)
359 ret.append(commit)
360 return ret
361
362 def fetch_issues(self):
363 """Logs into Docs/Tracker, and fetches spreadsheet and tracker issues"""
364 print 'Logging into Docs...'
365 self.spreadsheet_login()
366 print 'Logging into Tracker...'
367 self.tracker_login()
368
369 print 'Fetching spreadsheet issues...'
370 ss_issues = self.fetch_spreadsheet_issues()
371 self.debug('Spreadsheet issues: %s' % ss_issues)
372 print 'Fetching tracker issues...'
373 t_issues = self.fetch_tracker_issues(ss_issues)
374 self.debug('Tracker issues: %s' % t_issues)
375 return (t_issues, ss_issues)
376
377 def spreadsheet_to_tracker(self):
378 """High-level function to manage migrating data from the spreadsheet
379 to Tracker."""
380 (t_issues, ss_issues) = self.fetch_issues()
381 print 'Calculating deltas...'
382 commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues)
383 self.debug('got commits: %s' % commits)
384 if not commits:
385 print 'No deltas. Done.'
386 return
387
388 for commit in commits:
389 dic = commit['dict']
390 labels = dic.get('labels')
391 owner = dic.get('owner')
392 status = dic.get('status')
393
394 if commit['type'] == 'append':
395 print 'Creating new tracker issue...'
396 if self.pretend:
397 print '(Skipping because --pretend is set)'
398 continue
399 created = self.it_client.add_issue(self.tracker_project,
400 dic['title'],
401 self.tracker_message,
402 self.tracker_user,
403 labels=labels,
404 owner=owner,
405 status=status)
406 issue_id = created.id.text.split('/')[-1]
407 print 'Created issue with id:', issue_id
408 print 'Write id back to spreadsheet row...'
409 raw_entry = commit['__ss_issue']['__raw_entry']
410 ss_issue = commit['__ss_issue']
411 del ss_issue['__raw_entry']
412 ss_issue.update({'id': issue_id})
413 self.gd_client.UpdateRow(raw_entry, ss_issue)
414 print 'Done.'
415 else:
416 print 'Updating issue with id:', dic['id']
417 if self.pretend:
418 print '(Skipping because --pretend is set)'
419 continue
420 self.it_client.update_issue(self.tracker_project,
421 dic['id'],
422 self.tracker_user,
423 comment=self.tracker_message,
424 status=status,
425 owner=owner,
426 labels=labels)
427 print 'Done.'
428
429 def spreadsheet_issue_for_id(self, issues, id_):
430 """Given the array of spreadsheet issues, return the first one that
431 has id_ for the key 'id'."""
432 for issue in issues:
433 if issue['id'] == id_:
434 return issue
435 return None
436
437 def value_for_key_in_labels(self, label_array, prefix):
438 """Given an array of labels and a prefix, return the non-prefix part
439 of the first label that has that prefix. E.g. if label_array is
440 ["Mstone-R7", "Area-Installer"] and prefix is "Area-", returns
441 "Installer"."""
442 for label in label_array:
443 if label.startswith(prefix):
444 return label[len(prefix):]
445 return None
446
447 def tracker_issue_to_spreadsheet_issue(self, t_issue, ss_keys):
448 """Converts a tracker issue to the format used by spreadsheet, given
449 the row headings ss_keys."""
450 new_row = {}
451 for key in ss_keys:
452 if key.endswith('-'):
453 # label
454 new_row[key] = self.value_for_key_in_labels(t_issue['labels'],
455 key.title())
456 # Special cases
457 if key in self.it_keys and key in t_issue:
458 new_row[key] = t_issue[key]
459 return new_row
460
461 def spreadsheet_row_needs_update(self, ss_issue, t_issue):
462 """Returns True iff the spreadsheet issue passed in needs to be updated
463 to match data in the tracker issue."""
464 new_ss_issue = self.tracker_issue_to_spreadsheet_issue(t_issue,
465 ss_issue.keys())
466 for key in new_ss_issue.keys():
467 if not ss_issue.has_key(key):
468 continue
469 if new_ss_issue[key] != ss_issue[key]:
470 return True
471 return False
472
473 def tracker_to_spreadsheet_commits(self, t_issues, ss_issues):
474 """Given the current set of spreadsheet and tracker issues, computes
475 commits needed to go to Spreadsheets to get the spreadsheet in line
476 with what's in Tracker."""
477 ret = []
478 keys = ss_issues[0].keys()
479 for t_issue in t_issues:
480 commit = {}
481 ss_issue = self.spreadsheet_issue_for_id(ss_issues, t_issue['id'])
482 if ss_issue is None:
483 # New issue
484 commit['new_row'] = self.tracker_issue_to_spreadsheet_issue(t_issue,
485 keys)
486 commit['type'] = 'append'
487 elif self.spreadsheet_row_needs_update(ss_issue, t_issue):
488 commit['__raw_entry'] = ss_issue['__raw_entry']
489 del ss_issue['__raw_entry']
490 ss_issue.update(self.tracker_issue_to_spreadsheet_issue(t_issue, keys))
491 commit['dict'] = ss_issue
492 commit['type'] = 'update'
493 else:
494 continue
495 ret.append(commit)
496 return ret
497
498 def tracker_to_spreadsheet(self):
499 """High-level function to migrate data from Tracker to the spreadsheet."""
500 (t_issues, ss_issues) = self.fetch_issues()
501 if len(ss_issues) == 0:
502 raise Exception('Error: must have at least one non-header row in '\
503 'spreadsheet')
504 return
505 ss_keys = ss_issues[0].keys()
506
507 print 'Calculating deltas...'
508 ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues)
509 self.debug('commits: %s' % ss_commits)
510 if not ss_commits:
511 print 'Nothing to commit.'
512 return
513 print 'Committing...'
514 for commit in ss_commits:
515 self.debug('Operating on commit: %s' % commit)
516 if commit['type'] == 'append':
517 print 'Appending new row...'
518 if not self.pretend:
519 self.gd_client.InsertRow(commit['new_row'],
520 self.ss_key, self.ss_ws_key)
521 else:
522 print '(Skipped because --pretend set)'
523 if commit['type'] == 'update':
524 print 'Updating row...'
525 if not self.pretend:
526 self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict'])
527 else:
528 print '(Skipped because --pretend set)'
529 print 'Done.'
530
531 def main():
532 class PureEpilogOptionParser(OptionParser):
533 def format_epilog(self, formatter):
534 return self.epilog
535
536 parser = PureEpilogOptionParser()
537 parser.add_option('-a', '--action', dest='action', metavar='ACTION',
538 help='Action to perform')
539 parser.add_option('-d', '--debug', action='store_true', dest='debug',
540 default=False, help='Print debug output.')
541 parser.add_option('-m', '--message', dest='message', metavar='TEXT',
542 help='Log message when updating Tracker issues')
543 parser.add_option('-p', '--pretend', action='store_true', dest='pretend',
544 default=False, help="Don't commit anything.")
545 parser.add_option('--ss_key', dest='ss_key', metavar='KEY',
546 help='Spreadsheets key (find with browse action)')
547 parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY',
548 help='Spreadsheets worksheet key (find with browse action)')
549 parser.add_option('--tracker_project', dest='tracker_project',
550 metavar='PROJECT',
551 help='Tracker project (default: chromium-os)',
552 default='chromium-os')
553 parser.epilog = """Actions:
554 browse -- browse spreadsheets to find spreadsheet and worksheet keys.
555 ss_to_t -- for each entry in spreadsheet, apply its values to tracker.
556 If no ID is in the spreadsheet row, a new tracker item is created
557 and the spreadsheet is updated.
558 t_to_ss -- for each tracker entry, apply it or add it to the spreadsheet.
559
560
561 This script can be used to migrate Issue Tracker issues between Issue Tracker
562 and Google Spreadsheets. The spreadsheet should have certain columns in any
563 order: Id, Owner, Title, Status. The spreadsheet may have any label of the
564 form 'Key-'. For those labels that end in '-', this script assumes the cell
565 value and the header form a label that should be applied to the issue. E.g.
566 if the spredsheet has a column named 'Mstone-' and a cell under it called
567 'R8' that corresponds to the label 'Mstone-R8' in Issue Tracker.
568
569 To migrate data, you must choose on each invocation of this script if you
570 wish to migrate data from Issue Tracker to a spreadsheet of vice-versa.
571
572 When migrating from Tracker, all found issues based on the query
573 (which is currently hard-coded to "label=Area-Installer") will be inserted
574 into the spreadsheet (overwritng existing cells if a row with matching ID
575 is found). Custom columns in the spreadsheet won't be overwritten, so if
576 the spreadsheet contains extra columns about issues (e.g. time estimates)
577 they will be preserved.
578
579 When migrating from spreadsheet to Tracker, each row in the spreadsheet
580 is compared to existing tracker issues that match the query
581 (which is currently hard-coded to "label=Area-Installer"). If the
582 spreadsheet row has no Id, a new Issue Tracker issue is created and the new
583 Id is written back to the spreadsheet. If an existing tracker issue exists,
584 it's updated with the data from the spreadsheet if anything has changed.
585
586 Suggested usage:
587 - Create a spreadsheet with columns Id, Owner, Title, Status, and any label
588 prefixes as desired.
589 - Run this script with '-b' to browse your spreadsheet and get the
590 spreadsheet key.
591 - Run this script again with '-b' and the spreadsheet key to get the
592 worksheet key.
593 - Run this script with "-a t_to_ss" or "-a ss_to_t" to migrate data in either
594 direction.
595
596 Known issues:
597 - query is currently hardcoded to label=Area-Installer. That should be
598 a command-line flag.
599 - When creating a new issue on tracker, the owner field isn't set. I (adlr)
600 am not sure why. Workaround: If you rerun this script, tho, it will detect
601 a delta and update the tracker issue with the owner, which seems to succeed.
602 """
603
604 (options, args) = parser.parse_args()
605
606 merger = Merger(options.ss_key, options.ss_ws_key,
607 options.message, options.tracker_project,
608 options.debug, options.pretend)
609 if options.action == 'browse':
610 merger.browse()
611 elif options.action == 'ss_to_t':
612 if not options.message:
613 print 'Error: when updating tracker, -m MESSAGE required.'
614 return
615 merger.spreadsheet_to_tracker()
616 elif options.action == 't_to_ss':
617 merger.tracker_to_spreadsheet()
618 else:
619 raise Exception('Unknown action requested.')
620
621 if __name__ == '__main__':
622 main()
OLDNEW
« no previous file with comments | « save_pinned_deps ('k') | upgrade.d/1_init » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698