""" Nimbly chops the info you're interested in out of Wesabe's XML.

from gleaner import *

txs = read_txactions()
total = 0
for month in [MONTH(m) for m in '3', '4', '5', '6']:
    charlie = filter(AND(TAG('charlie'), month), txs)
    spending = sum(t.total(TAG('charlie')) for t in charlie)
    print '%s: % 8.2f' % (month.name, spending)
    total += spending
print 'Total: ', total

There's a main that allows some basic exploration of an accounts.xml in the
current directory, but gleaner is really intended to be used as a library
to pull together custom reports.

The all caps functions and classes like TAG, MERCHANT and BEFORE are filter
creators.  When called with whatever their specific filter data is, they
return a callable that returns true when passed a Txaction that matches
that filter data and false otherwise.  AND, OR and NOT can be given these
created filters to do the normal sort of boolean thing on Txactions.

URL: http://bungleton.com/
Author: Charlie Groves <charlie.groves\x40gmail\x2ecom>
Date: 2007-06-03

Requires Python 2.5 for xml.etree.

"""
import operator
import time

from optparse import OptionParser
from xml.etree import ElementTree

def read_accounts(filename='accounts.xml'):
    root = ElementTree.parse(filename)
    accts = []
    for acct in root.findall('account'):
        accts.append(Account(acct))
    return accts

def read_txactions(filename='accounts.xml'):
    """Load all transactions from filename into Txaction objects.
    
    Returns a list containing a Txaction for every transaction element in
    filename.  

    """
    root = ElementTree.parse(filename)
    txactions = []
    for acct in root.findall('account'):
        name = acct.find('name').text
        for txaction in acct.findall('txactions/txaction'):
            txactions.append(Txaction(txaction, name)) 
    return txactions


class Account(object):
    def __init__(self, element):
        self.name = element.find('name').text
        balel = element.find('current-balance')
        if balel:
            self.balance = float(balel.text)
        else:
            self.balance = 0


class Txaction(object):
    """Wrapper around a txaction element from Wesabe's xml format.

    Txaction has fields for several pieces of the element's data.  amount as
    amount, merchant as merchant, the year, month and day portion of date as
    date and the name of the account the txaction is in as account. The
    underlying xml.etree.Element object is in the elem field.

    """

    def __init__(self, element, account):
        self.account = account
        self.date = element.findtext('date')[:10]
        self.elem = element
        self.merchant = self.findtext('merchant/name')
        if self.merchant is None:
            self.merchant = self.findtext('raw-name').strip()
        self.amount = float(self.findtext('amount'))

    def __str__(self):
        return 'Txaction at %s on %s for %s' % (self.merchant, 
                self.date, self.amount)

    def findtext(self, path):
        """Pass-thru to findtext on self.elem."""
        return self.elem.findtext(path)

    def findall(self, path):
        """Pass-thru to findall on self.elem."""
        return self.elem.findall(path)

    def total(self, *tags):
        """Figure the amount spent on this transaction for the given tags.

        Takes TAG objects as varargs.  If none are given, the total for
        the transaction is returned.  If there are any, the sum of the
        split amount for each tag is taken.  If the sum is greater than the
        transaction's total, the total is returned.  Otherwise the tag sum
        is returned.

        """
        if not len(tags):
            return self.amount 
        tagged = sum((tag.findsplit(self) for tag in tags))
        if self.amount > 0:
            return min(tagged, self.amount)
        else:
            return max(tagged, self.amount)


class TAG(object):
    """Filter on txaction tags.

    If an instance of TAG is called with a txaction containing the given
    tag with either no split_amount or a split_amount of 0, returns true.
    Otherwise returns false.
    
    """
    def __init__(self, tag):
        """Create a filter on the given tag name."""
        self.tag = tag
        self.name = 'has tag "%s"' % tag

    def __call__(self, txaction):
        return self.findsplit(txaction)

    def __str__(self):
        return 'Tag("%s")' % self.tag

    def findsplit(self, txaction):
        """Find the split_amount of this tag on txaction.

        If there is no split_amount on this tag on txaction, the txaction's
        total is returned.  If this tag doesn't exist in txaction, 0 is
        returned.  Otherwise the float value of split_amount is returned.

        """

        for e in txaction.findall('tags/tag/name'):
            if e.text == self.tag:
                if 'split_amount' in e.attrib:
                    return float(e.attrib['split_amount'])
                else:
                    return txaction.amount
        return 0


def _name_func(func, name):
    func.name = name
    return func
                            

def MERCHANT(merch):
    """Makes a filter that matches on a Txaction.merchant"""
    return _name_func(lambda txaction: merch == txaction.merchant,
            'merchant is "%s"' % merch) 


def ACCOUNT(acct):
    """Makes a filter on the account of a Txaction."""
    return _name_func(lambda txaction: txaction.account == acct, 
            'account is "%s"' % acct)


def AFTER(date):
    """Makes a filter on Txaction dates on or after the given date string.

    The date should be an ISO-8601 date string up to day like 2007-05-01.

    """
    return _name_func(lambda txaction: txaction.date >= date,
            'after %s' % date)


def BEFORE(date):
     """Makes a filter on Txaction dates on or before the given date string.

     The date should be an ISO-8601 date string up to day like 2007-05-01.

     """
     return _name_func(lambda txaction: txaction.date <= date,
            'before %s' % date)

def MONTHS(date, end=None):
    if not end:
        end = list(time.localtime())
    begin = list(to_time(date))
    while begin < end:
        yield MONTH(time.strftime('%Y-%m', begin))
        if begin[1] == 12:
            begin[1] = 1
            begin[0] += 1
        else:
            begin[1] += 1


def to_time(date):
    if not date.count('-'):
        date = '%s-%s' % (time.localtime().tm_year, date)
    return time.strptime(date, '%Y-%m')

def MONTH(date):
    """Makes a filter on Txactions dates falling in the given month.

    The date can be an ISO-8601 date string up to the month like 2007-05.
    It can also be just the month number like 5 or 05 in which case it's
    assumed to be in the current year.

    """
    begin = to_time(date)

    #turn struct_time into a list to make it mutable
    end = list(begin)
    if end[1] == 12:
        end[1] = 1
        end[0] += 1
    else:
        end[1] += 1
    return _name_func(
            AND(AFTER(time.strftime('%Y-%m-%d', begin)), 
                BEFORE(time.strftime('%Y-%m-%d', end))),
            time.strftime('%B %Y', begin))

def INCOME():
    return _name_func(lambda x: x.amount > 0, 'income')

def EXPENSE():
    return _name_func(lambda x: x.amount < 0, 'expense')

def NOT(filter_func):
    """Makes a filter function that negates the result of filter_func."""
    return _name_func(lambda txaction: not filter_func(txaction),
            'not %s' % filter_func.name)


def AND(*filters):
    """Makes a filter that returns true if all of filters return true."""
    return _name_func(lambda txaction: all((f(txaction) for f in filters)),
            '(%s)' % ' and '.join([f.name for f in filters]))
    

def OR(*filters):
    """Makes a filter that returns true if any of filters return true."""
    return _name_func(lambda txaction: any((f(txaction) for f in filters)),
        '(%s)' % ' or '.join([f.name for f in filters]))

def summary(name, values):
    print name
    spacing = max((len(val[0]) for val in values))
#    values.sort(lambda x, y: len(x[0]).__cmp__(len(y[0])))
    print '=' * max([len(name) + 1, (spacing + 11)])
    for name, value in values:
        print '%*s: % .2f' % (spacing, name, value)
    print

if __name__ == '__main__':
   parser = OptionParser()
   begin_default ='0000-00-00' 
   parser.add_option('-b', dest='begin', default=begin_default,
           help='begin time like yyyy-mm-dd')
   end_default = '9999-99-99' 
   parser.add_option('-e', dest='end', default=end_default,
           help='end time like yyyy-mm-dd')
   parser.add_option('-r', dest='required', default='', 
           help='required tags separated by commas')
   parser.add_option('-x', dest='excluded', default='', 
           help='excluded tags separated by commas')
   parser.add_option('-m', dest='merchants', default='', 
           help='required merchants separated by commas')
   (options, args) = parser.parse_args()

   filters = []
   if options.begin != begin_default:
       filters.append(AFTER(options.begin))
   if options.end != end_default:
       filters.append(BEFORE(options.end))
   filters.extend([TAG(t) for t in options.required.split(',') if t])
   filters.extend([NOT(TAG(t)) for t in options.excluded.split(',') if t])
   filters.extend([MERCHANT(t) for t in options.merchants.split(',') if t])

   full_filter = AND(*filters)
   print full_filter.name
   txactions = filter(full_filter, read_txactions())
   txactions.sort(key=operator.attrgetter('date')) 
   for t in txactions:
       print "%s % 8.2f %s" % (t.date, t.amount, t.merchant)
   total = sum((t.amount for t in txactions))
   print len(txactions), 'transactions totaling', total

