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

Side by Side Diff: my_activity.py

Issue 1176243002: my_activity.py: update to use oauth for projecthosting (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: removed more dead/broken code, tidied up some hacks Created 5 years, 6 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
« no previous file with comments | « auth.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Get stats about your activity. 6 """Get stats about your activity.
7 7
8 Example: 8 Example:
9 - my_activity.py for stats for the current week (last week on mondays). 9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter. 10 - my_activity.py -Q for stats for last quarter.
(...skipping 24 matching lines...) Expand all
35 import sys 35 import sys
36 import urllib 36 import urllib
37 import urllib2 37 import urllib2
38 38
39 import auth 39 import auth
40 import fix_encoding 40 import fix_encoding
41 import gerrit_util 41 import gerrit_util
42 import rietveld 42 import rietveld
43 from third_party import upload 43 from third_party import upload
44 44
45 import auth
46 from third_party import httplib2
47
45 try: 48 try:
46 from dateutil.relativedelta import relativedelta # pylint: disable=F0401 49 from dateutil.relativedelta import relativedelta # pylint: disable=F0401
47 except ImportError: 50 except ImportError:
48 print 'python-dateutil package required' 51 print 'python-dateutil package required'
49 exit(1) 52 exit(1)
50 53
51 # python-keyring provides easy access to the system keyring. 54 # python-keyring provides easy access to the system keyring.
52 try: 55 try:
53 import keyring # pylint: disable=W0611,F0401 56 import keyring # pylint: disable=W0611,F0401
54 except ImportError: 57 except ImportError:
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
92 95
93 gerrit_instances = [ 96 gerrit_instances = [
94 { 97 {
95 'url': 'chromium-review.googlesource.com', 98 'url': 'chromium-review.googlesource.com',
96 'shorturl': 'crosreview.com', 99 'shorturl': 'crosreview.com',
97 }, 100 },
98 { 101 {
99 'url': 'chrome-internal-review.googlesource.com', 102 'url': 'chrome-internal-review.googlesource.com',
100 'shorturl': 'crosreview.com/i', 103 'shorturl': 'crosreview.com/i',
101 }, 104 },
102 {
103 'host': 'gerrit.chromium.org',
104 'port': 29418,
105 },
106 ] 105 ]
107 106
108 google_code_projects = [ 107 google_code_projects = [
109 { 108 {
110 'name': 'brillo', 109 'name': 'brillo',
111 'shorturl': 'brbug.com', 110 'shorturl': 'brbug.com',
112 }, 111 },
113 { 112 {
114 'name': 'chromium', 113 'name': 'chromium',
115 'shorturl': 'crbug.com', 114 'shorturl': 'crbug.com',
116 }, 115 },
117 { 116 {
118 'name': 'chromium-os', 117 'name': 'chromium-os',
119 'shorturl': 'crosbug.com', 118 'shorturl': 'crosbug.com',
120 }, 119 },
121 { 120 {
122 'name': 'chrome-os-partner', 121 'name': 'chrome-os-partner',
123 }, 122 },
124 { 123 {
125 'name': 'google-breakpad', 124 'name': 'google-breakpad',
126 }, 125 },
127 { 126 {
128 'name': 'gyp', 127 'name': 'gyp',
129 }, 128 },
130 { 129 {
131 'name': 'skia', 130 'name': 'skia',
132 }, 131 },
133 ] 132 ]
134 133
135 # Uses ClientLogin to authenticate the user for Google Code issue trackers.
136 def get_auth_token(email):
137 # KeyringCreds will use the system keyring on the first try, and prompt for
138 # a password on the next ones.
139 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email)
140 for _ in xrange(3):
141 email, password = creds.GetUserCredentials()
142 url = 'https://www.google.com/accounts/ClientLogin'
143 data = urllib.urlencode({
144 'Email': email,
145 'Passwd': password,
146 'service': 'code',
147 'source': 'chrome-my-activity',
148 'accountType': 'GOOGLE',
149 })
150 req = urllib2.Request(url, data=data, headers={'Accept': 'text/plain'})
151 try:
152 response = urllib2.urlopen(req)
153 response_body = response.read()
154 response_dict = dict(x.split('=')
155 for x in response_body.split('\n') if x)
156 return response_dict['Auth']
157 except urllib2.HTTPError, e:
158 print e
159
160 print 'Unable to authenticate to code.google.com.'
161 print 'Some issues may be missing.'
162 return None
163
164
165 def username(email): 134 def username(email):
166 """Keeps the username of an email address.""" 135 """Keeps the username of an email address."""
167 return email and email.split('@', 1)[0] 136 return email and email.split('@', 1)[0]
168 137
169 138
170 def datetime_to_midnight(date): 139 def datetime_to_midnight(date):
171 return date - timedelta(hours=date.hour, minutes=date.minute, 140 return date - timedelta(hours=date.hour, minutes=date.minute,
172 seconds=date.second, microseconds=date.microsecond) 141 seconds=date.second, microseconds=date.microsecond)
173 142
174 143
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
223 self.user = options.user 192 self.user = options.user
224 self.changes = [] 193 self.changes = []
225 self.reviews = [] 194 self.reviews = []
226 self.issues = [] 195 self.issues = []
227 self.check_cookies() 196 self.check_cookies()
228 self.google_code_auth_token = None 197 self.google_code_auth_token = None
229 198
230 # Check the codereview cookie jar to determine which Rietveld instances to 199 # Check the codereview cookie jar to determine which Rietveld instances to
231 # authenticate to. 200 # authenticate to.
232 def check_cookies(self): 201 def check_cookies(self):
233 cookie_file = os.path.expanduser('~/.codereview_upload_cookies')
234 if not os.path.exists(cookie_file):
235 print 'No Rietveld cookie file found.'
236 cookie_jar = []
237 else:
238 cookie_jar = cookielib.MozillaCookieJar(cookie_file)
239 try:
240 cookie_jar.load()
241 print 'Found cookie file: %s' % cookie_file
242 except (cookielib.LoadError, IOError):
243 print 'Error loading Rietveld cookie file: %s' % cookie_file
244 cookie_jar = []
245
246 filtered_instances = [] 202 filtered_instances = []
247 203
248 def has_cookie(instance): 204 def has_cookie(instance):
249 for cookie in cookie_jar: 205 auth_config = auth.extract_auth_config_from_options(self.options)
250 if cookie.name == 'SACSID' and cookie.domain == instance['url']: 206 a = auth.get_authenticator_for_host(instance['url'], auth_config)
251 return True 207 return a.has_cached_credentials()
252 if self.options.auth:
253 return get_yes_or_no('No cookie found for %s. Authorize for this '
254 'instance? (may require application-specific '
255 'password)' % instance['url'])
256 filtered_instances.append(instance)
257 return False
258 208
259 for instance in rietveld_instances: 209 for instance in rietveld_instances:
260 instance['auth'] = has_cookie(instance) 210 instance['auth'] = has_cookie(instance)
261 211
262 if filtered_instances: 212 if filtered_instances:
263 print ('No cookie found for the following Rietveld instance%s:' % 213 print ('No cookie found for the following Rietveld instance%s:' %
264 ('s' if len(filtered_instances) > 1 else '')) 214 ('s' if len(filtered_instances) > 1 else ''))
265 for instance in filtered_instances: 215 for instance in filtered_instances:
266 print '\t' + instance['url'] 216 print '\t' + instance['url']
267 print 'Use --auth if you would like to authenticate to them.\n' 217 print 'Use --auth if you would like to authenticate to them.\n'
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
454 replies = filter(lambda r: 'author' in r and 'email' in r['author'], 404 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
455 replies) 405 replies)
456 for reply in replies: 406 for reply in replies:
457 ret.append({ 407 ret.append({
458 'author': reply['author']['email'], 408 'author': reply['author']['email'],
459 'created': datetime_from_gerrit(reply['date']), 409 'created': datetime_from_gerrit(reply['date']),
460 'content': reply['message'], 410 'content': reply['message'],
461 }) 411 })
462 return ret 412 return ret
463 413
464 def google_code_issue_search(self, instance): 414 def project_hosting_issue_search(self, instance):
465 time_format = '%Y-%m-%dT%T' 415 auth_config = auth.extract_auth_config_from_options(self.options)
466 # See http://code.google.com/p/support/wiki/IssueTrackerAPI 416 authenticator = auth.get_authenticator_for_host(
467 # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. 417 "code.google.com", auth_config)
468 # This will accept the issue if owner is the owner or in the cc list. Might 418 http = authenticator.authorize(httplib2.Http())
469 # have some false positives, though. 419 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % (
420 instance["name"])
421 epoch = datetime.utcfromtimestamp(0)
422 user_str = '%s@chromium.org' % self.user
470 423
471 # Don't filter normally on modified_before because it can filter out things 424 query_data = urllib.urlencode({
472 # that were modified in the time period and then modified again after it. 425 'maxResults': 10000,
473 gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % 426 'q': user_str,
474 instance['name']) 427 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
475 428 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
476 gcode_data = urllib.urlencode({
477 'alt': 'json',
478 'max-results': '100000',
479 'q': '%s' % self.user,
480 'published-max': self.modified_before.strftime(time_format),
481 'updated-min': self.modified_after.strftime(time_format),
482 }) 429 })
483 430 url = url + '?' + query_data
484 opener = urllib2.build_opener() 431 _, body = http.request(url)
485 if self.google_code_auth_token: 432 content = json.loads(body)
486 opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % 433 if not content:
487 self.google_code_auth_token)] 434 print "Unable to parse %s response from projecthosting." % (
488 gcode_json = None 435 instance["name"])
489 try:
490 gcode_get = opener.open(gcode_url + '?' + gcode_data)
491 gcode_json = json.load(gcode_get)
492 gcode_get.close()
493 except urllib2.HTTPError, _:
494 print 'Unable to access ' + instance['name'] + ' issue tracker.'
495
496 if not gcode_json or 'entry' not in gcode_json['feed']:
497 return [] 436 return []
498 437
499 issues = gcode_json['feed']['entry'] 438 issues = []
500 issues = map(partial(self.process_google_code_issue, instance), issues) 439 if 'items' in content:
501 issues = filter(self.filter_issue, issues) 440 items = content['items']
502 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) 441 for item in items:
442 issue = {
443 "header": item["title"],
444 "created": item["published"],
445 "modified": item["updated"],
446 "author": item["author"]["name"],
447 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
448 instance["name"], item["id"]),
449 "comments": []
450 }
451 if 'owner' in item:
452 issue['owner'] = item['owner']['name']
453 else:
454 issue['owner'] = 'None'
455 if issue['owner'] == user_str or issue['author'] == user_str:
456 issues.append(issue)
457
503 return issues 458 return issues
504 459
505 def process_google_code_issue(self, project, issue):
506 ret = {}
507 ret['created'] = datetime_from_google_code(issue['published']['$t'])
508 ret['modified'] = datetime_from_google_code(issue['updated']['$t'])
509
510 ret['owner'] = ''
511 if 'issues$owner' in issue:
512 ret['owner'] = issue['issues$owner']['issues$username']['$t']
513 ret['author'] = issue['author'][0]['name']['$t']
514
515 if 'shorturl' in project:
516 issue_id = issue['id']['$t']
517 issue_id = issue_id[issue_id.rfind('/') + 1:]
518 ret['url'] = 'http://%s/%d' % (project['shorturl'], int(issue_id))
519 else:
520 issue_url = issue['link'][1]
521 if issue_url['rel'] != 'alternate':
522 raise RuntimeError
523 ret['url'] = issue_url['href']
524 ret['header'] = issue['title']['$t']
525
526 ret['replies'] = self.get_google_code_issue_replies(issue)
527 return ret
528
529 def get_google_code_issue_replies(self, issue):
530 """Get all the comments on the issue."""
531 replies_url = issue['link'][0]
532 if replies_url['rel'] != 'replies':
533 raise RuntimeError
534
535 replies_data = urllib.urlencode({
536 'alt': 'json',
537 'fields': 'entry(published,author,content)',
538 })
539
540 opener = urllib2.build_opener()
541 opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' %
542 self.google_code_auth_token)]
543 try:
544 replies_get = opener.open(replies_url['href'] + '?' + replies_data)
545 except urllib2.HTTPError, _:
546 return []
547
548 replies_json = json.load(replies_get)
549 replies_get.close()
550 return self.process_google_code_issue_replies(replies_json)
551
552 @staticmethod
553 def process_google_code_issue_replies(replies):
554 if 'entry' not in replies['feed']:
555 return []
556
557 ret = []
558 for entry in replies['feed']['entry']:
559 e = {}
560 e['created'] = datetime_from_google_code(entry['published']['$t'])
561 e['content'] = entry['content']['$t']
562 e['author'] = entry['author'][0]['name']['$t']
563 ret.append(e)
564 return ret
565
566 def print_heading(self, heading): 460 def print_heading(self, heading):
567 print 461 print
568 print self.options.output_format_heading.format(heading=heading) 462 print self.options.output_format_heading.format(heading=heading)
569 463
570 def print_change(self, change): 464 def print_change(self, change):
571 optional_values = { 465 optional_values = {
572 'reviewers': ', '.join(change['reviewers']) 466 'reviewers': ', '.join(change['reviewers'])
573 } 467 }
574 self.print_generic(self.options.output_format, 468 self.print_generic(self.options.output_format,
575 self.options.output_format_changes, 469 self.options.output_format_changes,
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
641 535
642 def auth_for_changes(self): 536 def auth_for_changes(self):
643 #TODO(cjhopman): Move authentication check for getting changes here. 537 #TODO(cjhopman): Move authentication check for getting changes here.
644 pass 538 pass
645 539
646 def auth_for_reviews(self): 540 def auth_for_reviews(self):
647 # Reviews use all the same instances as changes so no authentication is 541 # Reviews use all the same instances as changes so no authentication is
648 # required. 542 # required.
649 pass 543 pass
650 544
651 def auth_for_issues(self):
652 self.google_code_auth_token = (
653 get_auth_token(self.options.local_user + '@chromium.org'))
654
655 def get_changes(self): 545 def get_changes(self):
656 for instance in rietveld_instances: 546 for instance in rietveld_instances:
657 self.changes += self.rietveld_search(instance, owner=self.user) 547 self.changes += self.rietveld_search(instance, owner=self.user)
658 548
659 for instance in gerrit_instances: 549 for instance in gerrit_instances:
660 self.changes += self.gerrit_search(instance, owner=self.user) 550 self.changes += self.gerrit_search(instance, owner=self.user)
661 551
662 def print_changes(self): 552 def print_changes(self):
663 if self.changes: 553 if self.changes:
664 self.print_heading('Changes') 554 self.print_heading('Changes')
(...skipping 10 matching lines...) Expand all
675 self.reviews += reviews 565 self.reviews += reviews
676 566
677 def print_reviews(self): 567 def print_reviews(self):
678 if self.reviews: 568 if self.reviews:
679 self.print_heading('Reviews') 569 self.print_heading('Reviews')
680 for review in self.reviews: 570 for review in self.reviews:
681 self.print_review(review) 571 self.print_review(review)
682 572
683 def get_issues(self): 573 def get_issues(self):
684 for project in google_code_projects: 574 for project in google_code_projects:
685 self.issues += self.google_code_issue_search(project) 575 self.issues += self.project_hosting_issue_search(project)
686 576
687 def print_issues(self): 577 def print_issues(self):
688 if self.issues: 578 if self.issues:
689 self.print_heading('Issues') 579 self.print_heading('Issues')
690 for issue in self.issues: 580 for issue in self.issues:
691 self.print_issue(issue) 581 self.print_issue(issue)
692 582
693 def print_activity(self): 583 def print_activity(self):
694 self.print_changes() 584 self.print_changes()
695 self.print_reviews() 585 self.print_reviews()
(...skipping 138 matching lines...) Expand 10 before | Expand all | Expand 10 after
834 options.changes = True 724 options.changes = True
835 options.issues = True 725 options.issues = True
836 options.reviews = True 726 options.reviews = True
837 727
838 # First do any required authentication so none of the user interaction has to 728 # First do any required authentication so none of the user interaction has to
839 # wait for actual work. 729 # wait for actual work.
840 if options.changes: 730 if options.changes:
841 my_activity.auth_for_changes() 731 my_activity.auth_for_changes()
842 if options.reviews: 732 if options.reviews:
843 my_activity.auth_for_reviews() 733 my_activity.auth_for_reviews()
844 if options.issues:
845 my_activity.auth_for_issues()
846 734
847 print 'Looking up activity.....' 735 print 'Looking up activity.....'
848 736
849 if options.changes: 737 try:
850 my_activity.get_changes() 738 if options.changes:
851 if options.reviews: 739 my_activity.get_changes()
852 my_activity.get_reviews() 740 if options.reviews:
853 if options.issues: 741 my_activity.get_reviews()
854 my_activity.get_issues() 742 if options.issues:
743 my_activity.get_issues()
744 except auth.AuthenticationError as e:
745 print "auth.AuthenticationError: %s" % e
855 746
856 print '\n\n\n' 747 print '\n\n\n'
857 748
858 my_activity.print_changes() 749 my_activity.print_changes()
859 my_activity.print_reviews() 750 my_activity.print_reviews()
860 my_activity.print_issues() 751 my_activity.print_issues()
861 return 0 752 return 0
862 753
863 754
864 if __name__ == '__main__': 755 if __name__ == '__main__':
865 # Fix encoding to support non-ascii issue titles. 756 # Fix encoding to support non-ascii issue titles.
866 fix_encoding.fix_encoding() 757 fix_encoding.fix_encoding()
867 758
868 try: 759 try:
869 sys.exit(main()) 760 sys.exit(main())
870 except KeyboardInterrupt: 761 except KeyboardInterrupt:
871 sys.stderr.write('interrupted\n') 762 sys.stderr.write('interrupted\n')
872 sys.exit(1) 763 sys.exit(1)
OLDNEW
« no previous file with comments | « auth.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698