1. Robert Brewer
  2. flowrate

Source

flowrate / flowrate / ledger.py

from collections import defaultdict

import flowrate


class defaultlist(object):
    """defaultlist(default_factory) --> boundless list with default factory

    Note that negative indices do *not* count backward from the end. They are
    valid indices just like any positive integer.
    """

    def __init__(self, default_factory=int):
        self._data = []
        self._initial = 0
        self.default_factory = default_factory

    def append(self, item):
        self._data.append(item)

    def extend(self, items):
        self._data.extend(items)

    def __len__(self):
        return len(self._data)

    def __getitem__(self, key):
        key = self._grow(key)
        #print "Fetching", key, self._data[key]
        return self._data[key]

    def __setitem__(self, key, value):
        key = self._grow(key)
        self._data[key] = value

    def _grow(self, key):
        """Grow the list to encompass the key. Return the adjusted key."""
        #print "before", key, self._initial, self._data

        # Calculate boundaries
        final = self._initial + len(self._data)
        if isinstance(key, slice):
            lo, hi = key.start, key.stop
        else:
            lo, hi = key, key + 1

        # Find the new low
        minima = []
        if lo is not None:
            minima.append(lo)
        if self._data:
            minima.append(self._initial)
        if not minima and hi is not None:
            minima.append(hi - 1)
        newlow = min(minima)

        # Find the new high
        maxima = []
        if hi is not None:
            maxima.append(hi)
        if self._data:
            maxima.append(final)
        if not maxima and lo is not None:
            maxima.append(lo)
        newhigh = max(maxima)

        # Grow boundaries if needed
        #print "Resizing from %s:%s to %s:%s" % (self._initial, final, newlow, newhigh)
        if self._data:
            if newlow < self._initial:
                self._data = [self.default_factory()
                              for i in xrange(self._initial - newlow)
                              ] + self._data
            if newhigh > final:
                self._data.extend(self.default_factory()
                                  for i in xrange(newhigh - final))
        else:
            self._data = [self.default_factory()
                          for i in xrange(newhigh - newlow)]
        self._initial = newlow

        # Modify the key to match our new boundaries.
        if isinstance(key, slice):
            key = slice(lo if lo is None else max(lo - self._initial, 0),
                        hi if hi is None else max(hi - self._initial, 0),
                        key.step)
        else:
            key = lo if lo is None else max(lo - self._initial, 0)

        #print "after", key, self._initial, self._data
        return key


accounts = {}

class Account(object):

    fields = ('name', 'type')

    def __init__(self, id, **kwargs):
        self.id = id
        self.row = dict((k, kwargs[k]) for k in self.fields)

        # A defaultlist of transaction activity on this account,
        # one entry per datetime.date.
        # Each value is a balance *difference* for the day.
        # So to read the balance at the end of 2012-04-13:
        #   sum(activity[:date(2012, 4, 14)])
        self.activity = defaultlist()

        # A defaultlist of obligation budgets on this account,
        # one entry per datetime.date.
        # Each value is a budget for the day.
        # So to read the total budget for 2012-04:
        #   sum(budgets[date(2012, 4, 1):date(2012, 5, 1)])
        self.budgets = defaultlist()

        # A defaultlist of sum(obligation fulfillments) on this account,
        # one entry per datetime.date.
        # Each value is a balance *difference* for the day.
        # So to read the balance fulfilled at the end of 2012-04-13:
        #   sum(fulfillments[:date(2012, 4, 14)])
        self.fulfillments = defaultlist()

    @classmethod
    def load_all(cls):
        """Populate the accounts dictionary from the database."""
        for row in flowrate.db.execute("SELECT * FROM accounts;").fetchall():
            acct = cls(row.id, **dict((k, row[k]) for k in cls.fields))
            accounts[row.id] = acct

    def __getitem__(self, key):
        return self.row[key]

    def __setitem__(self, key, value):
        self.row[key] = value

    def create(self):
        flowrate.db.execute(
            "INSERT INTO accounts (id, name, type) "
            "VALUES (%(id)s, %(name)s, %(type)s);",
            {'id': self.id, 'name': self['name'], 'type': self['type']}
            )
        accounts[self.id] = self

    def update(self):
        row = {'id': self.id}
        row.update(self.row)
        flowrate.db.execute(
            "UPDATE accounts SET name = %(name)s, type = %(type)s "
            "WHERE id = %(id)s;", row)

    def delete(self):
        flowrate.db.execute(
            "DELETE FROM accounts WHERE id = %s;", (self.id,))
        accounts.pop(self.id, None)


transactions = {}

class Transaction(object):

    fields = ('postdate', 'credit', 'debit', 'description',
              'amount', 'credit_mult', 'debit_mult')

    def __init__(self, id, **kwargs):
        self.id = id
        self.row = dict((k, kwargs[k]) for k in self.fields)
        self.fulfillments = []

    @classmethod
    def load_all(cls):
        """Populate the transactions dictionary from the database."""
        for row in flowrate.db.execute("SELECT * FROM transactions;").fetchall():
            data = {}
            for k in cls.fields:
                f = k
                if f in ('credit', 'debit'):
                    f = f + '_account'
                data[k] = row[f]
            tx = cls(row.id, **data)
            transactions[row.id] = tx
            tx.activate()

    def activate(self):
        """Apply this transaction's amount to daily balances."""
        ca = accounts[self['credit']].activity
        ca[self['postdate'].toordinal()] += self['amount'] * self['credit_mult']
        da = accounts[self['debit']].activity
        da[self['postdate'].toordinal()] += self['amount'] * self['debit_mult']

    def deactivate(self):
        """Apply this transaction's amount to daily balances."""
        ca = accounts[self['credit']].activity
        ca[self['postdate'].toordinal()] -= self['amount'] * self['credit_mult']
        da = accounts[self['debit']].activity
        da[self['postdate'].toordinal()] -= self['amount'] * self['debit_mult']

    def __getitem__(self, key):
        return self.row[key]

    def __setitem__(self, key, value):
        self.row[key] = value

    def save(self):
        if self.id is None:
            row = flowrate.db.execute(
                "INSERT INTO transactions"
                " (postdate, credit_account, debit_account, description,"
                " amount, credit_mult, debit_mult) "
                "VALUES (%(postdate)s, %(credit)s, %(debit)s,"
                " %(description)s, %(amount)s, %(credit_mult)s, %(debit_mult)s) "
                "RETURNING *", self.row).fetchone()
            self.id = row.id
            transactions[self.id] = self
        else:
            row = {'id': self.id}
            row.update(self.row)
            flowrate.db.execute(
                "UPDATE transactions SET postdate = %(postdate)s, "
                "credit_account = %(credit)s, "
                "debit_account = %(debit)s, "
                "description = %(description)s, amount = %(amount)s "
                "credit_mult = %(credit_mult)s, debit_mult = %(debit_mult)s, "
                "WHERE id = %(id)s;", row)

    def delete(self):
        flowrate.db.execute(
            "DELETE FROM transactions WHERE id = %s;", (self.id,))
        transactions.pop(self.id, None)


def find_transactions(filters):
    """Return Transaction objects (dicts) matching the given criteria."""
    if not filters:
        # If there are no filters, return nothing instead of everything
        return []

    txs = [{'id': tx.id, 'postdate': tx['postdate'],
            'credit': tx['credit'], 'debit': tx['debit'],
            'description': tx['description'], 'amount': tx['amount']}
           for tx in transactions.itervalues()
           if all(f(tx) for f in filters)]
    return flowrate.ordered(txs, '-postdate', 'credit', 'debit')