OLD | NEW |
---|---|
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 Loading... | |
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 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
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 { | 105 { |
103 'host': 'gerrit.chromium.org', | 106 'host': 'gerrit.chromium.org', |
Vadim Sh.
2015/06/11 01:37:31
this host has been turn off today, can be removed
seanmccullough
2015/06/11 02:05:22
Done.
| |
104 'port': 29418, | 107 'port': 29418, |
105 }, | 108 }, |
106 ] | 109 ] |
107 | 110 |
108 google_code_projects = [ | 111 google_code_projects = [ |
109 { | 112 { |
110 'name': 'brillo', | 113 'name': 'brillo', |
111 'shorturl': 'brbug.com', | 114 'shorturl': 'brbug.com', |
112 }, | 115 }, |
113 { | 116 { |
(...skipping 12 matching lines...) Expand all Loading... | |
126 }, | 129 }, |
127 { | 130 { |
128 'name': 'gyp', | 131 'name': 'gyp', |
129 }, | 132 }, |
130 { | 133 { |
131 'name': 'skia', | 134 'name': 'skia', |
132 }, | 135 }, |
133 ] | 136 ] |
134 | 137 |
135 # Uses ClientLogin to authenticate the user for Google Code issue trackers. | 138 # Uses ClientLogin to authenticate the user for Google Code issue trackers. |
136 def get_auth_token(email): | 139 def get_auth_token(email): |
Vadim Sh.
2015/06/11 01:37:31
this should be eventually deleted, it's broken
seanmccullough
2015/06/11 02:05:22
Done.
| |
137 # KeyringCreds will use the system keyring on the first try, and prompt for | 140 # KeyringCreds will use the system keyring on the first try, and prompt for |
138 # a password on the next ones. | 141 # a password on the next ones. |
139 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) | 142 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) |
140 for _ in xrange(3): | 143 for _ in xrange(3): |
141 email, password = creds.GetUserCredentials() | 144 email, password = creds.GetUserCredentials() |
142 url = 'https://www.google.com/accounts/ClientLogin' | 145 url = 'https://www.google.com/accounts/ClientLogin' |
143 data = urllib.urlencode({ | 146 data = urllib.urlencode({ |
144 'Email': email, | 147 'Email': email, |
145 'Passwd': password, | 148 'Passwd': password, |
146 'service': 'code', | 149 'service': 'code', |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
223 self.user = options.user | 226 self.user = options.user |
224 self.changes = [] | 227 self.changes = [] |
225 self.reviews = [] | 228 self.reviews = [] |
226 self.issues = [] | 229 self.issues = [] |
227 self.check_cookies() | 230 self.check_cookies() |
228 self.google_code_auth_token = None | 231 self.google_code_auth_token = None |
229 | 232 |
230 # Check the codereview cookie jar to determine which Rietveld instances to | 233 # Check the codereview cookie jar to determine which Rietveld instances to |
231 # authenticate to. | 234 # authenticate to. |
232 def check_cookies(self): | 235 def check_cookies(self): |
233 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') | 236 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') |
Vadim Sh.
2015/06/11 01:37:31
it's not directly related to OAuth code.google.com
seanmccullough
2015/06/11 02:05:22
removed it, now it just checks "has_cookie"
| |
234 if not os.path.exists(cookie_file): | 237 if not os.path.exists(cookie_file): |
235 print 'No Rietveld cookie file found.' | 238 print 'No Rietveld cookie file found.' |
236 cookie_jar = [] | 239 cookie_jar = [] |
237 else: | 240 else: |
238 cookie_jar = cookielib.MozillaCookieJar(cookie_file) | 241 cookie_jar = cookielib.MozillaCookieJar(cookie_file) |
239 try: | 242 try: |
240 cookie_jar.load() | 243 cookie_jar.load() |
241 print 'Found cookie file: %s' % cookie_file | 244 print 'Found cookie file: %s' % cookie_file |
242 except (cookielib.LoadError, IOError): | 245 except (cookielib.LoadError, IOError): |
243 print 'Error loading Rietveld cookie file: %s' % cookie_file | 246 print 'Error loading Rietveld cookie file: %s' % cookie_file |
244 cookie_jar = [] | 247 cookie_jar = [] |
245 | 248 |
246 filtered_instances = [] | 249 filtered_instances = [] |
247 | 250 |
248 def has_cookie(instance): | 251 def has_cookie(instance): |
Vadim Sh.
2015/06/11 01:37:31
this can be reformulated as:
a = auth.get_authent
seanmccullough
2015/06/11 02:05:22
Done.
| |
249 for cookie in cookie_jar: | 252 for cookie in cookie_jar: |
250 if cookie.name == 'SACSID' and cookie.domain == instance['url']: | 253 if cookie.name == 'SACSID' and cookie.domain == instance['url']: |
251 return True | 254 return True |
252 if self.options.auth: | 255 if self.options.auth: |
253 return get_yes_or_no('No cookie found for %s. Authorize for this ' | 256 return get_yes_or_no('No cookie found for %s. Authorize for this ' |
254 'instance? (may require application-specific ' | 257 'instance? (may require application-specific ' |
255 'password)' % instance['url']) | 258 'password)' % instance['url']) |
256 filtered_instances.append(instance) | 259 filtered_instances.append(instance) |
257 return False | 260 return False |
258 | 261 |
(...skipping 195 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
454 replies = filter(lambda r: 'author' in r and 'email' in r['author'], | 457 replies = filter(lambda r: 'author' in r and 'email' in r['author'], |
455 replies) | 458 replies) |
456 for reply in replies: | 459 for reply in replies: |
457 ret.append({ | 460 ret.append({ |
458 'author': reply['author']['email'], | 461 'author': reply['author']['email'], |
459 'created': datetime_from_gerrit(reply['date']), | 462 'created': datetime_from_gerrit(reply['date']), |
460 'content': reply['message'], | 463 'content': reply['message'], |
461 }) | 464 }) |
462 return ret | 465 return ret |
463 | 466 |
464 def google_code_issue_search(self, instance): | 467 def project_hosting_issue_search(self, instance): |
465 time_format = '%Y-%m-%dT%T' | 468 auth_config = auth.extract_auth_config_from_options(self.options) |
466 # See http://code.google.com/p/support/wiki/IssueTrackerAPI | 469 authenticator = auth.get_authenticator_for_host( |
467 # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. | 470 "https://code.google.com", auth_config) |
Vadim Sh.
2015/06/11 01:37:32
just "code.google.com" here should work too
seanmccullough
2015/06/11 02:05:22
Done.
| |
468 # This will accept the issue if owner is the owner or in the cc list. Might | 471 http = authenticator.authorize(httplib2.Http()) |
469 # have some false positives, though. | 472 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % ( |
473 instance["name"]) | |
474 epoch = datetime.utcfromtimestamp(0) | |
475 user_str = '%s@chromium.org' % self.user | |
470 | 476 |
471 # Don't filter normally on modified_before because it can filter out things | 477 query_data = urllib.urlencode({ |
472 # that were modified in the time period and then modified again after it. | 478 'maxResults': 10000, |
473 gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % | 479 'q': '%s' % user_str, |
Vadim Sh.
2015/06/11 01:37:31
'q': user_str
seanmccullough
2015/06/11 02:05:22
Done.
| |
474 instance['name']) | 480 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(), |
475 | 481 '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 }) | 482 }) |
483 | 483 url = url + '?' + query_data |
484 opener = urllib2.build_opener() | 484 _, body = http.request(url) |
Vadim Sh.
2015/06/11 01:37:31
this thing will raise auth.AuthenticationError if
seanmccullough
2015/06/11 02:05:22
Done.
| |
485 if self.google_code_auth_token: | 485 content = json.loads(body) |
486 opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % | 486 if not content: |
487 self.google_code_auth_token)] | 487 print "Unable to parse %s response from projecthosting." % ( |
488 gcode_json = None | 488 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 [] | 489 return [] |
498 | 490 |
499 issues = gcode_json['feed']['entry'] | 491 issues = [] |
500 issues = map(partial(self.process_google_code_issue, instance), issues) | 492 if 'items' in content: |
501 issues = filter(self.filter_issue, issues) | 493 items = content['items'] |
502 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 494 for item in items: |
495 issue = { | |
496 "header": item["title"], | |
497 "created": item["published"], | |
498 "modified": item["updated"], | |
499 "author": item["author"]["name"], | |
500 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % ( | |
501 instance["name"], item["id"]), | |
502 "comments": [] | |
503 } | |
504 if 'owner' in item: | |
505 issue['owner'] = item['owner']['name'] | |
506 else: | |
507 issue['owner'] = 'None' | |
508 if issue['owner'] == user_str or issue['author'] == user_str: | |
509 issues.append(issue) | |
510 | |
503 return issues | 511 return issues |
504 | 512 |
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): | 513 def print_heading(self, heading): |
567 print | 514 print |
568 print self.options.output_format_heading.format(heading=heading) | 515 print self.options.output_format_heading.format(heading=heading) |
569 | 516 |
570 def print_change(self, change): | 517 def print_change(self, change): |
571 optional_values = { | 518 optional_values = { |
572 'reviewers': ', '.join(change['reviewers']) | 519 'reviewers': ', '.join(change['reviewers']) |
573 } | 520 } |
574 self.print_generic(self.options.output_format, | 521 self.print_generic(self.options.output_format, |
575 self.options.output_format_changes, | 522 self.options.output_format_changes, |
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
675 self.reviews += reviews | 622 self.reviews += reviews |
676 | 623 |
677 def print_reviews(self): | 624 def print_reviews(self): |
678 if self.reviews: | 625 if self.reviews: |
679 self.print_heading('Reviews') | 626 self.print_heading('Reviews') |
680 for review in self.reviews: | 627 for review in self.reviews: |
681 self.print_review(review) | 628 self.print_review(review) |
682 | 629 |
683 def get_issues(self): | 630 def get_issues(self): |
684 for project in google_code_projects: | 631 for project in google_code_projects: |
685 self.issues += self.google_code_issue_search(project) | 632 self.issues += self.project_hosting_issue_search(project) |
686 | 633 |
687 def print_issues(self): | 634 def print_issues(self): |
688 if self.issues: | 635 if self.issues: |
689 self.print_heading('Issues') | 636 self.print_heading('Issues') |
690 for issue in self.issues: | 637 for issue in self.issues: |
691 self.print_issue(issue) | 638 self.print_issue(issue) |
692 | 639 |
693 def print_activity(self): | 640 def print_activity(self): |
694 self.print_changes() | 641 self.print_changes() |
695 self.print_reviews() | 642 self.print_reviews() |
(...skipping 167 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
863 | 810 |
864 if __name__ == '__main__': | 811 if __name__ == '__main__': |
865 # Fix encoding to support non-ascii issue titles. | 812 # Fix encoding to support non-ascii issue titles. |
866 fix_encoding.fix_encoding() | 813 fix_encoding.fix_encoding() |
867 | 814 |
868 try: | 815 try: |
869 sys.exit(main()) | 816 sys.exit(main()) |
870 except KeyboardInterrupt: | 817 except KeyboardInterrupt: |
871 sys.stderr.write('interrupted\n') | 818 sys.stderr.write('interrupted\n') |
872 sys.exit(1) | 819 sys.exit(1) |
OLD | NEW |