| Index: my_reviews.py
 | 
| diff --git a/my_reviews.py b/my_reviews.py
 | 
| index 3e1ab73251d7f008288e879362be2e0047680134..1d2377278ee3a9493b56cddcd97cfcd73749720d 100755
 | 
| --- a/my_reviews.py
 | 
| +++ b/my_reviews.py
 | 
| @@ -9,6 +9,7 @@ Example:
 | 
|    - my_reviews.py -r me@chromium.org -Q  for stats for last quarter.
 | 
|  """
 | 
|  import datetime
 | 
| +import math
 | 
|  import optparse
 | 
|  import os
 | 
|  import sys
 | 
| @@ -17,48 +18,221 @@ import rietveld
 | 
|  
 | 
|  
 | 
|  def username(email):
 | 
| +  """Keeps the username of an email address."""
 | 
|    return email.split('@', 1)[0]
 | 
|  
 | 
|  
 | 
| +def to_datetime(string):
 | 
| +  """Load UTC time as a string into a datetime object."""
 | 
| +  try:
 | 
| +    # Format is 2011-07-05 01:26:12.084316
 | 
| +    return datetime.datetime.strptime(
 | 
| +        string.split('.', 1)[0], '%Y-%m-%d %H:%M:%S')
 | 
| +  except ValueError:
 | 
| +    return datetime.datetime.strptime(string, '%Y-%m-%d')
 | 
| +
 | 
| +
 | 
| +def to_time(seconds):
 | 
| +  """Convert a number of seconds into human readable compact string."""
 | 
| +  prefix = ''
 | 
| +  if seconds < 0:
 | 
| +    prefix = '-'
 | 
| +    seconds *= -1
 | 
| +  minutes = math.floor(seconds / 60)
 | 
| +  seconds -= minutes * 60
 | 
| +  hours = math.floor(minutes / 60)
 | 
| +  minutes -= hours * 60
 | 
| +  days = math.floor(hours / 24)
 | 
| +  hours -= days * 24
 | 
| +  out = []
 | 
| +  if days > 0:
 | 
| +    out.append('%dd' % days)
 | 
| +  if hours > 0 or days > 0:
 | 
| +    out.append('%02dh' % hours)
 | 
| +  if minutes > 0 or hours > 0 or days > 0:
 | 
| +    out.append('%02dm' % minutes)
 | 
| +  if seconds > 0 and not out:
 | 
| +    # Skip seconds unless there's only seconds.
 | 
| +    out.append('%02ds' % seconds)
 | 
| +  return prefix + ''.join(out)
 | 
| +
 | 
| +
 | 
| +class Stats(object):
 | 
| +  def __init__(self):
 | 
| +    self.total = 0
 | 
| +    self.actually_reviewed = 0
 | 
| +    self.average_latency = 0.
 | 
| +    self.number_latency = 0
 | 
| +    self.lgtms = 0
 | 
| +    self.multiple_lgtms = 0
 | 
| +    self.drive_by = 0
 | 
| +    self.not_requested = 0
 | 
| +
 | 
| +    self.percent_done = 0.
 | 
| +    self.percent_lgtm = 0.
 | 
| +    self.percent_drive_by = 0.
 | 
| +    self.percent_not_requested = 0.
 | 
| +    self.days = None
 | 
| +    self.review_per_day = 0.
 | 
| +    self.review_done_per_day = 0.
 | 
| +
 | 
| +  def add_latency(self, latency):
 | 
| +    self.average_latency = (
 | 
| +        (self.average_latency * self.number_latency + latency) /
 | 
| +        (self.number_latency + 1.))
 | 
| +    self.number_latency += 1
 | 
| +
 | 
| +  def finalize(self, first_day, last_day):
 | 
| +    if self.total:
 | 
| +      self.percent_done = (self.actually_reviewed * 100. / self.total)
 | 
| +    if self.actually_reviewed:
 | 
| +      self.percent_lgtm = (self.lgtms * 100. / self.actually_reviewed)
 | 
| +      self.percent_drive_by = (self.drive_by * 100. / self.actually_reviewed)
 | 
| +      self.percent_not_requested = (
 | 
| +          self.not_requested * 100. / self.actually_reviewed)
 | 
| +    if first_day and last_day:
 | 
| +      self.days = (to_datetime(last_day) - to_datetime(first_day)).days + 1
 | 
| +    if self.days:
 | 
| +      self.review_per_day = self.total * 1. / self.days
 | 
| +      self.review_done_per_day = self.actually_reviewed * 1. / self.days
 | 
| +
 | 
| +
 | 
| +def _process_issue_lgtms(issue, reviewer, stats):
 | 
| +  """Calculates LGTMs stats."""
 | 
| +  stats.actually_reviewed += 1
 | 
| +  reviewer_lgtms = len([
 | 
| +    msg for msg in issue['messages']
 | 
| +    if msg['approval'] and msg['sender'] == reviewer])
 | 
| +  if reviewer_lgtms > 1:
 | 
| +    stats.multiple_lgtms += 1
 | 
| +    return ' X '
 | 
| +  if reviewer_lgtms:
 | 
| +    stats.lgtms += 1
 | 
| +    return ' x '
 | 
| +  else:
 | 
| +    return ' o '
 | 
| +
 | 
| +
 | 
| +def _process_issue_latency(issue, reviewer, stats):
 | 
| +  """Calculates latency for an issue that was actually reviewed."""
 | 
| +  from_owner = [
 | 
| +    msg for msg in issue['messages'] if msg['sender'] == issue['owner_email']
 | 
| +  ]
 | 
| +  if not from_owner:
 | 
| +    # Probably requested by email.
 | 
| +    stats.not_requested += 1
 | 
| +    return '<no rqst sent>'
 | 
| +
 | 
| +  first_msg_from_owner = None
 | 
| +  latency = None
 | 
| +  received = False
 | 
| +  for index, msg in enumerate(issue['messages']):
 | 
| +    if not first_msg_from_owner and msg['sender'] == issue['owner_email']:
 | 
| +      first_msg_from_owner = msg
 | 
| +    if index and not received and msg['sender'] == reviewer:
 | 
| +      # Not first email, reviewer never received one, reviewer sent a mesage.
 | 
| +      stats.drive_by += 1
 | 
| +      return '<drive-by>'
 | 
| +    received |= reviewer in msg['recipients']
 | 
| +
 | 
| +    if first_msg_from_owner and msg['sender'] == reviewer:
 | 
| +      delta = msg['date'] - first_msg_from_owner['date']
 | 
| +      latency = delta.seconds + delta.days * 24 * 3600
 | 
| +      break
 | 
| +
 | 
| +  if latency is None:
 | 
| +    stats.not_requested += 1
 | 
| +    return '<no rqst sent>'
 | 
| +  if latency > 0:
 | 
| +    stats.add_latency(latency)
 | 
| +  else:
 | 
| +    stats.not_requested += 1
 | 
| +  return to_time(latency)
 | 
| +
 | 
| +
 | 
| +def _process_issue(issue):
 | 
| +  """Preprocesses the issue to simplify the remaining code."""
 | 
| +  issue['owner_email'] = username(issue['owner_email'])
 | 
| +  issue['reviewers'] = set(username(r) for r in issue['reviewers'])
 | 
| +  # By default, hide commit-bot.
 | 
| +  issue['reviewers'] -= set(['commit-bot'])
 | 
| +  for msg in issue['messages']:
 | 
| +    msg['sender'] = username(msg['sender'])
 | 
| +    msg['recipients'] = [username(r) for r in msg['recipients']]
 | 
| +    # Convert all times to datetime instances.
 | 
| +    msg['date'] = to_datetime(msg['date'])
 | 
| +  issue['messages'].sort(key=lambda x: x['date'])
 | 
| +
 | 
| +
 | 
| +def print_issue(issue, reviewer, stats):
 | 
| +  """Process an issue and prints stats about it."""
 | 
| +  stats.total += 1
 | 
| +  _process_issue(issue)
 | 
| +  if any(msg['sender'] == reviewer for msg in issue['messages']):
 | 
| +    reviewed = _process_issue_lgtms(issue, reviewer, stats)
 | 
| +    latency = _process_issue_latency(issue, reviewer, stats)
 | 
| +  else:
 | 
| +    latency = 'N/A'
 | 
| +    reviewed = ''
 | 
| +
 | 
| +  # More information is available, print issue.keys() to see them.
 | 
| +  print '%7d %10s %3s %14s %-15s  %s' % (
 | 
| +      issue['issue'],
 | 
| +      issue['created'][:10],
 | 
| +      reviewed,
 | 
| +      latency,
 | 
| +      issue['owner_email'],
 | 
| +      ', '.join(sorted(issue['reviewers'])))
 | 
| +
 | 
| +
 | 
|  def print_reviews(reviewer, created_after, created_before, instance_url):
 | 
| -  """Prints issues the dude reviewed."""
 | 
| +  """Prints issues |reviewer| received and potentially reviewed."""
 | 
|    remote = rietveld.Rietveld(instance_url, None, None)
 | 
| -  total = 0
 | 
| -  actually_reviewed = 0
 | 
| +
 | 
| +  # The stats we gather. Feel free to send me a CL to get more stats.
 | 
| +  stats = Stats()
 | 
| +
 | 
| +  last_issue = None
 | 
| +  first_day = None
 | 
| +  last_day = None
 | 
| +
 | 
| +  # Column sizes need to match print_issue() output.
 | 
| +  print >> sys.stderr, (
 | 
| +      'Issue   Creation   Did         Latency Owner           Reviewers')
 | 
|  
 | 
|    # See def search() in rietveld.py to see all the filters you can use.
 | 
|    for issue in remote.search(
 | 
|        reviewer=reviewer,
 | 
|        created_after=created_after,
 | 
|        created_before=created_before,
 | 
| -      with_messages=True,
 | 
| -      ):
 | 
| -    total += 1
 | 
| -    # By default, hide commit-bot and the domain.
 | 
| -    reviewers = set(username(r) for r in issue['reviewers'])
 | 
| -    reviewers -= set(['commit-bot'])
 | 
| -    # Strip time.
 | 
| -    timestamp = issue['created'][:10]
 | 
| -    if any(
 | 
| -      username(msg['sender']) == username(reviewer)
 | 
| -      for msg in issue['messages']):
 | 
| -      reviewed = ' x '
 | 
| -      actually_reviewed += 1
 | 
| -    else:
 | 
| -      reviewed = '   '
 | 
| -
 | 
| -    # More information is available, print issue.keys() to see them.
 | 
| -    print '%7d %s %s O:%-15s  R:%s' % (
 | 
| -        issue['issue'],
 | 
| -        timestamp,
 | 
| -        reviewed,
 | 
| -        username(issue['owner_email']),
 | 
| -        ', '.join(reviewers))
 | 
| -  percent = 0.
 | 
| -  if total:
 | 
| -    percent = (actually_reviewed * 100. / total)
 | 
| -  print 'You actually reviewed %d issues out of %d (%1.1f%%)' % (
 | 
| -      actually_reviewed, total, percent)
 | 
| +      with_messages=True):
 | 
| +    last_issue = issue
 | 
| +    if not first_day:
 | 
| +      first_day = issue['created'][:10]
 | 
| +    print_issue(issue, username(reviewer), stats)
 | 
| +  if last_issue:
 | 
| +    last_day = last_issue['created'][:10]
 | 
| +  stats.finalize(first_day, last_day)
 | 
| +
 | 
| +  print >> sys.stderr, (
 | 
| +      '%s reviewed %d issues out of %d (%1.1f%%).' %
 | 
| +      (reviewer, stats.actually_reviewed, stats.total, stats.percent_done))
 | 
| +  print >> sys.stderr, (
 | 
| +      '%4.1f review request/day during %3d days   (%4.1f r/d done).' % (
 | 
| +      stats.review_per_day, stats.days, stats.review_done_per_day))
 | 
| +  print >> sys.stderr, (
 | 
| +      '%4d were drive-bys                       (%5.1f%% of reviews done).' % (
 | 
| +        stats.drive_by, stats.percent_drive_by))
 | 
| +  print >> sys.stderr, (
 | 
| +      '%4d were requested over IM or irc        (%5.1f%% of reviews done).' % (
 | 
| +        stats.not_requested, stats.percent_not_requested))
 | 
| +  print >> sys.stderr, (
 | 
| +      ('%4d issues LGTM\'d                        (%5.1f%% of reviews done),'
 | 
| +       ' gave multiple LGTMs on %d issues.') % (
 | 
| +      stats.lgtms, stats.percent_lgtm, stats.multiple_lgtms))
 | 
| +  print >> sys.stderr, (
 | 
| +      'Average latency from request to first comment is %s.' %
 | 
| +      to_time(stats.average_latency))
 | 
|  
 | 
|  
 | 
|  def print_count(reviewer, created_after, created_before, instance_url):
 | 
| 
 |