[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 1 | #!/usr/bin/env python |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 2 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 6 | """Get rietveld stats about the review you done, or forgot to do. |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 7 | |
| 8 | Example: |
[email protected] | af52a50 | 2011-09-22 20:54:37 | [diff] [blame] | 9 | - my_reviews.py -r [email protected] -Q for stats for last quarter. |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 10 | """ |
| 11 | import datetime |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 12 | import math |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 13 | import optparse |
| 14 | import os |
| 15 | import sys |
| 16 | |
| 17 | import rietveld |
| 18 | |
| 19 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 20 | def username(email): |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 21 | """Keeps the username of an email address.""" |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 22 | return email.split('@', 1)[0] |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 23 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 24 | |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 25 | def to_datetime(string): |
| 26 | """Load UTC time as a string into a datetime object.""" |
| 27 | try: |
| 28 | # Format is 2011-07-05 01:26:12.084316 |
| 29 | return datetime.datetime.strptime( |
| 30 | string.split('.', 1)[0], '%Y-%m-%d %H:%M:%S') |
| 31 | except ValueError: |
| 32 | return datetime.datetime.strptime(string, '%Y-%m-%d') |
| 33 | |
| 34 | |
| 35 | def to_time(seconds): |
| 36 | """Convert a number of seconds into human readable compact string.""" |
| 37 | prefix = '' |
| 38 | if seconds < 0: |
| 39 | prefix = '-' |
| 40 | seconds *= -1 |
| 41 | minutes = math.floor(seconds / 60) |
| 42 | seconds -= minutes * 60 |
| 43 | hours = math.floor(minutes / 60) |
| 44 | minutes -= hours * 60 |
| 45 | days = math.floor(hours / 24) |
| 46 | hours -= days * 24 |
| 47 | out = [] |
| 48 | if days > 0: |
| 49 | out.append('%dd' % days) |
| 50 | if hours > 0 or days > 0: |
| 51 | out.append('%02dh' % hours) |
| 52 | if minutes > 0 or hours > 0 or days > 0: |
| 53 | out.append('%02dm' % minutes) |
| 54 | if seconds > 0 and not out: |
| 55 | # Skip seconds unless there's only seconds. |
| 56 | out.append('%02ds' % seconds) |
| 57 | return prefix + ''.join(out) |
| 58 | |
| 59 | |
| 60 | class Stats(object): |
| 61 | def __init__(self): |
| 62 | self.total = 0 |
| 63 | self.actually_reviewed = 0 |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 64 | self.latencies = [] |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 65 | self.lgtms = 0 |
| 66 | self.multiple_lgtms = 0 |
| 67 | self.drive_by = 0 |
| 68 | self.not_requested = 0 |
[email protected] | 2e36aad | 2011-09-25 00:50:49 | [diff] [blame] | 69 | self.self_review = 0 |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 70 | |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 71 | self.percent_lgtm = 0. |
| 72 | self.percent_drive_by = 0. |
| 73 | self.percent_not_requested = 0. |
[email protected] | 2e36aad | 2011-09-25 00:50:49 | [diff] [blame] | 74 | self.days = 0 |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 75 | |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 76 | @property |
| 77 | def average_latency(self): |
| 78 | if not self.latencies: |
| 79 | return 0 |
| 80 | return sum(self.latencies) / float(len(self.latencies)) |
| 81 | |
| 82 | @property |
| 83 | def median_latency(self): |
| 84 | if not self.latencies: |
| 85 | return 0 |
| 86 | length = len(self.latencies) |
| 87 | latencies = sorted(self.latencies) |
| 88 | if (length & 1) == 0: |
[email protected] | fbb00c4 | 2013-07-17 00:15:39 | [diff] [blame] | 89 | return (latencies[length/2] + latencies[length/2-1]) / 2. |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 90 | else: |
| 91 | return latencies[length/2] |
| 92 | |
| 93 | @property |
| 94 | def percent_done(self): |
| 95 | if not self.total: |
| 96 | return 0 |
| 97 | return self.actually_reviewed * 100. / self.total |
| 98 | |
| 99 | @property |
| 100 | def review_per_day(self): |
| 101 | if not self.days: |
| 102 | return 0 |
| 103 | return self.total * 1. / self.days |
| 104 | |
| 105 | @property |
| 106 | def review_done_per_day(self): |
| 107 | if not self.days: |
| 108 | return 0 |
| 109 | return self.actually_reviewed * 1. / self.days |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 110 | |
| 111 | def finalize(self, first_day, last_day): |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 112 | if self.actually_reviewed: |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 113 | assert self.actually_reviewed > 0 |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 114 | self.percent_lgtm = (self.lgtms * 100. / self.actually_reviewed) |
| 115 | self.percent_drive_by = (self.drive_by * 100. / self.actually_reviewed) |
| 116 | self.percent_not_requested = ( |
| 117 | self.not_requested * 100. / self.actually_reviewed) |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 118 | assert bool(first_day) == bool(last_day) |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 119 | if first_day and last_day: |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 120 | assert first_day <= last_day |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 121 | self.days = (to_datetime(last_day) - to_datetime(first_day)).days + 1 |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 122 | assert self.days > 0 |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 123 | |
| 124 | |
| 125 | def _process_issue_lgtms(issue, reviewer, stats): |
| 126 | """Calculates LGTMs stats.""" |
| 127 | stats.actually_reviewed += 1 |
| 128 | reviewer_lgtms = len([ |
| 129 | msg for msg in issue['messages'] |
| 130 | if msg['approval'] and msg['sender'] == reviewer]) |
| 131 | if reviewer_lgtms > 1: |
| 132 | stats.multiple_lgtms += 1 |
| 133 | return ' X ' |
| 134 | if reviewer_lgtms: |
| 135 | stats.lgtms += 1 |
| 136 | return ' x ' |
| 137 | else: |
| 138 | return ' o ' |
| 139 | |
| 140 | |
| 141 | def _process_issue_latency(issue, reviewer, stats): |
| 142 | """Calculates latency for an issue that was actually reviewed.""" |
| 143 | from_owner = [ |
| 144 | msg for msg in issue['messages'] if msg['sender'] == issue['owner_email'] |
| 145 | ] |
| 146 | if not from_owner: |
| 147 | # Probably requested by email. |
| 148 | stats.not_requested += 1 |
| 149 | return '<no rqst sent>' |
| 150 | |
| 151 | first_msg_from_owner = None |
| 152 | latency = None |
| 153 | received = False |
| 154 | for index, msg in enumerate(issue['messages']): |
| 155 | if not first_msg_from_owner and msg['sender'] == issue['owner_email']: |
| 156 | first_msg_from_owner = msg |
| 157 | if index and not received and msg['sender'] == reviewer: |
| 158 | # Not first email, reviewer never received one, reviewer sent a mesage. |
| 159 | stats.drive_by += 1 |
| 160 | return '<drive-by>' |
| 161 | received |= reviewer in msg['recipients'] |
| 162 | |
| 163 | if first_msg_from_owner and msg['sender'] == reviewer: |
| 164 | delta = msg['date'] - first_msg_from_owner['date'] |
| 165 | latency = delta.seconds + delta.days * 24 * 3600 |
| 166 | break |
| 167 | |
| 168 | if latency is None: |
| 169 | stats.not_requested += 1 |
| 170 | return '<no rqst sent>' |
| 171 | if latency > 0: |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 172 | stats.latencies.append(latency) |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 173 | else: |
| 174 | stats.not_requested += 1 |
| 175 | return to_time(latency) |
| 176 | |
| 177 | |
| 178 | def _process_issue(issue): |
| 179 | """Preprocesses the issue to simplify the remaining code.""" |
| 180 | issue['owner_email'] = username(issue['owner_email']) |
| 181 | issue['reviewers'] = set(username(r) for r in issue['reviewers']) |
| 182 | # By default, hide commit-bot. |
| 183 | issue['reviewers'] -= set(['commit-bot']) |
| 184 | for msg in issue['messages']: |
| 185 | msg['sender'] = username(msg['sender']) |
| 186 | msg['recipients'] = [username(r) for r in msg['recipients']] |
| 187 | # Convert all times to datetime instances. |
| 188 | msg['date'] = to_datetime(msg['date']) |
| 189 | issue['messages'].sort(key=lambda x: x['date']) |
| 190 | |
| 191 | |
| 192 | def print_issue(issue, reviewer, stats): |
| 193 | """Process an issue and prints stats about it.""" |
| 194 | stats.total += 1 |
| 195 | _process_issue(issue) |
[email protected] | 2e36aad | 2011-09-25 00:50:49 | [diff] [blame] | 196 | if issue['owner_email'] == reviewer: |
| 197 | stats.self_review += 1 |
| 198 | latency = '<self review>' |
| 199 | reviewed = '' |
| 200 | elif any(msg['sender'] == reviewer for msg in issue['messages']): |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 201 | reviewed = _process_issue_lgtms(issue, reviewer, stats) |
| 202 | latency = _process_issue_latency(issue, reviewer, stats) |
| 203 | else: |
| 204 | latency = 'N/A' |
| 205 | reviewed = '' |
| 206 | |
| 207 | # More information is available, print issue.keys() to see them. |
| 208 | print '%7d %10s %3s %14s %-15s %s' % ( |
| 209 | issue['issue'], |
| 210 | issue['created'][:10], |
| 211 | reviewed, |
| 212 | latency, |
| 213 | issue['owner_email'], |
| 214 | ', '.join(sorted(issue['reviewers']))) |
| 215 | |
| 216 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 217 | def print_reviews(reviewer, created_after, created_before, instance_url): |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 218 | """Prints issues |reviewer| received and potentially reviewed.""" |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 219 | remote = rietveld.Rietveld(instance_url, None, None) |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 220 | |
| 221 | # The stats we gather. Feel free to send me a CL to get more stats. |
| 222 | stats = Stats() |
| 223 | |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 224 | # Column sizes need to match print_issue() output. |
| 225 | print >> sys.stderr, ( |
| 226 | 'Issue Creation Did Latency Owner Reviewers') |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 227 | |
| 228 | # See def search() in rietveld.py to see all the filters you can use. |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 229 | issues = [] |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 230 | for issue in remote.search( |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 231 | reviewer=reviewer, |
| 232 | created_after=created_after, |
| 233 | created_before=created_before, |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 234 | with_messages=True): |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 235 | issues.append(issue) |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 236 | print_issue(issue, username(reviewer), stats) |
[email protected] | 17f5282 | 2013-06-03 23:40:10 | [diff] [blame] | 237 | |
| 238 | issues.sort(key=lambda x: x['created']) |
| 239 | first_day = None |
| 240 | last_day = None |
| 241 | if issues: |
| 242 | first_day = issues[0]['created'][:10] |
| 243 | last_day = issues[-1]['created'][:10] |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 244 | stats.finalize(first_day, last_day) |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 245 | |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 246 | print >> sys.stderr, ( |
[email protected] | 2e36aad | 2011-09-25 00:50:49 | [diff] [blame] | 247 | '%s reviewed %d issues out of %d (%1.1f%%). %d were self-review.' % |
| 248 | (reviewer, stats.actually_reviewed, stats.total, stats.percent_done, |
| 249 | stats.self_review)) |
[email protected] | 34dd5eb | 2011-09-23 21:11:18 | [diff] [blame] | 250 | print >> sys.stderr, ( |
| 251 | '%4.1f review request/day during %3d days (%4.1f r/d done).' % ( |
| 252 | stats.review_per_day, stats.days, stats.review_done_per_day)) |
| 253 | print >> sys.stderr, ( |
| 254 | '%4d were drive-bys (%5.1f%% of reviews done).' % ( |
| 255 | stats.drive_by, stats.percent_drive_by)) |
| 256 | print >> sys.stderr, ( |
| 257 | '%4d were requested over IM or irc (%5.1f%% of reviews done).' % ( |
| 258 | stats.not_requested, stats.percent_not_requested)) |
| 259 | print >> sys.stderr, ( |
| 260 | ('%4d issues LGTM\'d (%5.1f%% of reviews done),' |
| 261 | ' gave multiple LGTMs on %d issues.') % ( |
| 262 | stats.lgtms, stats.percent_lgtm, stats.multiple_lgtms)) |
| 263 | print >> sys.stderr, ( |
| 264 | 'Average latency from request to first comment is %s.' % |
| 265 | to_time(stats.average_latency)) |
[email protected] | 40b7687 | 2012-03-21 01:07:44 | [diff] [blame] | 266 | print >> sys.stderr, ( |
| 267 | 'Median latency from request to first comment is %s.' % |
| 268 | to_time(stats.median_latency)) |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 269 | |
| 270 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 271 | def print_count(reviewer, created_after, created_before, instance_url): |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 272 | remote = rietveld.Rietveld(instance_url, None, None) |
| 273 | print len(list(remote.search( |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 274 | reviewer=reviewer, |
| 275 | created_after=created_after, |
| 276 | created_before=created_before, |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 277 | keys_only=True))) |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 278 | |
| 279 | |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 280 | def get_previous_quarter(today): |
| 281 | """There are four quarters, 01-03, 04-06, 07-09, 10-12. |
| 282 | |
| 283 | If today is in the last month of a quarter, assume it's the current quarter |
| 284 | that is requested. |
| 285 | """ |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 286 | end_year = today.year |
| 287 | end_month = today.month - (today.month % 3) + 1 |
| 288 | if end_month <= 0: |
| 289 | end_year -= 1 |
| 290 | end_month += 12 |
| 291 | if end_month > 12: |
| 292 | end_year += 1 |
| 293 | end_month -= 12 |
| 294 | end = '%d-%02d-01' % (end_year, end_month) |
| 295 | begin_year = end_year |
| 296 | begin_month = end_month - 3 |
| 297 | if begin_month <= 0: |
| 298 | begin_year -= 1 |
| 299 | begin_month += 12 |
| 300 | begin = '%d-%02d-01' % (begin_year, begin_month) |
| 301 | return begin, end |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 302 | |
| 303 | |
| 304 | def main(): |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 305 | # Silence upload.py. |
| 306 | rietveld.upload.verbosity = 0 |
[email protected] | 70e91c0 | 2011-09-22 19:14:51 | [diff] [blame] | 307 | today = datetime.date.today() |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 308 | begin, end = get_previous_quarter(today) |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 309 | default_email = os.environ.get('EMAIL_ADDRESS') |
| 310 | if not default_email: |
| 311 | user = os.environ.get('USER') |
| 312 | if user: |
| 313 | default_email = user + '@chromium.org' |
| 314 | |
| 315 | parser = optparse.OptionParser(description=__doc__) |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 316 | parser.add_option( |
| 317 | '--count', action='store_true', |
| 318 | help='Just count instead of printing individual issues') |
[email protected] | 70e91c0 | 2011-09-22 19:14:51 | [diff] [blame] | 319 | parser.add_option( |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 320 | '-r', '--reviewer', metavar='<email>', default=default_email, |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 321 | help='Filter on issue reviewer, default=%default') |
[email protected] | 70e91c0 | 2011-09-22 19:14:51 | [diff] [blame] | 322 | parser.add_option( |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 323 | '-b', '--begin', metavar='<date>', |
[email protected] | 70e91c0 | 2011-09-22 19:14:51 | [diff] [blame] | 324 | help='Filter issues created after the date') |
| 325 | parser.add_option( |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 326 | '-e', '--end', metavar='<date>', |
| 327 | help='Filter issues created before the date') |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 328 | parser.add_option( |
| 329 | '-Q', '--last_quarter', action='store_true', |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 330 | help='Use last quarter\'s dates, e.g. %s to %s' % (begin, end)) |
[email protected] | 70e91c0 | 2011-09-22 19:14:51 | [diff] [blame] | 331 | parser.add_option( |
| 332 | '-i', '--instance_url', metavar='<host>', |
| 333 | default='http://codereview.chromium.org', |
| 334 | help='Host to use, default is %default') |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 335 | # Remove description formatting |
[email protected] | 14e37ad | 2011-11-30 20:26:16 | [diff] [blame] | 336 | parser.format_description = ( |
| 337 | lambda _: parser.description) # pylint: disable=E1101 |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 338 | options, args = parser.parse_args() |
| 339 | if args: |
| 340 | parser.error('Args unsupported') |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 341 | if options.reviewer is None: |
| 342 | parser.error('$EMAIL_ADDRESS and $USER are not set, please use -r') |
| 343 | |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 344 | print >> sys.stderr, 'Searching for reviews by %s' % options.reviewer |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 345 | if options.last_quarter: |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 346 | options.begin = begin |
| 347 | options.end = end |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 348 | print >> sys.stderr, 'Using range %s to %s' % ( |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 349 | options.begin, options.end) |
[email protected] | dc33eae | 2014-12-11 17:15:26 | [diff] [blame^] | 350 | else: |
| 351 | if options.begin is None or options.end is None: |
| 352 | parser.error('Please specify either --last_quarter or --begin and --end') |
[email protected] | 16c319d | 2014-03-11 20:04:20 | [diff] [blame] | 353 | |
| 354 | # Validate dates. |
| 355 | try: |
| 356 | to_datetime(options.begin) |
| 357 | to_datetime(options.end) |
| 358 | except ValueError as e: |
| 359 | parser.error('%s: %s - %s' % (e, options.begin, options.end)) |
| 360 | |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 361 | if options.count: |
| 362 | print_count( |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 363 | options.reviewer, |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 364 | options.begin, |
| 365 | options.end, |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 366 | options.instance_url) |
| 367 | else: |
| 368 | print_reviews( |
[email protected] | 0bccb92 | 2011-09-22 19:22:22 | [diff] [blame] | 369 | options.reviewer, |
[email protected] | 5e6868b | 2011-09-22 20:15:07 | [diff] [blame] | 370 | options.begin, |
| 371 | options.end, |
[email protected] | b770432 | 2011-09-22 15:22:05 | [diff] [blame] | 372 | options.instance_url) |
[email protected] | c73e516 | 2011-09-21 23:16:12 | [diff] [blame] | 373 | return 0 |
| 374 | |
| 375 | |
| 376 | if __name__ == '__main__': |
| 377 | sys.exit(main()) |