Awight has submitted this change and it was merged.

Change subject: Copy files from /srv/br
......................................................................


Copy files from /srv/br

Change-Id: I7dea2c11b602d4b9b18bd3174af469d7423f6b82
---
A __init__.py
A amountchoices.py
A amountstats
A bimp
A check_bannerimpressions.py
A ecom
A fr.py
A fr_util.py
A statler
A stats_abba.py
A timeline.py
A which_impression.py
12 files changed, 1,857 insertions(+), 0 deletions(-)

Approvals:
  Awight: Verified; Looks good to me, approved



diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/amountchoices.py b/amountchoices.py
new file mode 100755
index 0000000..2bccabc
--- /dev/null
+++ b/amountchoices.py
@@ -0,0 +1,71 @@
+#! /usr/bin/env python
+
+from datetime import datetime
+import dateutil.parser
+import MySQLdb, fr_util
+
+def main():
+    rows = get_results()
+    print "amount\tdonations\ttotal"
+    for row in rows:
+        for c in row:
+            print c, '\t',
+        print
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(description="""Get donations split out by amount 
for a given currency.""")
+
+    parser.add_argument('currency', help='ISO code of currency to check e.g. 
GBP')
+    parser.add_argument('-s', '--start', dest='start', 
default='20130701000000',
+        help='Start time (UTC). If not specified, defaults to 1 Jul 2013.')
+    parser.add_argument('-e', '--end', dest='end', 
default=datetime.utcnow().strftime('%Y%m%d%H%M%S'), 
+        help='End time (UTC). If not specified, defaults to now.')
+    parser.add_argument('--sub', dest='substring', 
+        help='utm_source substring e.g. B13_0905_badge')
+    parser.add_argument('--country', dest='country',
+        help='Filter by country (ISO code) e.g. GB')
+
+    args = vars(parser.parse_args())
+    return args
+
+def get_results():
+    # need to use a subquery here, in order to do calculation using aliases
+    query = """
+    SELECT 
+        e.original_amount AS amount, 
+        count(e.id) AS donations, 
+        e.original_amount * count(e.id) AS total        
+    FROM 
+        drupal.contribution_tracking ct
+        INNER JOIN civicrm.civicrm_contribution cc ON ct.contribution_id = 
cc.id
+        LEFT JOIN civicrm.wmf_contribution_extra e ON e.entity_id = cc.id
+    WHERE
+        e.original_currency = %(currency)s
+        AND receive_date BETWEEN %(start2)s AND %(end2)s"""
+
+    if args['substring']:
+        query += " AND ct.utm_source RLIKE %(substring)s"
+
+    if args['country']:
+        query += " AND ct.country=%(country)s"
+
+    query += " GROUP BY e.original_amount ORDER BY e.original_amount"
+
+    cursor.execute(query, args)
+
+    print cursor._last_executed
+    
+    rows = cursor.fetchall()
+    return rows
+
+
+if __name__ == "__main__":
+    args = get_options()
+
+    # have these in the correct format
+    args['start2'] = dateutil.parser.parse(args['start'])
+    args['end2']   = dateutil.parser.parse(args['end'])
+
+    cursor = fr_util.get_db_cursor()
+    main()
diff --git a/amountstats b/amountstats
new file mode 100755
index 0000000..8ff334b
--- /dev/null
+++ b/amountstats
@@ -0,0 +1,106 @@
+#! /usr/bin/env python
+
+from datetime import datetime
+import dateutil.parser
+import dateutil.relativedelta
+import numpy
+import MySQLdb, sys, fr_util
+
+TSFORMAT = '%Y%m%d%H%M%S'
+DATEFORMAT = '%Y-%m-%d %H:%M:%S'
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(
+        description="Get statistics on amounts for a given banner/email"
+        )
+
+    parser.add_argument('substring',
+        help='Banner/email substring e.g. B14_0723_mob. You can even use a 
regular expression if you like.')
+
+    # We'll run the date parser on these later
+    parser.add_argument('-s', '--start', dest='start',
+        help='Start time (UTC). If not specified, defaults to one month ago.')
+    parser.add_argument('-e', '--end', dest='end',
+        help='End time (UTC). If not specified, defaults to now.')
+
+    parser.add_argument('--sql', action='store_true',
+        help='Show SQL used (for debugging)')
+
+    parser.add_argument('--language', dest='language', 
+        help='Filter results to specific language')
+    parser.add_argument('--country', dest='country', 
+        help='Filter results to specific country code')
+
+    if len(sys.argv) == 1:
+        # No arguments, show instructions
+        parser.print_help()
+        sys.exit(1)
+
+    args = vars(parser.parse_args())
+    return args
+
+
+def build_query():
+    query = """SELECT
+        cc.total_amount as amount
+    FROM
+        drupal.contribution_tracking ct
+        INNER JOIN civicrm.civicrm_contribution cc ON ct.contribution_id = 
cc.id
+    WHERE
+        ts BETWEEN %(start_ts)s AND %(end_ts)s
+        AND ct.utm_source REGEXP %(substring)s"""
+
+    if args['country']:
+        query += "AND ct.country=%(country)s"
+
+    if args['language']:
+        query += "AND ct.language=%(language)s"
+
+    return query
+
+def get_results():
+    cursor.execute(query, args)
+    rows = cursor.fetchall()
+    return rows
+
+
+if __name__ == "__main__":
+    args = get_options()
+
+    if args['start']:
+        args['start'] = dateutil.parser.parse(args['start'])
+    else:
+        # default to 1 month ago
+        args['start'] = (datetime.now() + 
dateutil.relativedelta.relativedelta(months = -1))
+
+    if args['end']:
+        args['end'] = dateutil.parser.parse(args['end'])
+    else:
+        # default to now
+        args['end'] = datetime.now()
+
+    # Timestamp format for queries
+    args['start_ts'] = args['start'].strftime(TSFORMAT)
+    args['end_ts']   = args['end'].strftime(TSFORMAT)
+
+    cursor = fr_util.get_db_cursor()
+    query  = build_query()
+    rows   = get_results()
+    if args['sql']:
+        print cursor._last_executed
+
+    if args['country']:
+        print "Country: " + args['country']
+    if args['language']:
+        print "Language: " + args['language']
+
+    print "From: " + args['start'].strftime(DATEFORMAT)
+    print "To: "   + args['end'].strftime(DATEFORMAT)
+    print
+    amounts = [float(r[0]) for r in rows]
+    # print amounts
+    print "Number of donations: {0}".format( len(amounts) )
+    print "Total amount:  ${:.2f}".format( sum(amounts) )
+    print "Mean amount:   ${:.2f}".format( numpy.mean(amounts) )
+    print "Median amount: ${:.2f}".format( numpy.median(amounts) )
\ No newline at end of file
diff --git a/bimp b/bimp
new file mode 100755
index 0000000..f9894f6
--- /dev/null
+++ b/bimp
@@ -0,0 +1,107 @@
+#! /usr/bin/env python
+
+import time
+import MySQLdb, fr_util
+
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(description='Get banner impressions (only accurate 
about 15 minutes after test)')
+
+    parser.add_argument('-s', '--start', dest='start',
+                        metavar='YYYYMMDDHHMMSS',
+                        help='Start time', 
+                        required=True)
+    parser.add_argument('-e', '--end', dest='end',
+                        metavar='YYYYMMDDHHMMSS',
+                        help='End time. If not specified, defaults to now',
+                        default=time.strftime('%Y%m%d%H%M%S'))
+    parser.add_argument('--sub', dest='substring', 
+                        help='Banner name substring e.g. B13_0905_badge')
+    parser.add_argument('-c', '--campaign', dest='campaign',
+                        help='Filter by campaign e.g. C14_enFI')
+    parser.add_argument('--country', dest='country',
+                        help='Filter by country e.g. GB')
+    parser.add_argument('--language', dest='language',
+                        help='Filter by language e.g. en')
+    parser.add_argument('-g', '--group_by', dest='group_by',
+                        metavar='bg', 
+                        help='Fields to group by. b=ban, c=country, 
g=language, t=campaign (test)',
+                        default='b')
+    parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
+                        help='Don\'t display SQL code')
+    parser.add_argument('--raw', dest='raw', action='store_true',
+                        help='For compatibility with ecom (has no effect)')
+    args = vars(parser.parse_args());
+    return args
+
+def build_query(args):
+    query =  "SELECT " 
+    for i in args['group_by']:
+        if i == 'b': query += 'banner, '
+        if i == 'c': query += 'country.iso_code AS country, '
+        if i == 'g': query += 'language.iso_code AS language, '
+        if i == 't': query += 'campaign,'
+    query += "SUM(count) AS ban_imps"
+
+    query += "\n FROM pgehres.bannerimpressions"
+    query += "\n   JOIN pgehres.country ON (country_id=country.id)"
+    query += "\n   JOIN pgehres.language ON (language_id=language.id)"
+    query += "\n WHERE timestamp BETWEEN '{0}' and '{1}' 
".format(args['start'], args['end'])
+    if (args['substring']):
+        query += "\n AND banner LIKE '%{0}%' ".format(args['substring'])
+    if (args['campaign']):
+        query += "\n AND campaign LIKE '{0}'".format(args['campaign'])
+    if (args['country']):
+        query += "\n AND country.iso_code='{0}'".format(args['country'])
+    if (args['language']):
+        query += "\n AND language.iso_code='{0}'".format(args['language'])
+
+    query += "\n GROUP BY "
+    for i in args['group_by']:
+        if i == 'b': query += 'banner, '
+        if i == 'c': query += 'country.iso_code, '
+        if i == 'g': query += 'language.iso_code, '
+        if i == 't': query += 'campaign, '
+    query = query.rstrip(', ')
+
+    query += "\n ORDER BY "
+    if 'c' in args['group_by']: query += 'country.iso_code, '
+    if 'g' in args['group_by']: query += 'language.iso_code, '
+    if 't' in args['group_by']: query += 'campaign, '
+    query += 'banner'
+
+    query = query.rstrip(', ')
+    return query
+
+def print_results(rows):
+    for c in desc:
+        print c[0], # column title
+    print
+    for row in rows:
+        for c in row:
+            print c,
+        print
+
+class TimeTravelException(Exception):
+    pass
+
+
+if __name__ == "__main__":
+
+    args = get_options()
+
+    if (args['end'] < args['start']):
+        raise TimeTravelException("End time cannot be before start time!")
+
+    query = build_query(args)
+
+    if (not args['quiet']):
+        print query
+
+    cursor = fr_util.get_db_cursor()
+    cursor.execute(query)
+    rows = cursor.fetchall()
+    desc = cursor.description
+
+    print_results(rows)
\ No newline at end of file
diff --git a/check_bannerimpressions.py b/check_bannerimpressions.py
new file mode 100755
index 0000000..5f3b474
--- /dev/null
+++ b/check_bannerimpressions.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+"""Check to see if there were impressions within the last hour.
+If not, print a warning to stderr."""
+
+from __future__ import print_function
+import sys
+import MySQLdb, fr_util
+
+interval = "1 hour"
+message = "Warning: no bannerimpressions entries in past {}. You should 
probably check on that.".format(interval)
+
+def main():
+
+    cursor = fr_util.get_db_cursor(type='dict')
+
+    query = """
+        SELECT timestamp 
+        FROM pgehres.bannerimpressions 
+        WHERE timestamp > DATE_SUB( NOW(), INTERVAL {} )
+        LIMIT 10;""".format(interval)
+
+    cursor.execute(query)
+    data = cursor.fetchall()
+
+    if not data:
+        print(message, file=sys.stderr)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/ecom b/ecom
new file mode 100755
index 0000000..2cb4646
--- /dev/null
+++ b/ecom
@@ -0,0 +1,5 @@
+#! /usr/bin/env python
+
+import fr
+optdict = fr.get_options()
+fr.donations(optdict)
diff --git a/fr.py b/fr.py
new file mode 100644
index 0000000..c811a0c
--- /dev/null
+++ b/fr.py
@@ -0,0 +1,733 @@
+#! /usr/bin/env python
+from datetime import datetime, timedelta
+import MySQLdb
+from itertools import *
+from operator import *
+import sys
+import re
+
+class bcolors:
+    HEADER = '\033[95m'
+    OKBLUE = '\033[94m'
+    OKGREEN = '\033[92m'
+    WARNING = '\033[93m'
+    FAIL = '\033[91m'
+    ENDC = '\033[0m'
+
+def str_time_offset(str_time, offset = 1):
+    time_time = datetime.strptime( str_time, '%Y%m%d%H%M%S' )
+    str_time = ( time_time + timedelta( hours = offset )).strftime( 
'%Y%m%d%H%M%S' )
+    return(str_time)
+
+def str_now():
+    return( datetime.now().strftime('%Y%m%d%H%M%S') )
+
+def datetimefunix( unix_timestamp ):
+    return datetime.fromtimestamp(unix_timestamp)
+
+def strfunix( unix_timestamp ):
+    return datetime.fromtimestamp(unix_timestamp).strftime('%Y-%m-%d %H:%M')
+
+def get_cursor(type='dict'):
+    conn = MySQLdb.connect(host='localhost',
+                            db='civicrm',
+                            unix_socket='/tmp/mysql.sock',
+                            read_default_file="~/.my.cnf")
+    if type == 'dict':
+        cursor = conn.cursor(MySQLdb.cursors.DictCursor)
+    elif type == 'norm':
+        cursor = conn.cursor()
+    return cursor
+
+def count_symbol( c ):
+    char = ""
+    if c == 0: char = " "
+    elif c == 1: char = u"\u2802"
+    elif c == 2: char = u"\u2803"
+    elif c == 3: char = u"\u2807"
+    elif c == 4: char = u"\u280F"
+    elif c == 5: char = u"\u281F"
+    elif c == 6: char = u"\u283F"
+    elif c == 7: char = u"\u287F"
+    elif c == 8: char = u"\u28FF"
+    elif c >= 9 and c < 50: char = u"\u2169"
+    elif c >= 50 and c < 100: char = u"\u216C"
+    elif c >= 100 and c < 500: char = u"\u216D"
+    elif c >= 500 and c < 1000: char = u"\u216E"
+    elif c >= 1000 and c < 5000: char = u"\u216E"
+    elif c >= 5000 and c < 10000: char = u"\u2181"
+    elif c >= 10000: char = u"\u2182"
+    else: char = "?"
+    return( char )
+
+class Campaign:
+    def __init__(self, utm_campaign, start=None, end=None,
+            interval=1, country=None, language=None, multicountry=None):
+        self.utm_campaign = utm_campaign
+        self.interval = interval
+        query = '''select
+            min(ts) cstart, max(ts) cend
+            from drupal.contribution_tracking
+            where '''
+        if start != None and end != None:
+            query += " ts between '%s' and '%s' and  " % (start, end)
+        query += "utm_campaign like '%s%%';" % ( utm_campaign )
+        cursor = get_cursor()
+        cursor.execute(query)
+        row = cursor.fetchone()
+# find the outermost book ends of the campign
+        self.first_don = row['cstart']
+        self.last_don = row['cend']
+        if start == None and end == None:
+            start = self.first_don
+            end = self.last_don
+            self.start = start
+            self.end = end
+        query = '''select
+            unix_timestamp(ts) div (60*%s) minit,
+            count(*) cnt
+            from drupal.contribution_tracking
+            where ''' % interval
+        if start != None and end != None:
+            query += " ts between '%s' and '%s' and  " % (start, end)
+        query += '''utm_campaign like '%s%%'
+        group by unix_timestamp(ts) div (60*%s) having count(*) > 0;''' % (
+        utm_campaign,  interval)
+        cursor.execute(query)
+        self.minits = cursor.fetchall() # making the results available in case 
useful
+# list of all the minit values, and then don counts for each
+        self.minit_list = []
+        self.minit_counts = {}
+        for row in self.minits:
+            self.minit_list.append(row['minit'])
+            self.minit_counts[row['minit']] = row['cnt']
+# list of lists of consequtive blocks of minits (i.e. when the camp was on)
+        self.runs = []
+        for k, g in groupby(enumerate(self.minit_list), lambda (i,x):i-x):
+            self.runs.append( map(itemgetter(1), g) )
+        self.runs_by_len = sorted(self.runs, key=len)
+# lists of the hours and days over which a campaign ran
+        query = '''select
+        left(ts,8) dy,
+        left(ts,10) hr,
+        count(*) cnt
+        from drupal.contribution_tracking
+        where ts between '%s' and '%s' and  utm_campaign like '%s%%'
+        group by 1 ,2
+        order by 2;''' % (start, end, utm_campaign)
+        cursor.execute(query)
+        rs = cursor.fetchall()
+        self.days = sorted(set([ row['dy'] for row in rs ]))
+        self.hours = sorted(set([ row['hr'] for row in rs ]))
+        self.day_counts = {}
+        self.hour_counts = {}
+        for row in rs:
+            self.day_counts[row['dy']] = row['cnt']
+        for row in rs:
+            self.hour_counts[row['hr']] = row['cnt']
+
+    def show_days(self, single_day = None):
+        print "         +-----------+-----------+-----------+-----------+"
+        if single_day == None:
+            days = self.days
+        else:
+            days = [single_day]
+        for dy in days:
+            print dy,
+            for hr in range(0,23):
+                strhr = "%s%02d" % (dy, hr)
+                if dy in strhr:
+                    if strhr in self.hour_counts.keys():
+                        print count_symbol(self.hour_counts[strhr]),
+                    else:
+                        print " ",
+            print
+
+    def show_hours(self, single_hour = None):
+        cursor = get_cursor()
+        if self.interval == 1:
+            print "           
--------+--------(1)--------+--------(2)--------+--------(3)--------+--------(4)--------+--------(5)--------+--------(6)"
+        elif self.interval == 5:
+            print "           ---+---+---+---+---+---+"
+        for hr in self.hours:
+            print "%s %02d " % ( hr[2:8], int(hr[8:10]) ),
+            hour_start_minit =  int(datetime.strptime(hr, 
"%Y%m%d%H").strftime("%s")) // (60*self.interval)
+            hour_end_minit =  ( int(datetime.strptime(hr, 
"%Y%m%d%H").strftime("%s"))+60*60) // (60*self.interval)
+            query = '''select
+            unix_timestamp(ts) div (60*%s) minit,
+            count(*) cnt
+            from drupal.contribution_tracking
+            where ts between '%s0000' and '%s5959' and  utm_campaign like 
'%s%%'
+            group by unix_timestamp(ts) div (60*%s)
+            having count(*) > 0;''' % (self.interval, hr, hr, 
self.utm_campaign, self.interval)
+            cursor.execute(query)
+            rs = cursor.fetchall()
+            minits_in_hour = dict([(row['minit'], row['cnt']) for row in rs ])
+            for minit in range(hour_start_minit, hour_end_minit):
+                if minit in minits_in_hour:
+                    print count_symbol( int(self.minit_counts[minit]) ),
+                else:
+                    print " " ,
+            print
+
+def show_campaign_months(start=None, end=None, country=None, language=None, 
multicountry=None):
+    query = '''select
+        if(utm_campaign regexp '_[A-Z]{2}$' and utm_campaign not like '%%_FR',
+        left(utm_campaign,length(utm_campaign)-3), utm_campaign) as camp,
+        left(ts,8) day,
+        count(*) cnt
+        from drupal.contribution_tracking
+        where ts between '%s' and '%s'
+        group by camp, day
+        having count(*) > 10
+        order by camp, day
+    ''' % ( start, end )
+    cursor = get_cursor()
+    cursor.execute(query)
+    rs = cursor.fetchall()
+    dist_camps =  sorted(set([row['camp'] if row['camp'] != None
+        else 'Null' for row in rs]))
+    dist_days = sorted(set([row['day'] for row in rs]))
+    camp_name_width = len(max(dist_camps, key=len))
+    if camp_name_width > 21: camp_name_width = 21
+    print "%*s +%s%s%s+" % (26, " ", dist_days[0]
+            ,"-"*((len(dist_days)*2)-18), dist_days[-1])
+    for c in dist_camps:
+        if len(c) > 20:
+            short_name = "%s*" % c[:19]
+        else:
+            short_name = c
+        print "%3s) %*s" % (dist_camps.index(c),
+                camp_name_width, short_name),
+        for dy in dist_days:
+            cc = count_from_camp_day(rs, c, dy)
+            if cc:
+                print count_symbol(cc),
+            else:
+                print " ",
+        print
+    return dist_camps
+
+def show_campaign_days2(start=None, end=None,
+        country=None, language=None, multicountry=None):
+    query = '''select if(utm_campaign regexp '_[A-Z]{2}$' and utm_campaign not 
like '%%_FR',
+    left(utm_campaign,length(utm_campaign)-3), utm_campaign) as camp,
+    left(ts,10) hour,
+    count(*) cnt
+    from drupal.contribution_tracking
+    where ts between '%s' and '%s'
+    group by camp, hour
+    having count(*) > 10
+    order by camp, hour
+    ''' % ( start, end )
+    cursor = get_cursor()
+    cursor.execute(query)
+    rs = cursor.fetchall()
+    dist_camps =  sorted(set([row['camp'] if row['camp'] != None else 'Null' 
for row in rs]))
+    dist_days = sorted(set([row['hour'][0:8] for row in rs]))
+    dist_hours = sorted(set([row['hour'] for row in rs]))
+    camp_name_width = len(max(dist_camps, key=len))
+    print "%*s +%s%s%s+" % (29, " ", dist_hours[0]
+            ,"-"*((len(dist_hours)*2)-22), dist_hours[-1])
+    for c in dist_camps:
+        print "%3s) %*s" % (dist_camps.index(c), camp_name_width, c),
+        for dy in dist_days:
+            for hr in range(0,23):
+                strhr = "%s%02d" % (dy, hr)
+                if strhr < dist_hours[0]: continue
+                if dy in strhr:
+                    cc = count_from_camp_hour(rs, c, strhr)
+                    if cc:
+                        print count_symbol(cc),
+                    else:
+                        print " ",
+                    if strhr == dist_hours[-1]: break
+        print
+    return dist_camps
+
+def show_campaign_days(start=None, end=None, thing_counted = 'donations',
+        country=None, language=None, multicountry=None):
+    if thing_counted == 'donations':
+        query = '''select if(utm_campaign regexp '_[A-Z]{2}$' and utm_campaign 
not like '%%_FR',
+                left(utm_campaign,length(utm_campaign)-3), utm_campaign) as 
camp,
+                left(ts,10) hour,
+                count(*) cnt
+                from
+                drupal.contribution_tracking ct
+                left join civicrm.civicrm_contribution cc on 
ct.contribution_id = cc.id
+                left join civicrm.civicrm_contact cn on cn.id = cc.contact_id
+                left join civicrm.civicrm_address ca on cc.contact_id = 
ca.contact_id
+                left join civicrm.civicrm_state_province sp on 
ca.state_province_id = sp.id
+                left join civicrm.civicrm_country co on ca.country_id = co.id
+                left join civicrm.civicrm_email ce on ce.contact_id = 
cc.contact_id
+                where ts between '%s' and '%s'
+                group by camp, hour
+                having count(*) > 10
+                order by camp, hour
+                ''' % ( start, end )
+    if thing_counted == 'clicks':
+        query = '''select if(utm_campaign regexp '_[A-Z]{2}$' and utm_campaign 
not like '%%_FR',
+                left(utm_campaign,length(utm_campaign)-3), utm_campaign) as 
camp,
+                left(ts,10) hour,
+                count(*) cnt
+                from drupal.contribution_tracking
+                where ts between '%s' and '%s'
+                group by camp, hour
+                having count(*) > 10
+                order by camp, hour
+                ''' % ( start, end )
+    cursor = get_cursor()
+    cursor.execute(query)
+    rs = cursor.fetchall()
+    dist_camps =  sorted(set([row['camp'] if row['camp'] != None else 'Null' 
for row in rs]))
+    dist_days = sorted(set([row['hour'][0:8] for row in rs]))
+    camp_name_width = len(max(dist_camps, key=len))
+    print "%*s +%s%s%s+" % (camp_name_width+3, " ", dist_days[0]
+            ,"-"*((len(dist_days)*2*24)-27), dist_days[-1])
+    for c in dist_camps:
+        print "%s) %*s" % (dist_camps.index(c), camp_name_width, c),
+        for dy in dist_days:
+            for hr in range(0,23):
+                strhr = "%s%02d" % (dy, hr)
+                if dy in strhr:
+                    cc = count_from_camp_hour(rs, c, strhr)
+                    if cc:
+                        print count_symbol(cc),
+                    else:
+                        print " ",
+        print
+    return dist_camps
+
+def count_from_camp_day(rs,camp,day):
+    for row in rs:
+        if row['camp'] == camp and row['day'] == day:
+            return row['cnt']
+    return None
+
+def count_from_camp_hour(rs,camp,hour):
+    for row in rs:
+        if row['camp'] == camp and row['hour'] == hour:
+            return row['cnt']
+    return None
+
+def get_group_by(group_by,optdict,semi=None):
+    query = "group by "
+    for c in group_by:
+        if c == 'b': 
+            if optdict['left']:
+                query += "left(src_banner,%s), " % optdict['left']
+            else:
+                query += "src_banner, "
+        if c == 'l': query += "landingpage, "
+        if c == 'p': query += "src_payment_method, "
+        if c == 'f': query += "payments_form, "
+        if c == 'c': query += "country, "
+    query = query.rstrip(', ')
+    if semi:
+        query += semi
+    return query
+
+def get_on(group_by,semi=None):
+    query = "on "
+    for c in group_by:
+        if c == 'b': query += "ecom.banner=lps.banner and "
+        if c == 'l': query += "ecom.landingpage=lps.landingpage and "
+        if c == 'p': query += "ecom.payment_method=lps.payment_method and "
+        if c == 'c': query += "convert(ecom.iso_code using utf8) 
=convert(lps.iso_code using utf8) and "
+    query = query[:-4]
+    if semi:
+        query += semi
+    return query
+
+def get_amts_query(optdict, bans):
+    start = optdict['start']
+    end = optdict['end']
+    campaign = optdict['campaign']
+    query = '''select
+if( utm_source regexp '%s','%s','%s') as banner,
+total_amount as amount
+from
+drupal.contribution_tracking ct
+join civicrm.civicrm_contribution cc on ct.contribution_id = cc.id
+where ts >= '%s' and ts < '%s'
+-- and utm_campaign regexp '%s'
+and (utm_source regexp '%s' or utm_source regexp '%s')
+''' % ( bans[0], bans[0], bans[1], start, end, campaign, bans[0], bans[1])
+    return query
+
+def utest(optdict,bans):
+    import rpy2.robjects as robjects
+    query = get_amts_query(optdict, bans)
+    rs_string, rs = get_rs(query, optdict['raw'])
+    a1 = []
+    a2 = []
+    r = robjects.r
+    # as_numeric = r['as.numeric']
+    for row in rs:
+        if row['banner'] == bans[1]:
+            a1.append( row['amount'] )
+        else:
+            a2.append( row['amount'] )
+    v1 = robjects.FloatVector(a1)
+    v2 = robjects.FloatVector(a2)
+    wilcox_result =  r['wilcox.test'](v1, v2)
+    print  "Wilcox test of means p= %.6f" % wilcox_result[2][0]
+    # print re.sub('data:.*\n','',wilcox_result)
+
+def get_test_query(optdict, type="donations"):
+    start = optdict['start']
+    end = optdict['end']
+    campaign = optdict['campaign']
+    query =""
+
+#wrapper around basic query for the bigger impression query
+    if type == "donations-impressions":
+        query += '''select lps.lpi, ecom.* 
+        from
+        (select '''
+        if 'group_by' in optdict.keys():
+            display_cols = optdict['group_by']
+            if display_cols:
+                for c in display_cols:
+                    if c == 'b':
+                        if optdict['multicountry']:
+                            query += "left(utm_source, length(utm_source)-3) 
as banner, "
+                        else:
+                            query += "utm_source as banner, "
+                    if c == 'l': 
+                        query += "landingpage, "
+                    if c == 'c': query += "ct.country, "
+        query += ("count(*) as lpi "
+            "from pgehres.landingpageimpression_raw l "
+            "left join pgehres.country co on l.country_id=co.id "
+            "where timestamp between '%s' and '%s' ") % (start, end)
+        if campaign != "None":
+            query += "and  utm_campaign regexp '%s' " % (campaign)
+        if optdict['substring']:
+            query += "and bis.banner regexp '%s' " % (optdict['substring'])
+# group by
+        if optdict['group_by']:
+            query += get_group_by(optdict['group_by'],optdict)
+        query += ") lps left join ("
+
+# donations part starts here
+# columns to display
+    query += "select "
+    if 'group_by' in optdict.keys():
+        display_cols = optdict['group_by']
+        if display_cols:
+            for c in display_cols:
+                if c == 'b': # TODO: change these to use new ct banner column 
when it's introduced
+                    if optdict['left']:
+                        query += 
"left(SUBSTRING_index(substring_index(ct.utm_source, '.', 2),'.',1),%s) as 
src_banner, " % optdict['left'] 
+                    elif optdict['multicountry']:
+                        query += 
"left(SUBSTRING_index(substring_index(ct.utm_source, '.', 2),'.',1), 
length(SUBSTRING_index(substring_index(ct.utm_source, '.', 2),'.',1))-3) as 
src_banner, "
+                    else:
+                        query += 
"SUBSTRING_index(substring_index(ct.utm_source, '.', 2),'.',1) as src_banner, "
+                if c == 'l': query += 
"SUBSTRING_index(substring_index(ct.utm_source, '.', 2),'.',-1) as landingpage, 
"
+                if c == 'p': query += "substring_index(ct.utm_source, '.', -1) 
as src_payment_method, " # TODO: change to use new ct payment_method column 
when it's introduced
+                if c == 'f': query += "payments_form as form, "
+                if c == 'c': query += "ct.country as country, "
+                if c == 't': query += "utm_campaign, "
+    query += '''sum(not isnull(cc.id)) as donations,
+            count(ct.id) as clicks,
+            -- sum(if(trxn_id like "GLOBALCOLLECT%%" or trxn_id like 
"PAYFLOW%%",1,0)) as cc,
+            -- sum(if(trxn_id like "PAYPAL%%",1,0)) as pp,
+            concat(round(sum(if(trxn_id like "GLOBALCOLLECT%%" or trxn_id like 
"PAYFLOW%%" or trxn_id like "ADYEN%%" or trxn_id like 
"WORLDPAY%%",1,0))/sum(not isnull(cc.id))*100),"%") as ccpct,
+            -- count(ct.id) as clicks,
+            -- sum(if(utm_source regexp ".cc",1,0)) as cc,
+            -- sum(if(utm_source regexp ".pp" or utm_source regexp 
".paypal",1,0)) as pp,
+            concat(round(sum(if(trxn_id like "GLOBALCOLLECT%%" or trxn_id like 
"PAYFLOW%%" or trxn_id like "ADYEN%%" or trxn_id like "WORLDPAY%%",1,0)) / 
sum(if(utm_source like "%.cc",1,0))*100),"%") as cccnv,
+            concat(round(sum(if(trxn_id like "PAYPAL%%",1,0)) / 
sum(if(utm_source like "%.pp" or utm_source like "%.paypal",1,0))*100),"%") as 
ppcnv,
+            concat(round(sum(if(trxn_id like "AMAZON%%",1,0)) / 
sum(if(utm_source like "%.amazon" ,1,0))*100),"%") as azcnv,
+            sum(total_amount) AS amount,
+            sum(if(total_amount > 3, 3, total_amount)) AS amount3,
+            sum(if(total_amount > 5, 5, total_amount)) AS amount5,
+            sum(if(total_amount > 20, 20, total_amount)) AS amount20,
+            sum(if(total_amount > 50, 50, total_amount)) AS amount50,
+            max(total_amount) as max,
+            avg(total_amount) as avg,
+            avg(if(total_amount > 20, 20, total_amount)) as avg20,
+            std(total_amount) as stdev
+            '''
+# join
+    query += '''
+            from
+            drupal.contribution_tracking ct
+            left join civicrm.civicrm_contribution cc on ct.contribution_id = 
cc.id
+            left join civicrm.civicrm_address ca on cc.contact_id = 
ca.contact_id
+            left join civicrm.civicrm_country co on ca.country_id = co.id
+            where ts >=  '%s' and ts < '%s'
+            and (ca.is_primary=1 OR ca.is_primary IS NULL) -- don't double 
count extra addresses
+            ''' % ( start, end )
+# where
+    if optdict['campaign'] != 'None':
+        if optdict['campaign'] and optdict['multicountry']:
+            query += "and utm_campaign regexp '%s' " % campaign
+        elif optdict['campaign']:
+            query += "and utm_campaign = '%s' " % campaign
+    if optdict['country']:
+        query += " and ct.country='%s' " % (optdict['country'])
+    if optdict['language']:
+        query += "and ct.language = '%s' " % (optdict['language'])
+    if optdict['medium']:
+        query += " and utm_medium = '%s' " % optdict['medium']
+    if optdict['substring']:
+        query += " and utm_source  regexp '%s' " % (optdict['substring'])
+
+# group by
+    if optdict['group_by']:
+        query += get_group_by(optdict['group_by'],optdict)
+# having
+    if optdict['having']:
+        query += " having donations > %s " % optdict['having']
+# order by
+    if optdict['order_by']:
+        query += " order by "
+        for c in optdict['order_by']:
+            if c == 'l': query += "landingpage, "
+            if c == 'b': query += "src_banner, "
+            if c == 'p': query += "src_payment_method, "
+            if c == 'c': query += "country, "
+            if c == 'd': query += "donations desc, "
+            if c == 'i': query += "lpi desc, "
+            if c == 'g': query += "language, "
+            if c == 'f': query += "payments_form, "
+            if c == 't': query += "campaign, "
+        query = query[0:-2]
+
+# end of wrapper for larger impressions query
+    if type == "donations-impressions":
+        query += ") ecom "
+        if optdict['group_by']:
+            if 'l' in optdict['group_by'] and 'b' in optdict['group_by']:
+                query += "on ecom.landingpage=lps.landingpage and 
ecom.banner=lps.banner "
+            elif 'l' in optdict['group_by']: 
+                query += "on ecom.landingpage=lps.landingpage "
+            elif 'b' in optdict['group_by']: 
+                query += "on ecom.banner=lps.banner "
+        elif optdict['campaign'] == 'None':
+            query += "on True "
+        query += "order by lpi desc"
+
+    return query
+
+def donations(optdict):
+    query = get_test_query(optdict,"donations-impressions" if 
optdict['impressions'] else "donations")
+# output sql if requested
+    if optdict['sqlonly']:
+        print query
+# output results
+    rs_string, rs = get_rs(query, optdict['raw'])
+    print rs_string
+# link(s) to thumbtack tool
+    do_confidence(optdict,rs)
+# ban imps
+    if optdict['impressions']:
+        print_ban_imps(optdict)
+# u.test
+    if len(rs) != 2:
+        print "\nu test only possible with 2 banners -- use --having to narrow 
it down."
+    else:
+        bans = []
+        bans.append( rs[0]['src_banner'] )
+        bans.append( rs[1]['src_banner'] )
+        utest(optdict,bans)
+def do_confidence(optdict,rs):
+    if optdict['group_by']:
+        if optdict['group_by'][0]=='l':
+            print "d/clicks confidence test:"
+            tt_query_string =  "&".join(
+                    [ ( "%s=%s%%2C%s" % ( row['landingpage'] ,
+                        row['donations'], row['clicks']) ) for row in rs ] )
+            tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                    "&abba%%3AintervalConfidenceLevel=0.95&"
+                    "abba%%3AuseMultipleTestCorrection=true" % tt_query_string 
)
+            print tt_url
+            print "d/bi confidence test:"
+            tt_query_string =  "&".join(
+                    [ ( "%s=%s%%2C%s" % ( row['landingpage'] ,
+                        row['donations'], 1000000) ) for row in rs ] )
+            tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                    "&abba%%3AintervalConfidenceLevel=0.95&"
+                    "abba%%3AuseMultipleTestCorrection=true" % tt_query_string 
)
+            print tt_url
+            if optdict['impressions']:
+                print "d/lpi confidence test:"
+                tt_query_string =  "&".join(
+                        [ ( "%s=%s%%2C%s" % ( row['landingpage'] ,
+                            row['donations'], row['lpi']) ) for row in rs ] )
+                tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                        "&abba%%3AintervalConfidenceLevel=0.95&"
+                        "abba%%3AuseMultipleTestCorrection=true" % 
tt_query_string )
+                print tt_url
+                print "lpi/bi confidence test:"
+                tt_query_string =  "&".join(
+                        [ ( "%s=%s%%2C%s" % ( row['landingpage'] ,
+                            row['lpi'], 1000000) ) for row in rs ] )
+                tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                        "&abba%%3AintervalConfidenceLevel=0.95&"
+                        "abba%%3AuseMultipleTestCorrection=true" % 
tt_query_string )
+                print tt_url
+
+        elif optdict['group_by'][0]=='b':
+            print "d/bi confidence test:"
+            tt_query_string =  "&".join(
+                    [ ( "%s=%s%%2C%s" % ( row['src_banner'] ,
+                        row['donations'], 1000000) ) for row in rs ] )
+            tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                    "&abba%%3AintervalConfidenceLevel=0.95&"
+                    "abba%%3AuseMultipleTestCorrection=true" % tt_query_string 
)
+            print tt_url
+            print "clicks/bi confidence test:"
+            tt_query_string =  "&".join(
+                    [ ( "%s=%s%%2C%s" % ( row['src_banner'] ,
+                        row['clicks'], 1000000) ) for row in rs ] )
+            tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                    "&abba%%3AintervalConfidenceLevel=0.95&"
+                    "abba%%3AuseMultipleTestCorrection=true" % tt_query_string 
)
+            print tt_url
+            if optdict['impressions']:
+                print "d/lpi confidence test:"
+                tt_query_string =  "&".join(
+                        [ ( "%s=%s%%2C%s" % ( row['src_banner'] ,
+                            row['donations'], row['lpi']) ) for row in rs ] )
+                tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                        "&abba%%3AintervalConfidenceLevel=0.95&"
+                        "abba%%3AuseMultipleTestCorrection=true" % 
tt_query_string )
+                print tt_url
+                print "lpi/bi confidence test:"
+                tt_query_string =  "&".join(
+                        [ ( "%s=%s%%2C%s" % ( row['src_banner'] ,
+                            row['lpi'], 1000000) ) for row in rs ] )
+                tt_url = ( "http://www.thumbtack.com/labs/abba/#%s";
+                        "&abba%%3AintervalConfidenceLevel=0.95&"
+                        "abba%%3AuseMultipleTestCorrection=true" % 
tt_query_string )
+                print tt_url
+        else:
+            print "Confidence tests only working now with banner or landing 
page grouping."
+
+def get_options():
+    from optparse import OptionParser
+    parser = OptionParser()
+    parser.add_option("-d", "--display_cols", dest="display_cols",
+                    help="Fields to display. b=ban, l=lp, p=pay method, 
c=country, g=language, t=campaign (test)", metavar="blp")
+    parser.add_option("--having", dest="having",
+                    help="Only groups having more than n donations", 
metavar="10")
+    parser.add_option("-g", "--group_by", dest="group_by",
+                    help="Fields to group by. b=ban, l=lp, p=pay method, 
c=country, g=language, t=campaign (test)", metavar="blp")
+    parser.add_option("-o", "--order_by", dest="order_by",
+                    help="Fields to order by. d=donations, i=lp impressions, 
b=ban, l=lp, p=pay method, c=country, g=language, t=campaign (test)", 
metavar="blp")
+    parser.add_option("-c", "--campaign", dest="campaign", default = "None",
+                    help="utm_campaign, aka campaign", metavar="UTM_CAMPAIGN")
+    parser.add_option("-s", "--start", dest="start",
+                    help="test start time", metavar="YYYYMMDDHHMMSS")
+    parser.add_option("-e", "--end", dest="end",
+                    help="test end time", metavar="YYYYMMDDHHMMSS")
+    parser.add_option("--bi", "--ban impressions", dest="ban_imps", 
action="store_true", default=False, 
+                    help="Show banner impressions too (only accurate about 15 
minutes after test)" )
+    parser.add_option("-i", "--impressions", dest="impressions", 
action="store_true", default=False, 
+                    help="Show impressions too (only accurate about 10 minutes 
after test)" )
+    #parser.add_option("--gc", "--gcountry", dest="gcountry", 
action="store_true", default=False, 
+    #                help="Group results by country" )
+    parser.add_option("--sqlonly",
+                    action="store_true", dest="sqlonly", default=False,
+                    help="Print sql, not results.")
+    parser.add_option("--raw",
+                    action="store_true", dest="raw",
+                    help="Display raw output (may be easier to paste to 
spreadsheet.")
+    parser.add_option("--substring", dest="substring", 
+                    help="substring", metavar="")
+    parser.add_option("--left", dest="left", 
+                    help="left", metavar="")
+    parser.add_option("--language", dest="language", 
+                    help="language", metavar="en")
+    parser.add_option("--country", dest="country", 
+                    help="country", metavar="US")
+    parser.add_option("--medium", dest="medium", 
+                    help="medium", metavar="sitenotice")
+
+    #parser.add_option("-b", "--gbanner",
+    #                action="store_true", dest="gbanner", default=False,
+    #                help="Group by banner.")
+    #parser.add_option("-l", "--glandingpage",
+    #                action="store_true", dest="glandingpage", default=False,
+    #                help="Group by landing page.")
+    #parser.add_option("-p", "--gpayment_method",
+    #                action="store_true", dest="gpayment_method", 
default=False,
+    #                help="Group by payment method.")
+    parser.add_option("-m", "--multicountry",
+                    action="store_true", dest="multicountry", default=False,
+                    help="Remove country suffix of banner names to group by 
banner instead of banner+country.")
+
+    if len(sys.argv) == 1:
+        # No arguments, show instructions
+        parser.print_help()
+        sys.exit(1)
+
+    (options, args) = parser.parse_args()
+
+    if options.campaign and (not options.start or not options.end):
+        with open('campaign_times.tsv') as f:
+            for line in f.readlines():
+                c, s, e = line.split()
+                if c == options.campaign:
+                    if not options.start: options.start = s
+                    if not options.end: options.end = e
+    optdict = vars(options)
+    if  options.impressions:
+        if options.group_by:
+            if 'f' in options.group_by:
+                print "Can't group on 'form' while getting impressions."
+                sys.exit()
+    return optdict
+
+def print_ban_imps(optdict):
+    start = optdict['start']
+    end = optdict['end']
+    campaign = optdict['campaign']
+    query ="select "
+    if optdict['group_by']:
+        for c in optdict['group_by']:
+            if c == 'b': query += "banner, "
+#            if c == 'l': query += "landing_page, "
+            if c == 'c': query += "country, "
+    query += '''sum(counts)*100 as ban_imps
+    from faulkner.banner_impressions_2012
+    where on_minute > '%s' and on_minute < '%s' 
+    ''' % (start, end)
+    if optdict['campaign'] != 'None':
+        if optdict['campaign']:
+            query += "and campaign = '%s' " % campaign
+    if optdict['group_by'] and optdict['group_by'] != 'l':
+        query += "group by "
+        for c in optdict['group_by']:
+            if c == 'b': query += "banner, "
+#            if c == 'l': query += "landing_page, "
+            if c == 'c': query += "country, "
+        query = query.rstrip(', ')
+    rs_string, rs = get_rs(query)
+    print rs_string
+
+def get_rs(query, raw = None):
+    from prettytable import PrettyTable
+    cursor = get_cursor("dict")
+    cursor.execute(query)
+    rs = cursor.fetchall()
+    rs_string = ""
+    if not raw:
+        x = PrettyTable([i[0] for i in cursor.description])
+        for row in rs:
+            x.add_row([row[i[0]] for i in cursor.description])
+        rs_string = x
+    else:
+        headers = [i[0] for i in cursor.description]
+        for h in headers:
+            rs_string += "%s\t" % h
+        rs_string += "\n" 
+        for row in rs:
+            the_row = [row[i[0]] for i in cursor.description]
+            for col in the_row:
+                rs_string += "%s\t" % col
+            rs_string += "\n"
+    return rs_string, rs
diff --git a/fr_util.py b/fr_util.py
new file mode 100644
index 0000000..772d8b5
--- /dev/null
+++ b/fr_util.py
@@ -0,0 +1,59 @@
+#! /usr/bin/env python
+"""Fundraising utility belt
+"""
+
+import MySQLdb
+
+FORMAT_CHOICES = ['tsv', 'csv', 'pretty', 'mediawiki']
+
+def get_db_cursor(type='default'):
+    """Get a cursor to the database
+
+    Type can be:
+    - 'default'
+    - 'dict': returns rows as a dictionary
+
+    User must have .my.cnf file configured in their home directory
+    """
+    db = MySQLdb.connect(host='localhost',
+                        unix_socket='/tmp/mysql.sock',
+                        read_default_file="~/.my.cnf")
+    if type == 'dict':
+        cursor = db.cursor(MySQLdb.cursors.DictCursor)
+    else:
+        cursor = db.cursor()
+
+    return cursor
+
+
+def print_table(columns, rows, format):
+    # Put results in a nice table for output
+
+    if format == 'tsv':
+        print '\t'.join(columns)
+        for row in rows:
+            print '\t'.join(row)
+
+    elif format == 'csv':
+        print ', '.join(columns)
+        for row in rows:
+            print ', '.join(row)
+
+    elif format == 'pretty':
+        from prettytable import PrettyTable
+        x = PrettyTable(columns)
+        x.align = 'r'
+        for row in rows:
+            x.add_row(row)
+        print x
+
+    elif format == 'mediawiki':
+        print '{| class="wikitable sortable"'
+        print '|+ Caption'
+        print '|-'
+        print '! ' + ' !! '.join( columns )
+        for row in rows:
+            print '|-'
+            print '! scope="row" | ' + row[0]
+            print '| ' + ' || '.join( row[1:] )
+        print '|}'
\ No newline at end of file
diff --git a/statler b/statler
new file mode 100755
index 0000000..65dd436
--- /dev/null
+++ b/statler
@@ -0,0 +1,231 @@
+#! /usr/bin/env python
+
+from datetime import datetime
+import dateutil.parser
+import dateutil.relativedelta
+import MySQLdb, sys
+import fr_util
+
+TSFORMAT = '%Y%m%d%H%M%S'
+DATEFORMAT = '%Y-%m-%d %H:%M:%S'
+
+class color:
+   PURPLE    = '\033[95m'
+   CYAN      = '\033[96m'
+   DARKCYAN  = '\033[36m'
+   BLUE      = '\033[94m'
+   GREEN     = '\033[92m'
+   YELLOW    = '\033[93m'
+   RED       = '\033[91m'
+   DARKGRAY  = '\033[90m'
+   BOLD      = '\033[1m'
+   UNDERLINE = '\033[4m'
+   END       = '\033[0m'
+
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(
+        description="Get results for a simple test."
+        )
+
+    parser.add_argument('substring', nargs='?', default='.*',
+        help='Banner substring e.g. B1516_0902_en6C_ipd. You can even use a 
regular expression if you like.')
+
+    # We'll run the date parser on these later
+    parser.add_argument('-s', '--start', dest='start',
+        help='Start time (UTC). If not specified, defaults to one month ago.')
+    parser.add_argument('-e', '--end', dest='end',
+        help='End time (UTC). If not specified, defaults to now.')
+
+    parser.add_argument('--campaign', dest='campaign',
+        help='Filter results to specific campaign')
+
+    parser.add_argument('--language', dest='language',
+        help='Filter results to specific language (note that not all 
impressions have an associated language)')
+    parser.add_argument('--country', dest='country',
+        help='Filter results to specific country code')
+
+    parser.add_argument('--combine', action='store_true',
+        help='Combine results for banners where last part of name is 
identical')
+
+    parser.add_argument('--format', dest='format', default='tsv',
+        choices=fr_util.FORMAT_CHOICES,
+        help='Output format. For example "mediawiki" gives a table suitable 
for posting on wiki.')
+
+    parser.add_argument('--sql', action='store_true',
+        help='Show SQL used (for debugging)')
+
+    if len(sys.argv) == 1:
+        # No arguments, show instructions
+        parser.print_help()
+        sys.exit(1)
+
+    args = vars(parser.parse_args())
+    return args
+
+
+def build_query():
+    if args['combine']:
+        # take the last part
+        banner_select_dons = "SUBSTRING_INDEX( SUBSTRING_INDEX(ct.utm_source, 
'.', 1), '_', -1 )"
+        banner_select_imps = "SUBSTRING_INDEX( bi.banner, '_', -1 )"
+    else:
+        banner_select_dons = "SUBSTRING_INDEX( ct.utm_source, '.', 1 )"
+        banner_select_imps = "bi.banner"
+
+    query = """SELECT
+        dons.b AS banner,
+        donations,
+        impressions,
+        CAST(donations/impressions AS DOUBLE) AS "dons/i",
+        amount,
+        CAST(amount/impressions AS DOUBLE) AS "amount/i",
+        clicks,
+        CAST(clicks/impressions AS DOUBLE) AS "clicks/i",
+        CAST(donations/clicks AS DOUBLE) AS "dons/clicks",
+        amount50,
+        CAST(amount50/impressions AS DOUBLE) AS "amount50/i",
+        max,
+        CAST(amount/donations AS DOUBLE) AS avg,
+        CAST(amount50/donations AS DOUBLE) AS avg50
+    FROM (
+        SELECT """ + banner_select_dons + """ AS b,
+            SUM(not isnull(cc.id)) AS donations,
+            COUNT(ct.id) AS clicks,
+            SUM(cc.total_amount) AS amount,
+            SUM(if(cc.total_amount > 50, 50, cc.total_amount)) AS amount50,
+            MAX(cc.total_amount) AS max
+        FROM drupal.contribution_tracking ct
+            LEFT JOIN civicrm.civicrm_contribution cc ON ct.contribution_id = 
cc.id
+        WHERE ts BETWEEN %(start_ts)s AND %(end_ts)s
+            AND ct.utm_source REGEXP %(substring)s"""
+
+    if args['campaign']:
+        query += "AND utm_campaign=%(campaign)s"
+    if args['country']:
+        query += "AND ct.country=%(country)s"
+    if args['language']:
+        query += "AND ct.language=%(language)s"
+
+    query += """
+        GROUP BY
+            b
+        ) dons
+    LEFT JOIN (
+        SELECT
+            """ + banner_select_imps + """ AS b,
+            sum(count) AS impressions
+        FROM pgehres.bannerimpressions bi
+            LEFT JOIN pgehres.country ON (country_id=country.id)
+            LEFT JOIN pgehres.language ON (language_id=language.id)
+        WHERE timestamp BETWEEN %(start_ts)s AND %(end_ts)s
+            AND banner REGEXP %(substring)s"""
+
+    if args['campaign']:
+        query += "AND campaign=%(campaign)s"
+    if args['country']:
+        query += "AND country.iso_code=%(country)s"
+    if args['language']:
+        query += "AND language.iso_code=%(language)s"
+
+    query += """
+        GROUP BY b ) imps
+    ON dons.b = imps.b
+    ORDER BY (banner REGEXP 'cnt$|ctrl$|cntrl$|control$') DESC, banner -- put 
control first
+    """
+
+    return query
+
+def get_data():
+    # Gets the raw results from the database
+    cursor.execute(query, args)
+    data = cursor.fetchall()
+    # print data
+    return data
+
+def format_results(data):
+    # Format results nicely and bundle into lists.
+    num = '{:,}' # number with thousands separator
+    usd = '${:,.2f}'
+    columns = ['banner', 'donations', 'imps', 'dons/i',
+                'amount', 'amount/i', 'clicks', 'clicks/i',
+                'dons/clicks', 'amount50', 'amount50/i',
+                'max', 'avg', 'avg50']
+    results = []
+    for i in data:
+
+        # For some reason it keeps returning None, change that to 0 so 
formatter doesn't choke
+        for x in i:
+            if i[x] == None:
+                i[x] = 0
+
+        row = [
+            str( i['banner'] ),
+            num.format( i['donations'] ),
+            num.format( i['impressions'] ),
+            '{:.8f}'.format( i['dons/i'] ),
+            usd.format( i['amount'] ),
+            '{:.6f}'.format( i['amount/i'] ),
+            num.format( i['clicks'] ),
+            '{:.6f}'.format( i['clicks/i'] ),
+            '{:.2%}'.format( i['dons/clicks'] ),
+            usd.format( i['amount50'] ),
+            '{:.6f}'.format( i['amount50/i'] ),
+            usd.format( i['max'] ),
+            usd.format( i['avg'] ),
+            usd.format( i['avg50'] )
+        ]
+        results.append(row)
+    return columns, results
+
+
+def get_thumbtack_link(data):
+    link = "https://www.thumbtack.com/labs/abba/#";
+    for row in data:
+        link += "{banner}={donations}%2C{impressions}&".format(**row)
+    link += 
"abba%3AintervalConfidenceLevel=0.95&abba%3AuseMultipleTestCorrection=true"
+    return link
+
+
+if __name__ == "__main__":
+    args = get_options()
+
+    if args['start']:
+        args['start'] = dateutil.parser.parse(args['start'])
+    else:
+        # default to 1 month ago
+        args['start'] = (datetime.now() + 
dateutil.relativedelta.relativedelta(months = -1))
+        print "You didn't specify a start time, defaulting to 1 month ago. The 
query will run faster if you can specify a start time."
+
+    if args['end']:
+        args['end'] = dateutil.parser.parse(args['end'])
+    else:
+        # default to now
+        args['end'] = datetime.now()
+
+    # Timestamp format for queries
+    args['start_ts'] = args['start'].strftime(TSFORMAT)
+    args['end_ts']   = args['end'].strftime(TSFORMAT)
+
+    cursor = fr_util.get_db_cursor(type='dict')
+    query  = build_query()
+    data   = get_data()
+    if args['sql']:
+        print cursor._last_executed
+
+    if args['country']:
+        print "Country: " + args['country']
+    if args['language']:
+        print "Language: " + args['language']
+
+    print "N.B. Banner impressions subject to at least 15 minutes delay"
+    print "From: " + args['start'].strftime(DATEFORMAT)
+    print "To: "   + args['end'].strftime(DATEFORMAT)
+    print
+
+    columns, results = format_results(data)
+
+    fr_util.print_table(columns, results, args['format'])
+    print color.UNDERLINE + get_thumbtack_link(data) + color.END
+    print
diff --git a/stats_abba.py b/stats_abba.py
new file mode 100644
index 0000000..2d63b84
--- /dev/null
+++ b/stats_abba.py
@@ -0,0 +1,310 @@
+# From: http://github.com/thumbtack/abba/python/abba/stats.py, commit 0a7fcda0
+# Copyright (c) 2012 Thumbtack, Inc.
+
+import collections
+import math
+
+from scipy import stats
+
+def get_z_critical_value(alpha, two_tailed=True):
+    """
+    Returns the z critical value for a particular alpha = 1 - confidence 
level.  By default returns
+    a two-tailed z-value, meaning the actual tail probability is alpha / 2.
+    """
+    if two_tailed:
+        alpha /= 2
+    return stats.distributions.norm.ppf(1 - alpha)
+
+# a value with confidence interval bounds (not necessarily centered around the 
point estimate)
+ValueWithInterval = collections.namedtuple(
+    'ValueWithInterval',
+    ('value', 'lower_bound', 'upper_bound'),
+)
+
+class ValueWithError(object):
+    """
+    A value with standard error, from which a confidence interval can be 
derived.
+    """
+    def __init__(self, value, error):
+        self.value = value
+        self.error = error
+
+    def confidence_interval_width(self, z_critical_value):
+        """
+        z_critical_value should be the value at which the right-tail 
probability for a standard
+        normal distribution equals half the desired alpha = 1 - confidence 
level:
+
+        P(Z > z_value) = 1 - alpha / 2
+
+        where Z is an N(0, 1) random variable.  Use get_z_critical_value(), or 
see
+        http://en.wikipedia.org/wiki/Standard_normal_table.
+        """
+        return z_critical_value * self.error
+
+    def value_with_interval(self, z_critical_value, estimated_value=None):
+        width = self.confidence_interval_width(z_critical_value)
+        return ValueWithInterval(
+            value=estimated_value if estimated_value is not None else 
self.value,
+            lower_bound=self.value - width,
+            upper_bound=self.value + width,
+        )
+
+class BinomialDistribution(object):
+    def __init__(self, num_trials, probability):
+        self.num_trials = num_trials
+        self.probability = probability
+        self.expectation = num_trials * probability
+        self.standard_deviation = math.sqrt(self.expectation * (1 - 
probability))
+        self._binomial = stats.binom(num_trials, probability)
+
+    def mass(self, count):
+        return self._binomial.pmf(count)
+
+    def cdf(self, count):
+        return self._binomial.cdf(count)
+
+    def survival(self, count):
+        return 1 - self.cdf(count)
+
+    def inverse_cdf(self, probability):
+        return self._binomial.ppf(probability)
+
+    def inverse_survival(self, probability):
+        return self._binomial.isf(probability)
+
+class Proportion(object):
+    def __init__(self, num_successes, num_trials):
+        """
+        Represents a binomial proportion with num_successes successful samples 
out of num_trials
+        total.
+        """
+        self.num_successes = num_successes
+        self.num_trials = num_trials
+
+    def p_estimate(self, z_critical_value=0):
+        """
+        Generate an adjusted estimate and error using the "Agresti-Coull 
Interval", see
+        
http://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Agresti-Coull_Interval.
+
+        The estimated value is an adjusted best estimate for the actual 
probability. For example, if
+        0 successes were observed out of 10 samples, it's unlikely the actual 
probability is zero,
+        so the adjusted estimate will be slightly above zero.
+
+        A z_critical_value of zero yields the ordinary Wald interval.
+        """
+        adjusted_num_trials = float(self.num_trials + z_critical_value**2)
+        interval_center = (self.num_successes + z_critical_value**2 / 2) / 
adjusted_num_trials
+        standard_error = math.sqrt(interval_center * (1 - interval_center) / 
adjusted_num_trials)
+        return ValueWithError(interval_center, standard_error)
+
+    def mixed_estimate(self, z_critical_value):
+        """
+        Returns an ValueWithInterval with a MLE value and upper/lower bounds 
from the Agresti-Coull
+        interval.
+        """
+        return (
+            self.p_estimate(z_critical_value=z_critical_value)
+            .value_with_interval(z_critical_value, 
estimated_value=self.p_estimate().value)
+        )
+
+def confidence_interval_on_proportion(num_successes, num_trials, 
confidence_level=0.95):
+    '''Convenience function with more straightforward interface.'''
+    return Proportion(num_successes, num_trials).mixed_estimate(
+        get_z_critical_value(1 - confidence_level)
+    )
+
+class ProportionComparison(object):
+    def __init__(self, baseline, variation):
+        self.baseline = baseline
+        self.variation = variation
+
+    def difference_estimate(self, z_critical_value):
+        """
+        Generate an estimate of the difference in success rates between the 
variation and the
+        baseline.
+        """
+        baseline_p = 
self.baseline.p_estimate(z_critical_value=z_critical_value)
+        variation_p = 
self.variation.p_estimate(z_critical_value=z_critical_value)
+        difference = variation_p.value - baseline_p.value
+        standard_error = math.sqrt(baseline_p.error ** 2 + variation_p.error 
** 2)
+        return ValueWithError(difference, standard_error)
+
+    def difference_ratio(self, z_critical_value):
+        """
+        Return the difference in sucess rates as a proportion of the baseline 
success rate.
+        """
+        baseline_value = 
self.baseline.p_estimate(z_critical_value=z_critical_value).value
+        difference = 
self.difference_estimate(z_critical_value=z_critical_value)
+        ratio = difference.value / baseline_value
+        error = difference.error / baseline_value
+        return ValueWithError(ratio, error)
+
+    def z_test(self, z_multiplier=1):
+        """
+        Perform a large-sample z-test of null hypothesis H0: p_baseline == 
p_variation against
+        alternative hypothesis H1: p_baseline < p_variation.  Return the 
(one-tailed) p-value.
+
+        z_multiplier: test z-value will be multiplied by this factor before 
computing a p-value.
+
+        See 
http://en.wikipedia.org/wiki/Statistical_hypothesis_testing#Common_test_statistics,
+        "Two-proportion z-test, pooled for d0 = 0".
+        """
+        pooled_stats = Proportion(
+            self.baseline.num_successes + self.variation.num_successes,
+            self.baseline.num_trials + self.variation.num_trials,
+        )
+        pooled_p_value = pooled_stats.p_estimate().value
+        pooled_variance_of_difference = (
+            pooled_p_value * (1 - pooled_p_value)
+            * (1.0 / self.baseline.num_trials + 1.0 / 
self.variation.num_trials)
+        )
+        pooled_standard_error_of_difference = 
math.sqrt(pooled_variance_of_difference)
+        test_z_value = self.difference_estimate(0).value / 
pooled_standard_error_of_difference
+        adjusted_p_value = stats.distributions.norm.sf(test_z_value * 
z_multiplier)
+        return adjusted_p_value
+
+    def _binomial_coverage_interval(self, distribution, coverage_alpha):
+        """
+        For the given binomial distribution, compute an interval that covers 
at least (1 -
+        coverage_alpha) of the total probability mass, centered at the 
expectation (unless we're at
+        the boundary). Uses the normal approximation.
+        """
+        if distribution.num_trials < 1000:
+            # don't even bother trying to optimize for small-ish sample sizes
+            return (0, distribution.num_trials)
+        else:
+            return (
+                int(math.floor(distribution.inverse_cdf(coverage_alpha / 2))),
+                int(math.ceil(distribution.inverse_survival(coverage_alpha / 
2))),
+            )
+
+    def _probability_union(self, probability, num_tests):
+        """
+        Given the probability of an event, compute the probability that it 
happens at least once in
+        num_tests independent tests. This is used to adjust a p-value for 
multiple comparisons.
+        When used to adjust alpha instead, this is called a Sidak correction 
(the logic is the same,
+        the formula is inverted):
+        
http://en.wikipedia.org/wiki/Bonferroni_correction#.C5.A0id.C3.A1k_correction
+        """
+        return 1 - (1 - probability)**num_tests
+
+    def iterated_test(self, num_tests, coverage_alpha, improvement_only=False):
+        """
+        Compute a p-value testing null hypothesis H0: p_baseline == 
p_variation against alternative
+        hypothesis H1: p_baseline != p_variation by summing p-values 
conditioned on individual
+        baseline success counts. This provides a more accurate correction for 
multiple testing but
+        scales like O(sqrt(self.baseline.num_trials)), so can eventually get 
slow for very large
+        values.
+
+        Lower coverage_alpha increases accuracy at the cost of longer runtime. 
Roughly, the result
+        will be accurate within no more than coverage_alpha (but this ignores 
error due to the
+        normal approximation so isn't guaranteed).
+
+        If improvement_only=True, computes p-value for alternative hypothesis
+        H1: p_baseline < p_variation instead.
+        """
+        observed_delta = self.variation.p_estimate().value - 
self.baseline.p_estimate().value
+        if observed_delta == 0 and not improvement_only:
+            # a trivial case that the code below does not handle well
+            return 1
+
+        pooled_proportion = (
+            (self.baseline.num_successes + self.variation.num_successes)
+            / float(self.baseline.num_trials + self.variation.num_trials)
+        )
+        variation_distribution = 
BinomialDistribution(self.variation.num_trials, pooled_proportion)
+        baseline_distribution = BinomialDistribution(self.baseline.num_trials, 
pooled_proportion)
+
+        baseline_limits = 
self._binomial_coverage_interval(baseline_distribution, coverage_alpha)
+        p_value = 0
+        for baseline_successes in xrange(baseline_limits[0], 
baseline_limits[1] + 1):
+            baseline_proportion = 1.0 * baseline_successes / 
self.baseline.num_trials
+            if improvement_only:
+                lower_trial_count = -1
+                upper_trial_count = math.ceil(
+                    (baseline_proportion + observed_delta) * 
self.variation.num_trials
+                )
+            else:
+                observed_absolute_delta = abs(observed_delta)
+                lower_trial_count = math.floor(
+                    (baseline_proportion - observed_absolute_delta) * 
self.variation.num_trials
+                )
+                upper_trial_count = math.ceil(
+                    (baseline_proportion + observed_absolute_delta) * 
self.variation.num_trials
+                )
+
+            # p-value of variation success counts "at least as extreme" for 
this particular
+            # baseline success count
+            p_value_at_baseline = (
+                variation_distribution.cdf(lower_trial_count)
+                + variation_distribution.survival(upper_trial_count - 1)
+            )
+
+            # this is exact because we're conditioning on the baseline count, 
so the multiple
+            # tests are independent.
+            adjusted_p_value = self._probability_union(p_value_at_baseline, 
num_tests)
+
+            baseline_probability = 
baseline_distribution.mass(baseline_successes)
+            p_value += baseline_probability * adjusted_p_value
+
+        # the remaining baseline values we didn't cover contribute less than 
coverage_alpha to the
+        # sum, so adding that amount gives us a conservative upper bound.
+        return p_value + coverage_alpha
+
+Results = collections.namedtuple(
+    'Results',
+    (
+        'num_successes',
+        'num_trials',
+        'proportion', # ValueWithInterval
+        'improvement', # ValueWithInterval
+        'relative_improvement', # ValueWithInterval
+        'two_tailed_p_value', # two-tailed p-value for trial != baseline
+        'improvement_one_tailed_p_value', # one-tailed p-value for trial > 
baseline
+    ),
+)
+
+class Experiment(object):
+    P_VALUE_PRECISION = 1e-5
+
+    def __init__(self, num_trials, baseline_num_successes, baseline_num_trials,
+                 confidence_level=0.95):
+        """
+        num_trials: number of trials to be compared to the baseline
+        confidence_level: used for all confidence intervals generated
+        """
+        self.num_comparisons = max(1, num_trials)
+        self._baseline = Proportion(baseline_num_successes, 
baseline_num_trials)
+        alpha = (1 - confidence_level) / num_trials # Bonferroni correction
+        self._z_critical_value = get_z_critical_value(alpha)
+
+    def get_baseline_proportion(self):
+        return self._baseline.mixed_estimate(self._z_critical_value)
+
+    def get_results(self, num_successes, num_trials):
+        trial = Proportion(num_successes, num_trials)
+        comparison = ProportionComparison(self._baseline, trial)
+        return Results(
+            num_successes=num_successes,
+            num_trials=num_trials,
+            proportion=trial.mixed_estimate(self._z_critical_value),
+            improvement=comparison.difference_estimate(self._z_critical_value)
+                .value_with_interval(
+                    self._z_critical_value,
+                    estimated_value=comparison.difference_estimate(0).value,
+                ),
+            
relative_improvement=comparison.difference_ratio(self._z_critical_value)
+                .value_with_interval(
+                    self._z_critical_value,
+                    estimated_value=comparison.difference_ratio(0).value,
+                ),
+            two_tailed_p_value=comparison.iterated_test(
+                self.num_comparisons,
+                self.P_VALUE_PRECISION,
+            ),
+            improvement_one_tailed_p_value=comparison.iterated_test(
+                self.num_comparisons,
+                self.P_VALUE_PRECISION,
+                improvement_only=True,
+            ),
+        )
diff --git a/timeline.py b/timeline.py
new file mode 100755
index 0000000..1e1d86a
--- /dev/null
+++ b/timeline.py
@@ -0,0 +1,124 @@
+#! /usr/bin/env python
+
+from datetime import datetime, timedelta
+import dateutil.parser
+import MySQLdb, sys
+import fr_util
+
+displayFormat = "%Y-%m-%d-%H:%M"
+tsFormat      = "%Y%m%d%H%M%S"
+
+def main():
+    print """
+Campaign filter:   {campaign}
+utm_source filter: {source}
+Country filter:    {country}
+Language filter:   {language}
+    """.format(**args)
+    num = '{:,}'
+    print 'start\tend\timpressions\tclicks\tdonations\tconversion\tamount'
+    d = start
+    while d <= end:
+        d2 = d + delta
+        impressions = get_impressions(d, d2)
+        clicks, donations, conversion, amount = get_contribs(d, d2)
+
+        print '\t'.join(
+            [d.strftime(displayFormat), 
+            d2.strftime(displayFormat), 
+            num.format(impressions or 0), 
+            num.format(clicks or 0), 
+            num.format(donations or 0),
+            '{:.2%}'.format(conversion or 0),
+            '${:,.2f}'.format(amount or 0)])
+        d += delta
+        
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(description="""Get impression and donation info 
over time for a campaign.
+        Note that old impression queries can be slow, so don't do too many at 
once.""")
+
+    parser.add_argument('-s', '--start', dest='start', required=True,
+        help='Start time (UTC)')
+    parser.add_argument('-e', '--end', dest='end', 
default=datetime.utcnow().strftime('%Y%m%d%H%M%S'), 
+        help='End time (UTC). If not specified, defaults to now.')
+    parser.add_argument('-i', '--interval', dest='interval', default=24,
+        help='Time (in hours) covered per row. Defaults to 24 hours')
+
+    parser.add_argument('--campaign', dest='campaign',   
+        help='Regexp filter by campaign name')
+    parser.add_argument('--source', dest='source',
+        help='Regexp filter by utm_source / banner name')
+
+    parser.add_argument('--country',  dest='country', 
+        help='Filter by country code e.g. GB')
+    parser.add_argument('--language', dest='language', 
+        help='Filter by language code e.g. en')
+
+    if len(sys.argv) == 1:
+        # No arguments, show instructions
+        parser.print_help()
+        sys.exit(1)
+
+    args = vars(parser.parse_args())
+    return args
+
+def get_impressions(d, d2):
+    query = """SELECT SUM(count) AS ban_imps
+        FROM pgehres.bannerimpressions
+        LEFT JOIN pgehres.country ON (country_id=country.id)
+        LEFT JOIN pgehres.language ON (language_id=language.id)
+        WHERE timestamp BETWEEN '{0}' AND 
'{1}'""".format(d.strftime(tsFormat), d2.strftime(tsFormat))
+
+    if args['campaign']:
+        query += "AND campaign REGEXP '{0}'".format(args['campaign'])
+
+    if args['source']:
+        query += "AND banner REGEXP '{0}'".format(args['source'])
+
+    if args['country']:
+        query += "AND country.iso_code='{0}'".format(args['country'])
+
+    if args['language']:
+        query += "AND language.iso_code='{0}'".format(args['language'])
+
+    cursor.execute(query)
+    rows = cursor.fetchall()
+    return rows[0][0]
+
+def get_contribs(d, d2):
+    query = """SELECT 
+        COUNT(ct.id) AS clicks,
+        SUM(not isnull(cc.id)) AS donations,
+        SUM(not isnull(cc.id)) / COUNT(ct.id) AS conversion,
+        SUM(total_amount) AS amount
+        FROM drupal.contribution_tracking ct
+        LEFT JOIN civicrm.civicrm_contribution cc ON ct.contribution_id = cc.id
+        WHERE ts BETWEEN '{0}' AND '{1}'""".format(d.strftime(tsFormat), 
d2.strftime(tsFormat))
+    
+    if args['campaign']:
+        query += "AND utm_campaign REGEXP '{0}'".format(args['campaign'])
+
+    if args['source']:
+        query += "AND utm_source REGEXP '{0}'".format(args['source'])
+
+    if args['country']:
+        query += "AND ct.country='{0}'".format(args['country'])
+
+    if args['language']:
+        query += "AND ct.language='{0}'".format(args['language'])
+
+    cursor.execute(query)
+    rows = cursor.fetchall()
+    return rows[0]
+
+if __name__ == "__main__":
+    args     = get_options()
+
+    start    = dateutil.parser.parse(args['start'])
+    end      = dateutil.parser.parse(args['end'])
+    delta    = timedelta(hours=int(args['interval']))
+
+    cursor   = fr_util.get_db_cursor(type='default')
+    main()
diff --git a/which_impression.py b/which_impression.py
new file mode 100755
index 0000000..3e61886
--- /dev/null
+++ b/which_impression.py
@@ -0,0 +1,81 @@
+#! /usr/bin/env python
+
+import MySQLdb, fr_util, sys
+
+def get_options():
+    from argparse import ArgumentParser
+    parser = ArgumentParser(
+        description="Get results for which impression people clicked/donated 
on."
+        )
+    parser.add_argument('regex',
+        help='Banner regex e.g. B14_0723_mob.')
+    parser.add_argument('--format', dest='format', default='tsv',
+        choices=['tsv', 'csv', 'pretty', 'mediawiki'],
+        help='Output format.')
+
+    if len(sys.argv) == 1:
+        # No arguments, show instructions
+        parser.print_help()
+        sys.exit(1)
+
+    args = vars(parser.parse_args())
+    return args
+
+
+def get_results():
+    query = """SELECT
+                  utm_key,
+                  COUNT(*) AS clicks,
+                  SUM( contribution_id IS NOT NULL ) AS donations -- works 
because true=1, false=0
+                FROM drupal.contribution_tracking
+                WHERE utm_source REGEXP %(regex)s
+                AND ts > '20150701000000'
+                GROUP BY utm_key
+                ORDER BY cast(utm_key as int)
+                """
+    cursor.execute(query, args)
+    rows = cursor.fetchall()
+    return rows
+
+
+def print_results(description, rows, format):
+    if format == 'tsv':
+        print '\t'.join([c[0] for c in description])
+        for row in rows:
+            print '\t'.join([str(cell) for cell in row])
+
+    elif format == 'csv':
+        print ', '.join([c[0] for c in description])
+        for row in rows:
+            print ', '.join([str(cell) for cell in row])
+
+    elif format == 'pretty':
+        from prettytable import PrettyTable
+        headers = [d[0] for d in description]
+        x = PrettyTable(headers)
+        x.align = 'r'
+        for row in rows:
+            x.add_row(row)
+        print x
+
+    elif format == 'mediawiki':
+        print '{| class="wikitable sortable"'
+        print '|+ Caption'
+        print '|-'
+        for c in description:
+            print '! scope="col" | ' + c[0]
+        for row in rows:
+            print '|-'
+            print '! scope="row" | ' + row[0]
+            print '| ' + ' || '.join([ str(cell) for cell in row[1:] ])
+        print '|}'
+
+
+if __name__ == "__main__":
+    args = get_options()
+    cursor = fr_util.get_db_cursor()
+
+    rows = get_results()
+
+    print "Banner regex:\t" + args['regex']
+    print_results(cursor.description, rows, args['format'])

-- 
To view, visit https://gerrit.wikimedia.org/r/324762
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I7dea2c11b602d4b9b18bd3174af469d7423f6b82
Gerrit-PatchSet: 1
Gerrit-Project: wikimedia/fundraising/stats
Gerrit-Branch: master
Gerrit-Owner: Awight <awi...@wikimedia.org>
Gerrit-Reviewer: Awight <awi...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to