2013-12-17 14:41:30 +00:00
|
|
|
# Part of accounting-api project:
|
|
|
|
# https://gitorious.org/conservancy/accounting-api
|
|
|
|
# License: AGPLv3-or-later
|
|
|
|
|
2013-12-12 14:09:53 +00:00
|
|
|
'''
|
|
|
|
This module contains the high-level webservice logic such as the Flask setup
|
|
|
|
and the Flask endpoints.
|
|
|
|
'''
|
2013-12-10 22:22:57 +00:00
|
|
|
import sys
|
2013-12-09 19:53:20 +00:00
|
|
|
import logging
|
2013-12-10 22:22:57 +00:00
|
|
|
import argparse
|
2013-12-09 19:53:20 +00:00
|
|
|
|
2013-12-20 13:51:32 +00:00
|
|
|
from flask import Flask, jsonify, request, render_template
|
2013-12-14 15:08:47 +00:00
|
|
|
from flask.ext.script import Manager
|
|
|
|
from flask.ext.migrate import Migrate, MigrateCommand
|
2013-12-09 19:53:20 +00:00
|
|
|
|
2013-12-20 13:51:32 +00:00
|
|
|
from accounting.models import Transaction
|
2013-12-17 10:18:35 +00:00
|
|
|
from accounting.storage import Storage
|
2013-12-14 15:08:47 +00:00
|
|
|
from accounting.storage.ledgercli import Ledger
|
|
|
|
from accounting.storage.sql import SQLStorage
|
2013-12-10 22:22:57 +00:00
|
|
|
from accounting.transport import AccountingEncoder, AccountingDecoder
|
2013-12-10 23:25:16 +00:00
|
|
|
from accounting.exceptions import AccountingException
|
2013-12-20 13:51:32 +00:00
|
|
|
from accounting.decorators import jsonify_exceptions, cors
|
2013-12-09 19:53:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
app = Flask('accounting')
|
|
|
|
app.config.from_pyfile('config.py')
|
|
|
|
|
2013-12-17 10:18:35 +00:00
|
|
|
storage = Storage()
|
2013-12-14 15:08:47 +00:00
|
|
|
|
2013-12-16 06:33:56 +00:00
|
|
|
if isinstance(storage, SQLStorage):
|
|
|
|
# TODO: Move migration stuff into SQLStorage
|
|
|
|
db = storage.db
|
|
|
|
migrate = Migrate(app, db)
|
2013-12-14 15:08:47 +00:00
|
|
|
|
2013-12-16 06:33:56 +00:00
|
|
|
manager = Manager(app)
|
|
|
|
manager.add_command('db', MigrateCommand)
|
2013-12-14 15:08:47 +00:00
|
|
|
|
2013-12-12 14:09:53 +00:00
|
|
|
|
|
|
|
@app.before_request
|
|
|
|
def init_ledger():
|
|
|
|
'''
|
|
|
|
:py:meth:`flask.Flask.before_request`-decorated method that initializes an
|
|
|
|
:py:class:`accounting.Ledger` object.
|
|
|
|
'''
|
|
|
|
global ledger
|
2013-12-14 15:08:47 +00:00
|
|
|
#ledger = Ledger(ledger_file=app.config['LEDGER_FILE'])
|
2013-12-09 19:53:20 +00:00
|
|
|
|
2013-12-09 20:49:38 +00:00
|
|
|
|
2013-12-10 22:22:57 +00:00
|
|
|
# These will convert output from our internal classes to JSON and back
|
2013-12-09 19:53:20 +00:00
|
|
|
app.json_encoder = AccountingEncoder
|
2013-12-10 22:22:57 +00:00
|
|
|
app.json_decoder = AccountingDecoder
|
2013-12-09 19:53:20 +00:00
|
|
|
|
2013-12-09 20:49:38 +00:00
|
|
|
|
2013-12-09 19:53:20 +00:00
|
|
|
@app.route('/')
|
|
|
|
def index():
|
2013-12-12 14:09:53 +00:00
|
|
|
''' Hello World! '''
|
2013-12-09 19:53:20 +00:00
|
|
|
return 'Hello World!'
|
|
|
|
|
2013-12-20 13:51:32 +00:00
|
|
|
@app.route('/client')
|
|
|
|
def client():
|
|
|
|
return render_template('client.html')
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/transaction', methods=['OPTIONS'])
|
|
|
|
@cors()
|
|
|
|
@jsonify_exceptions
|
|
|
|
def transaction_options():
|
|
|
|
return jsonify(status='OPTIONS')
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/transaction/<string:transaction_id>', methods=['OPTIONS'])
|
|
|
|
@cors()
|
|
|
|
@jsonify_exceptions
|
|
|
|
def transaction_by_id_options(transaction_id=None):
|
|
|
|
return jsonify(status='OPTIONS')
|
|
|
|
|
2013-12-09 20:49:38 +00:00
|
|
|
|
2013-12-12 09:27:51 +00:00
|
|
|
@app.route('/transaction', methods=['GET'])
|
2013-12-20 13:51:32 +00:00
|
|
|
@app.route('/transaction/<string:transaction_id>', methods=['GET'])
|
|
|
|
@cors()
|
|
|
|
@jsonify_exceptions
|
|
|
|
def transaction_get(transaction_id=None):
|
2013-12-12 14:09:53 +00:00
|
|
|
'''
|
|
|
|
Returns the JSON-serialized output of :meth:`accounting.Ledger.reg`
|
|
|
|
'''
|
2013-12-20 13:51:32 +00:00
|
|
|
if transaction_id is None:
|
|
|
|
return jsonify(transactions=storage.get_transactions())
|
|
|
|
|
|
|
|
return jsonify(transaction=storage.get_transaction(transaction_id))
|
2013-12-14 15:08:47 +00:00
|
|
|
|
2013-12-18 21:02:03 +00:00
|
|
|
|
2013-12-16 06:33:56 +00:00
|
|
|
@app.route('/transaction/<string:transaction_id>', methods=['POST'])
|
2013-12-20 13:51:32 +00:00
|
|
|
@cors()
|
2013-12-16 06:33:56 +00:00
|
|
|
@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:
|
2013-12-18 21:02:03 +00:00
|
|
|
raise AccountingException('The transaction data has an ID attribute'
|
|
|
|
' and it is not the same ID as in the path')
|
2013-12-16 06:33:56 +00:00
|
|
|
elif transaction.id is None:
|
|
|
|
transaction.id = transaction_id
|
|
|
|
|
|
|
|
storage.update_transaction(transaction)
|
|
|
|
|
|
|
|
return jsonify(status='OK')
|
|
|
|
|
2013-12-09 20:49:38 +00:00
|
|
|
|
2013-12-18 21:02:03 +00:00
|
|
|
@app.route('/transaction/<string:transaction_id>', methods=['DELETE'])
|
2013-12-20 13:51:32 +00:00
|
|
|
@cors()
|
2013-12-18 21:02:03 +00:00
|
|
|
@jsonify_exceptions
|
|
|
|
def transaction_delete(transaction_id=None):
|
|
|
|
if transaction_id is None:
|
|
|
|
raise AccountingException('Transaction ID cannot be None')
|
|
|
|
|
|
|
|
storage.delete_transaction(transaction_id)
|
|
|
|
|
|
|
|
return jsonify(status='OK')
|
|
|
|
|
|
|
|
|
2013-12-10 23:25:16 +00:00
|
|
|
@app.route('/transaction', methods=['POST'])
|
2013-12-20 13:51:32 +00:00
|
|
|
@cors()
|
2013-12-10 23:25:16 +00:00
|
|
|
@jsonify_exceptions
|
2013-12-12 09:27:51 +00:00
|
|
|
def transaction_post():
|
2013-12-10 23:25:16 +00:00
|
|
|
'''
|
|
|
|
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
|
|
|
|
|
|
|
|
{
|
|
|
|
"transactions": [
|
|
|
|
{
|
|
|
|
"__type__": "Transaction",
|
|
|
|
"date": "2013-01-01",
|
|
|
|
"payee": "Kindly T. Donor",
|
|
|
|
"postings": [
|
|
|
|
{
|
|
|
|
"__type__": "Posting",
|
|
|
|
"account": "Income:Foo:Donation",
|
|
|
|
"amount": {
|
|
|
|
"__type__": "Amount",
|
|
|
|
"amount": "-100",
|
|
|
|
"symbol": "$"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"__type__": "Posting",
|
|
|
|
"account": "Assets:Checking",
|
|
|
|
"amount": {
|
|
|
|
"__type__": "Amount",
|
|
|
|
"amount": "100",
|
|
|
|
"symbol": "$"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
becomes::
|
|
|
|
|
|
|
|
2013-01-01 Kindly T. Donor
|
|
|
|
Income:Foo:Donation $ -100
|
|
|
|
Assets:Checking $ 100
|
|
|
|
'''
|
2013-12-20 13:51:32 +00:00
|
|
|
if not isinstance(request.json, Transaction):
|
|
|
|
transactions = request.json.get('transactions')
|
|
|
|
else:
|
|
|
|
transactions = [request.json]
|
2013-12-10 23:25:16 +00:00
|
|
|
|
|
|
|
if not transactions:
|
|
|
|
raise AccountingException('No transaction data provided')
|
|
|
|
|
2013-12-18 21:02:03 +00:00
|
|
|
transaction_ids = []
|
|
|
|
|
2013-12-10 23:25:16 +00:00
|
|
|
for transaction in transactions:
|
2013-12-18 21:02:03 +00:00
|
|
|
transaction_ids.append(storage.add_transaction(transaction))
|
2013-12-10 23:25:16 +00:00
|
|
|
|
2013-12-18 21:02:03 +00:00
|
|
|
return jsonify(status='OK', transaction_ids=transaction_ids)
|
2013-12-10 22:22:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
def main(argv=None):
|
|
|
|
prog = __name__
|
|
|
|
if argv is None:
|
|
|
|
prog = sys.argv[0]
|
|
|
|
argv = sys.argv[1:]
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog=prog)
|
|
|
|
parser.add_argument('-v', '--verbosity',
|
|
|
|
default='INFO',
|
|
|
|
help=('Filter logging output. Possible values:' +
|
2013-12-14 15:08:47 +00:00
|
|
|
' CRITICAL, ERROR, WARNING, INFO, DEBUG'))
|
2013-12-10 22:22:57 +00:00
|
|
|
|
2013-12-17 10:18:35 +00:00
|
|
|
global storage
|
|
|
|
storage = Ledger(app=app)
|
|
|
|
|
2013-12-10 22:22:57 +00:00
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
|
|
|
logging.basicConfig(level=getattr(logging, args.verbosity, 'INFO'))
|
|
|
|
|
2013-12-09 20:04:17 +00:00
|
|
|
app.run(host=app.config['HOST'], port=app.config['PORT'])
|
2013-12-09 19:53:20 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|