Changeset - fc2c3a6b4339
[Not reviewed]
0 13 0
Joar Wandborg - 11 years ago 2013-12-17 14:41:30
joar@wandborg.se
[license] Added notice to all python files
13 files changed with 52 insertions and 0 deletions:
0 comments (0 inline, 0 general)
accounting/__init__.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
accounting/client.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import sys
 
import argparse
 
import json
 
import logging
 
import locale
 

	
 
from datetime import datetime
 
from decimal import Decimal
 

	
 
import requests
 

	
 
from accounting.models import Transaction, Posting, Amount
 
from accounting.transport import AccountingDecoder, AccountingEncoder
 

	
 
locale.setlocale(locale.LC_ALL, '')
 

	
 
_log = logging.getLogger(__name__)
 

	
 

	
 
class Client:
 
    def __init__(self, host=None, json_encoder=None,
 
                 json_decoder=None):
 
        self.host = host or 'http://localhost:5000'
 
        self.json_encoder = json_encoder or AccountingEncoder
 
        self.json_decoder = json_decoder or AccountingDecoder
 

	
 
    def get_balance(self):
 
        balance = self.get('/balance')
 
        return balance['balance_report']
 

	
 
    def get(self, path):
 
        response = requests.get(self.host + path)
 

	
 
        return self._decode_response(response)
 

	
 
    def _decode_response(self, response):
 
        response_data = response.json(cls=self.json_decoder)
 

	
 
        _log.debug('response_data: %s', response_data)
 

	
 
        return response_data
 

	
 
    def post(self, path, payload, **kw):
 
        kw.update({'headers': {'Content-Type': 'application/json'}})
 
        kw.update({'data': json.dumps(payload, cls=self.json_encoder)})
 

	
 
        return self._decode_response(requests.post(self.host + path, **kw))
 

	
 
    def simple_transaction(self, from_acc, to_acc, amount, symbol=None,
 
                           payee=None):
 
        if symbol is None:
 
            # Get the currency from the environment locale
 
            symbol = locale.localeconv()['int_curr_symbol'].strip()
 

	
 
        t = Transaction(
 
            date=datetime.today(),
 
            payee=payee,
 
            postings=[
 
                Posting(account=from_acc,
 
                        amount=Amount(symbol=symbol, amount=-amount)),
 
                Posting(account=to_acc,
 
                        amount=Amount(symbol=symbol, amount=amount))
 
            ]
 
        )
 

	
 
        return self.post('/transaction', {'transactions': [t]})
 

	
 
    def get_register(self):
 
        register = self.get('/transaction')
 

	
 
        return register['transactions']
 

	
 

	
 
def print_transactions(transactions):
 
    for transaction in transactions:
 
        print('{date} {t.payee:.<69}'.format(
 
            date=transaction.date.strftime('%Y-%m-%d'),
 
            t=transaction))
 

	
 
        for posting in transaction.postings:
 
            print(' ' + posting.account +
 
                  ' ' * (80 - len(posting.account) -
 
                         len(posting.amount.symbol) -
 
                         len(str(posting.amount.amount)) - 1 - 1) +
 
                  posting.amount.symbol + ' ' + str(posting.amount.amount))
 

	
 

	
 
def print_balance_accounts(accounts, level=0):
 
    for account in accounts:
 
        print(' ' * level + ' + {account.name}'.format(account=account) +
 
              ' ' + '-' * (80 - len(str(account.name)) - level))
 

	
 
        for amount in account.amounts:
 
            print(' ' * level + '   {amount.symbol} {amount.amount}'.format(
 
                amount=amount))
 

	
accounting/config.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import os
 

	
 
LEDGER_FILE = os.environ.get('LEDGER_FILE', None)
 
DEBUG = bool(int(os.environ.get('DEBUG', 0)))
 
PORT = int(os.environ.get('PORT', 5000))
 
HOST = os.environ.get('HOST', '127.0.0.1')
 
SQLALCHEMY_DATABASE_URI = os.environ.get(
 
    'DATABASE_URI',
 
    'sqlite:///../accounting-api.sqlite')
accounting/decorators.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
from functools import wraps
 

	
 
from flask import jsonify
 

	
 
from accounting.exceptions import AccountingException
 

	
 

	
 
def jsonify_exceptions(func):
 
    '''
 
    Wraps a Flask endpoint and catches any AccountingException-based
 
    exceptions which are returned to the client as JSON.
 
    '''
 
    @wraps(func)
 
    def wrapper(*args, **kw):
 
        try:
 
            return func(*args, **kw)
 
        except AccountingException as exc:
 
            return jsonify(error=exc)
 

	
 
    return wrapper
accounting/exceptions.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
class AccountingException(Exception):
 
    '''
 
    Used as a base for exceptions that are returned to the caller via the
 
    jsonify_exceptions decorator
 
    '''
 
    pass
accounting/gtkclient.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import sys
 
import logging
 
import threading
 
import pkg_resources
 

	
 
from functools import wraps
 
from datetime import datetime
 

	
 
from gi.repository import Gtk
 
from gi.repository import GLib
 
from gi.repository import GObject
 

	
 
from accounting.client import Client
 

	
 
_log = logging.getLogger(__name__)
 

	
 

	
 
def indicate_activity(func_or_str):
 
    description = None
 

	
 
    def decorator(func):
 
        @wraps(func)
 
        def wrapper(self, *args, **kw):
 
            self.activity_description.set_text(description)
 
            self.activity_indicator.show()
 
            self.activity_indicator.start()
 

	
 
            return func(self, *args, **kw)
 

	
 
        return wrapper
 

	
 
    if callable(func_or_str):
 
        description = 'Working'
 
        return decorator(func_or_str)
 
    else:
 
        description = func_or_str
 
        return decorator
 

	
 

	
 
def indicate_activity_done(func):
 
    @wraps(func)
 
    def wrapper(self, *args, **kw):
 
        self.activity_description.set_text('')
 
        self.activity_indicator.stop()
 
        self.activity_indicator.hide()
 

	
 
        return func(self, *args, **kw)
 

	
 
    return wrapper
 

	
 

	
 
class AccountingApplication:
 
    def __init__(self):
 
        #Gtk.Window.__init__(self, title='Accounting Client')
 

	
 
        self.client = Client()
 

	
 
        self.load_ui(pkg_resources.resource_filename(
 
            'accounting', 'res/client-ui.glade'))
 

	
 
        self.about_dialog.set_transient_for(self.accounting_window)
 

	
 
        self.accounting_window.connect('delete-event', Gtk.main_quit)
 
        self.accounting_window.set_border_width(0)
 
        self.accounting_window.set_default_geometry(640, 360)
 

	
 
        self.accounting_window.show_all()
 
        self.transaction_detail.hide()
 

	
 
    def load_ui(self, path):
 
        _log.debug('Loading UI...')
 
        builder = Gtk.Builder()
 
        builder.add_from_file(path)
 
        builder.connect_signals(self)
 

	
 
        for element in builder.get_objects():
 
            try:
 
                setattr(self, Gtk.Buildable.get_name(element), element)
 
                _log.debug('Loaded %s', Gtk.Buildable.get_name(element))
 
            except TypeError as exc:
 
                _log.error('%s could not be loaded: %s', element, exc)
 

	
 
        _log.debug('UI loaded')
 

	
 
    def on_transaction_view_cursor_changed(self, widget):
 
        selection = self.transaction_view.get_selection()
 
        selection.set_mode(Gtk.SelectionMode.SINGLE)
 
        xact_store, xact_iter = selection.get_selected()
 

	
 
        xact_id = xact_store.get_value(xact_iter, 0)
 
        _log.debug('selection: %s', xact_id)
 

	
 
        for transaction in self.transaction_data:
 
            if transaction.id == xact_id:
 
                self.transaction_header.set_text(transaction.payee)
 

	
accounting/models.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import uuid
 
from decimal import Decimal
 

	
 

	
 
class Transaction:
 
    def __init__(self, id=None, date=None, payee=None, postings=None,
 
                 metadata=None, _generate_id=False):
 
        self.id = id
 
        self.date = date
 
        self.payee = payee
 
        self.postings = postings
 
        self.metadata = metadata if metadata is not None else {}
 

	
 
        if _generate_id:
 
            self.generate_id()
 

	
 
    def generate_id(self):
 
        self.id = str(uuid.uuid4())
 

	
 
    def __repr__(self):
 
        return ('<{self.__class__.__name__} {self.id} {date}' +
 
                ' {self.payee} {self.postings}').format(
 
                    self=self,
 
                    date=self.date.strftime('%Y-%m-%d'))
 

	
 

	
 
class Posting:
 
    def __init__(self, account=None, amount=None, metadata=None):
 
        self.account = account
 
        self.amount = amount
 
        self.metadata = metadata if metadata is not None else {}
 

	
 
    def __repr__(self):
 
        return ('<{self.__class__.__name__} "{self.account}"' +
 
                ' {self.amount}>').format(self=self)
 

	
 

	
 
class Amount:
 
    def __init__(self, amount=None, symbol=None):
 
        self.amount = Decimal(amount)
 
        self.symbol = symbol
 

	
 
    def __repr__(self):
 
        return ('<{self.__class__.__name__} {self.symbol}' +
 
                ' {self.amount}>').format(self=self)
 

	
 

	
 
class Account:
 
    def __init__(self, name=None, amounts=None, accounts=None):
 
        self.name = name
 
        self.amounts = amounts
 
        self.accounts = accounts
 

	
 
    def __repr__(self):
 
        return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' +
 
                ' {self.accounts}>').format(self=self)
accounting/storage/__init__.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
from abc import ABCMeta, abstractmethod
 

	
 

	
 
class Storage():
 
    '''
 
    ABC for accounting storage
 
    '''
 
    __metaclass__ = ABCMeta
 

	
 
    def __init__(self, *args, **kw):
 
        pass
 

	
 
    @abstractmethod
 
    def get_transactions(self, *args, **kw):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def get_transaction(self, *args, **kw):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def get_account(self, *args, **kw):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def get_accounts(self, *args, **kw):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def add_transaction(self, transaction):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def update_transaction(self, transaction):
 
        raise NotImplementedError
 

	
 
    @abstractmethod
 
    def reverse_transaction(self, transaction_id):
 
        raise NotImplementedError
accounting/storage/ledgercli.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import sys
 
import subprocess
 
import logging
 
import time
 

	
 
from datetime import datetime
 
from xml.etree import ElementTree
 
from contextlib import contextmanager
 

	
 
from accounting.models import Account, Transaction, Posting, Amount
 
from accounting.storage import Storage
 

	
 
_log = logging.getLogger(__name__)
 

	
 

	
 
class Ledger(Storage):
 
    def __init__(self, app=None, ledger_file=None, ledger_bin=None):
 
        if app:
 
            ledger_file = app.config['LEDGER_FILE']
 

	
 
        if ledger_file is None:
 
            raise ValueError('ledger_file cannot be None')
 

	
 
        self.ledger_bin = ledger_bin or 'ledger'
 
        self.ledger_file = ledger_file
 
        _log.info('ledger file: %s', ledger_file)
 

	
 
        self.locked = False
 
        self.ledger_process = None
 

	
 
    @contextmanager
 
    def locked_process(self):
 
        r'''
 
        Context manager that checks that the ledger process is not already
 
        locked, then "locks" the process and yields the process handle and
 
        unlocks the process when execution is returned.
 

	
 
        Since this decorated as a :func:`contextlib.contextmanager` the
 
        recommended use is with the ``with``-statement.
 

	
 
        .. code-block:: python
 

	
 
            with self.locked_process() as p:
 
                p.stdin.write(b'bal\n')
 

	
 
                output = self.read_until_prompt(p)
 

	
 
        '''
 
        if self.locked:
 
            raise RuntimeError('The process has already been locked,'
 
                               ' something\'s out of order.')
 

	
 
            # XXX: This code has no purpose in a single-threaded process
 
            timeout = 5  # Seconds
 

	
 
            for i in range(1, timeout + 2):
 
                if i > timeout:
 
                    raise RuntimeError('Ledger process is already locked')
 

	
 
                if not self.locked:
 
                    break
 
                else:
 
                    _log.info('Waiting for one second... %d/%d', i, timeout)
 
                    time.sleep(1)
 

	
 
        process = self.get_process()
 

	
 
        self.locked = True
 
        _log.debug('Lock enabled')
 

	
 
        yield process
 

	
 
        self.locked = False
 
        _log.debug('Lock disabled')
 

	
 
    def assemble_arguments(self):
 
        '''
 
        Returns a list of arguments suitable for :class:`subprocess.Popen`
 
        based on :attr:`self.ledger_bin` and :attr:`self.ledger_file`.
 
        '''
 
        return [
 
            self.ledger_bin,
 
            '-f',
 
            self.ledger_file,
 
        ]
 

	
 
    def init_process(self):
 
        '''
 
        Creates a new (presumably) ledger subprocess based on the args from
 
        :meth:`Ledger.assemble_arguments()` and then runs
 
        :meth:`Ledger.read_until_prompt()` once (which should return the banner
 
        text) and discards the output.
 
        '''
 
        _log.debug('Starting ledger process...')
 
        self.ledger_process = subprocess.Popen(
 
            self.assemble_arguments(),
accounting/storage/sql/__init__.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import logging
 
import json
 

	
 
from flask.ext.sqlalchemy import SQLAlchemy
 

	
 
from accounting.exceptions import AccountingException
 
from accounting.storage import Storage
 
from accounting.models import Transaction, Posting, Amount
 

	
 
_log = logging.getLogger(__name__)
 
db = SQLAlchemy()
 

	
 

	
 
class SQLStorage(Storage):
 
    def __init__(self, app=None):
 

	
 
        if not app:
 
            raise Exception('Missing app keyword argument')
 

	
 
        self.app = app
 
        db.init_app(app)
 

	
 
        from .models import Transaction as SQLTransaction, \
 
            Posting as SQLPosting, Amount as SQLAmount
 

	
 
        db.create_all()
 

	
 
        self.Transaction = SQLTransaction
 
        self.Posting = SQLPosting
 
        self.Amount = SQLAmount
 

	
 
    def get_transactions(self, *args, **kw):
 
        transactions = []
 

	
 
        for transaction in self.Transaction.query.all():
 
            dict_transaction = transaction.as_dict()
 
            dict_postings = dict_transaction.pop('postings')
 

	
 
            postings = []
 

	
 
            for dict_posting in dict_postings:
 
                dict_amount = dict_posting.pop('amount')
 
                posting = Posting(**dict_posting)
 
                posting.amount = Amount(**dict_amount)
 

	
 
                postings.append(posting)
 

	
 
            dict_transaction.update({'postings': postings})
 

	
 
            transactions.append(Transaction(**dict_transaction))
 

	
 
        return transactions
 

	
 
    def update_transaction(self, transaction):
 
        if transaction.id is None:
 
            raise AccountingException('The transaction id must be set for'
 
                                      ' update_transaction calls')
 

	
 
        _log.debug('DUMMY: Update transaction: %s', transaction)
 

	
 
    def add_transaction(self, transaction):
 
        if transaction.id is None:
 
            transaction.generate_id()
 

	
 
        _t = self.Transaction()
 
        _t.uuid = transaction.id
 
        _t.date = transaction.date
 
        _t.payee = transaction.payee
 
        _t.meta = json.dumps(transaction.metadata)
 

	
 
        db.session.add(_t)
 

	
 
        for posting in transaction.postings:
 
            _p = self.Posting()
 
            _p.transaction_uuid = transaction.id
 
            _p.account = posting.account
 
            _p.meta = json.dumps(posting.metadata)
 
            _p.amount = self.Amount(symbol=posting.amount.symbol,
 
                                    amount=posting.amount.amount)
 

	
 
            db.session.add(_p)
 

	
 
        db.session.commit()
accounting/storage/sql/models.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
import json
 

	
 
from . import db
 

	
 

	
 
class Transaction(db.Model):
 
    id = db.Column(db.Integer(), primary_key=True)
 
    uuid = db.Column(db.String, unique=True, nullable=False)
 
    date = db.Column(db.DateTime)
 
    payee = db.Column(db.String())
 
    meta = db.Column(db.String())
 

	
 
    def as_dict(self):
 
        return dict(
 
            id=self.uuid,
 
            date=self.date,
 
            payee=self.payee,
 
            postings=[p.as_dict() for p in self.postings],
 
            metadata=json.loads(self.meta)
 
        )
 

	
 

	
 
class Posting(db.Model):
 
    id = db.Column(db.Integer(), primary_key=True)
 

	
 
    transaction_uuid = db.Column(db.String, db.ForeignKey('transaction.uuid'))
 
    transaction = db.relationship('Transaction', backref='postings')
 

	
 
    account = db.Column(db.String, nullable=False)
 

	
 
    amount_id = db.Column(db.Integer, db.ForeignKey('amount.id'))
 
    amount = db.relationship('Amount')
 

	
 
    meta = db.Column(db.String)
 

	
 
    def as_dict(self):
 
        return dict(
 
            account=self.account,
 
            amount=self.amount.as_dict(),
 
            metadata=json.loads(self.meta)
 
        )
 

	
 

	
 
class Amount(db.Model):
 
    id = db.Column(db.Integer, primary_key=True)
 
    symbol = db.Column(db.String)
 
    amount = db.Column(db.Numeric)
 

	
 
    def as_dict(self):
 
        return dict(
 
            symbol=self.symbol,
 
            amount=self.amount
 
        )
accounting/transport.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
from datetime import datetime
 

	
 
from flask import json
 

	
 
from accounting.models import Amount, Transaction, Posting, Account
 

	
 

	
 
class AccountingEncoder(json.JSONEncoder):
 
    def default(self, o):
 
        if isinstance(o, Account):
 
            return dict(
 
                __type__=o.__class__.__name__,
 
                name=o.name,
 
                amounts=o.amounts,
 
                accounts=o.accounts
 
            )
 
        elif isinstance(o, Transaction):
 
            return dict(
 
                __type__=o.__class__.__name__,
 
                id=o.id,
 
                date=o.date.strftime('%Y-%m-%d'),
 
                payee=o.payee,
 
                postings=o.postings,
 
                metadata=o.metadata
 
            )
 
        elif isinstance(o, Posting):
 
            return dict(
 
                __type__=o.__class__.__name__,
 
                account=o.account,
 
                amount=o.amount,
 
                metadata=o.metadata
 
            )
 
        elif isinstance(o, Amount):
 
            return dict(
 
                __type__=o.__class__.__name__,
 
                amount=str(o.amount),
 
                symbol=o.symbol
 
            )
 
        elif isinstance(o, Exception):
 
            return dict(
 
                __type__=o.__class__.__name__,
 
                args=o.args
 
            )
 

	
 
        return json.JSONEncoder.default(self, o)
 

	
 

	
 
class AccountingDecoder(json.JSONDecoder):
 
    def __init__(self):
 
        json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)
 

	
 
    def dict_to_object(self, d):
 
        if '__type__' not in d:
 
            return d
 

	
 
        types = {c.__name__: c for c in [Amount, Transaction, Posting,
 
                                         Account]}
 

	
 
        _type = d.pop('__type__')
 

	
 
        if _type == 'Transaction':
 
            d['date'] = datetime.strptime(d['date'], '%Y-%m-%d')
 

	
 
        return types[_type](**d)
accounting/web.py
Show inline comments
 
# Part of accounting-api project:
 
# https://gitorious.org/conservancy/accounting-api
 
# License: AGPLv3-or-later
 

	
 
'''
 
This module contains the high-level webservice logic such as the Flask setup
 
and the Flask endpoints.
 
'''
 
import sys
 
import logging
 
import argparse
 

	
 
from flask import Flask, jsonify, request
 
from flask.ext.sqlalchemy import SQLAlchemy
 
from flask.ext.script import Manager
 
from flask.ext.migrate import Migrate, MigrateCommand
 

	
 
from accounting.storage import Storage
 
from accounting.storage.ledgercli import Ledger
 
from accounting.storage.sql import SQLStorage
 
from accounting.transport import AccountingEncoder, AccountingDecoder
 
from accounting.exceptions import AccountingException
 
from accounting.decorators import jsonify_exceptions
 

	
 

	
 
app = Flask('accounting')
 
app.config.from_pyfile('config.py')
 

	
 
storage = Storage()
 

	
 
if isinstance(storage, SQLStorage):
 
    # TODO: Move migration stuff into SQLStorage
 
    db = storage.db
 
    migrate = Migrate(app, db)
 

	
 
    manager = Manager(app)
 
    manager.add_command('db', MigrateCommand)
 

	
 

	
 
@app.before_request
 
def init_ledger():
 
    '''
 
    :py:meth:`flask.Flask.before_request`-decorated method that initializes an
 
    :py:class:`accounting.Ledger` object.
 
    '''
 
    global ledger
 
    #ledger = Ledger(ledger_file=app.config['LEDGER_FILE'])
 

	
 

	
 
# These will convert output from our internal classes to JSON and back
 
app.json_encoder = AccountingEncoder
 
app.json_decoder = AccountingDecoder
 

	
 

	
 
@app.route('/')
 
def index():
 
    ''' Hello World! '''
 
    return 'Hello World!'
 

	
 

	
 
@app.route('/transaction', methods=['GET'])
 
def transaction_get():
 
    '''
 
    Returns the JSON-serialized output of :meth:`accounting.Ledger.reg`
 
    '''
 
    return jsonify(transactions=storage.get_transactions())
 

	
 
@app.route('/transaction/<string:transaction_id>', methods=['POST'])
 
@jsonify_exceptions
 
def transaction_update(transaction_id=None):
 
    if transaction_id is None:
 
        raise AccountingException('The transaction ID cannot be None.')
 

	
 
    transaction = request.json['transaction']
 

	
 
    if transaction.id is not None and not transaction.id == transaction_id:
 
        raise AccountingException('The transaction data has an ID attribute and'
 
                                  ' it is not the same ID as in the path')
 
    elif transaction.id is None:
 
        transaction.id = transaction_id
 

	
 
    storage.update_transaction(transaction)
 

	
 
    return jsonify(status='OK')
 

	
 

	
 
@app.route('/transaction', methods=['POST'])
 
@jsonify_exceptions
 
def transaction_post():
 
    '''
 
    REST/JSON endpoint for transactions.
 

	
 
    Current state:
 

	
 
    Takes a POST request with a ``transactions`` JSON payload and writes it to
 
    the ledger file.
 

	
 
    Requires the ``transactions`` payload to be __type__-annotated:
 

	
 
    .. code-block:: json
0 comments (0 inline, 0 general)