diff --git a/accountant/__init__.py b/accountant/__init__.py
index e9ce411..02789c1 100644
--- a/accountant/__init__.py
+++ b/accountant/__init__.py
@@ -20,10 +20,9 @@ from flask import Flask
from flask_alembic import Alembic
from flask_alembic.cli.click import cli as alembic_cli
-from flask_restplus import Api
-from flask_cors import CORS
from .models import db
+from .views import api, cors
# The app
app = Flask(__name__, static_folder=None, template_folder=None)
@@ -42,6 +41,7 @@ alembic = Alembic(app)
app.cli.add_command(alembic_cli, 'db')
+
# Database initialization.
@app.cli.command()
@click.pass_context
@@ -58,19 +58,6 @@ def initdb(ctx):
#alembic.stamp()
click.echo("Database created.")
-# API initialization.
-authorizations = {
- 'apikey': {
- 'type': 'apiKey',
- 'in': 'header',
- 'name': 'Authorization'
- }
-}
-api = Api(app, authorizations=authorizations, prefix='/api')
-CORS(app)
-
-
-# Load all views.
-# pylint: disable=wildcard-import,wrong-import-position
-from .views import * # flake8: noqa
+api.init_app(app)
+cors.init_app(app)
diff --git a/accountant/views/__init__.py b/accountant/views/__init__.py
index 43a4ef3..07ca4cf 100644
--- a/accountant/views/__init__.py
+++ b/accountant/views/__init__.py
@@ -14,9 +14,38 @@
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see .
"""
-import pkgutil
+from flask_cors import CORS
+from flask_restplus import Api
-__all__ = []
+from .accounts import ns as accounts_ns
+from .operations import ns as operations_ns
+from .scheduled_operations import ns as scheduled_operations_ns
+from .users import ns as users_ns
-for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
- __all__.append(module_name)
+# API initialization.
+# pylint: disable=invalid-name
+authorizations = {
+ 'apikey': {
+ 'type': 'apiKey',
+ 'in': 'header',
+ 'name': 'Authorization',
+ },
+}
+
+# pylint: disable=invalid-name
+api = Api(
+ title='Accountant API',
+ version='1.0',
+ description='This is the Accountant API.',
+ authorizations=authorizations,
+ prefix='/api'
+)
+
+
+api.add_namespace(accounts_ns)
+api.add_namespace(operations_ns)
+api.add_namespace(scheduled_operations_ns)
+api.add_namespace(users_ns)
+
+# pylint: disable=invalid-name
+cors = CORS()
diff --git a/accountant/views/accounts.py b/accountant/views/accounts.py
index efe1225..f3f4459 100644
--- a/accountant/views/accounts.py
+++ b/accountant/views/accounts.py
@@ -16,12 +16,9 @@
"""
import dateutil.parser
-from flask_restplus import Resource, fields, marshal_with_field
-
+from flask_restplus import Namespace, Resource, fields, marshal_with_field
from accountant import db
-from .. import api
-
from ..models.accounts import Account
from ..models.operations import Operation
@@ -30,7 +27,8 @@ from ..fields import Object
from .users import requires_auth
-ns = api.namespace('account', description='Account management')
+# pylint: disable=invalid-name
+ns = Namespace('account', description='Account management')
# Account model.
account_model = ns.model('Account', {
@@ -136,14 +134,14 @@ range_parser.add_argument(
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
responses={
401: 'Unauthorized'
})
class AccountListResource(Resource):
@requires_auth
- @api.response(200, 'OK', [account_model])
+ @ns.response(200, 'OK', [account_model])
@marshal_with_field(fields.List(Object(account_model)))
def get(self):
"""
@@ -152,19 +150,19 @@ class AccountListResource(Resource):
return Account.query().all(), 200
@requires_auth
- @api.expect(account_model)
- @api.response(201, 'Account created', account_model)
- @api.response(406, 'Invalid account data')
+ @ns.expect(account_model)
+ @ns.response(201, 'Account created', account_model)
+ @ns.response(406, 'Invalid account data')
@marshal_with_field(Object(account_model))
def post(self):
"""
Create a new account.
"""
- data = api.payload
+ data = ns.apis[0].payload
# A new account MUST NOT have an id;
if 'id' in data and data['id']:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided on creation.'
)
@@ -183,7 +181,7 @@ class AccountListResource(Resource):
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
params={
'id': 'Id of the account to manage'
@@ -194,7 +192,7 @@ class AccountListResource(Resource):
})
class AccountResource(Resource):
@requires_auth
- @api.response(200, 'OK', account_model)
+ @ns.response(200, 'OK', account_model)
@marshal_with_field(Object(account_model))
def get(self, id):
"""
@@ -203,7 +201,7 @@ class AccountResource(Resource):
account = Account.query().get(id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % id
)
@@ -213,19 +211,19 @@ class AccountResource(Resource):
return account, 200
@requires_auth
- @api.expect(account_model)
- @api.response(200, 'OK', account_model)
- @api.response(406, 'Invalid account data')
+ @ns.expect(account_model)
+ @ns.response(200, 'OK', account_model)
+ @ns.response(406, 'Invalid account data')
@marshal_with_field(Object(account_model))
def post(self, id):
"""
Update an account.
"""
- data = api.payload
+ data = ns.apis[0].payload
# Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided or changed on update.'
)
@@ -234,7 +232,7 @@ class AccountResource(Resource):
account = Account.query().get(id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % id
)
@@ -249,7 +247,7 @@ class AccountResource(Resource):
return account, 200
@requires_auth
- @api.response(204, 'Account deleted', account_model)
+ @ns.response(204, 'Account deleted', account_model)
@marshal_with_field(Object(account_model))
def delete(self, id):
"""
@@ -260,7 +258,7 @@ class AccountResource(Resource):
account = Account.query().get(id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % id
)
@@ -273,7 +271,7 @@ class AccountResource(Resource):
@ns.route('//solds')
class SoldsResource(Resource):
@requires_auth
- @api.doc(
+ @ns.doc(
security='apikey',
responses={
200: ('OK', solds_model),
@@ -288,7 +286,7 @@ class SoldsResource(Resource):
account = Account.query().get(id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % id
)
@@ -301,14 +299,14 @@ class SoldsResource(Resource):
@ns.route('//balance')
class BalanceResource(Resource):
@requires_auth
- @api.doc(
+ @ns.doc(
security='apikey',
responses={
200: ('OK', balance_model),
401: 'Unauthorized',
404: 'Account not found'
})
- @api.expect(range_parser)
+ @ns.expect(range_parser)
@marshal_with_field(Object(balance_model))
def get(self, id):
"""
@@ -317,7 +315,7 @@ class BalanceResource(Resource):
account = Account.query().get(id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % id
)
@@ -332,14 +330,14 @@ class BalanceResource(Resource):
@ns.route("//category")
class CategoryResource(Resource):
@requires_auth
- @api.doc(
+ @ns.doc(
security='apikey',
responses={
200: ('OK', [category_model]),
401: 'Unauthorized',
404: 'Account not found'
})
- @api.expect(range_parser)
+ @ns.expect(range_parser)
@marshal_with_field(fields.List(Object(category_model)))
def get(self, id):
"""
@@ -353,14 +351,14 @@ class CategoryResource(Resource):
@ns.route('//ohlc')
class OHLCResource(Resource):
@requires_auth
- @api.doc(
+ @ns.doc(
security='apikey',
responses={
200: ('OK', [ohlc_model]),
401: 'Unauthorized',
404: 'Account not found'
})
- @api.expect(range_parser)
+ @ns.expect(range_parser)
@marshal_with_field(fields.List(Object(ohlc_model)))
def get(self, id):
"""
diff --git a/accountant/views/operations.py b/accountant/views/operations.py
index 87c95c3..7052f88 100644
--- a/accountant/views/operations.py
+++ b/accountant/views/operations.py
@@ -16,12 +16,9 @@
"""
import dateutil.parser
-from flask_restplus import Resource, fields, marshal_with_field
-
+from flask_restplus import Namespace, Resource, fields, marshal_with_field
from accountant import db
-from .. import api
-
from ..models.accounts import Account
from ..models.operations import Operation
@@ -30,7 +27,8 @@ from .users import requires_auth
from ..fields import Object
-ns = api.namespace('operation', description='Operation management')
+# pylint: disable=invalid-name
+ns = Namespace('operation', description='Operation management')
# Operation with sold model.
operation_model = ns.model('Operation', {
@@ -106,15 +104,15 @@ account_range_parser.add_argument(
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
responses={
401: 'Unauthorized'
})
class OperationListResource(Resource):
@requires_auth
- @api.response(200, 'OK', [operation_with_sold_model])
- @api.expect(parser=account_range_parser)
+ @ns.response(200, 'OK', [operation_with_sold_model])
+ @ns.expect(parser=account_range_parser)
@marshal_with_field(fields.List(Object(operation_with_sold_model)))
def get(self):
"""
@@ -130,28 +128,28 @@ class OperationListResource(Resource):
).all(), 200
@requires_auth
- @api.response(201, 'Operation created', operation_model)
- @api.response(404, 'Account not found')
- @api.response(406, 'Invalid operation data')
+ @ns.response(201, 'Operation created', operation_model)
+ @ns.response(404, 'Account not found')
+ @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(operation_model))
def post(self):
"""
Create a new operation.
"""
- data = api.payload
+ data = ns.apis[0].payload
account_id = data['account_id']
account = Account.query().get(account_id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % account_id
)
# A new operation MUST NOT have an id;
if 'id' in data and data['id']:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided on creation.'
)
@@ -164,7 +162,7 @@ class OperationListResource(Resource):
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
params={
'id': 'Id of the operation to manage'
@@ -175,7 +173,7 @@ class OperationListResource(Resource):
})
class OperationResource(Resource):
@requires_auth
- @api.response(200, 'OK', operation_model)
+ @ns.response(200, 'OK', operation_model)
@marshal_with_field(Object(operation_model))
def get(self, id):
"""
@@ -184,7 +182,7 @@ class OperationResource(Resource):
operation = db.session.query(Operation).get(id)
if not operation:
- api.abort(
+ ns.abort(
404,
error_message='Operation with id %d not found.' % id
)
@@ -192,16 +190,16 @@ class OperationResource(Resource):
return operation, 200
@requires_auth
- @api.expect(operation_model)
- @api.response(200, 'OK', operation_model)
- @api.response(406, 'Invalid operation data')
+ @ns.expect(operation_model)
+ @ns.response(200, 'OK', operation_model)
+ @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(operation_model))
def post(self, id):
- data = api.payload
+ data = ns.apis[0].payload
# Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided or changed on update.'
)
@@ -209,7 +207,7 @@ class OperationResource(Resource):
operation = db.session.query(Operation).get(id)
if not operation:
- api.abort(
+ ns.abort(
404,
error_message='Operation with id %d not found.' % id
)
@@ -225,13 +223,13 @@ class OperationResource(Resource):
return operation, 200
@requires_auth
- @api.response(204, 'Operation deleted', operation_model)
+ @ns.response(204, 'Operation deleted', operation_model)
@marshal_with_field(Object(operation_model))
def delete(self, id):
operation = db.session.query(Operation).get(id)
if not operation:
- api.abort(
+ ns.abort(
404,
error_message='Operation with id %d not found.' % id
)
diff --git a/accountant/views/scheduled_operations.py b/accountant/views/scheduled_operations.py
index a6fd85d..0b2fafd 100644
--- a/accountant/views/scheduled_operations.py
+++ b/accountant/views/scheduled_operations.py
@@ -14,14 +14,12 @@
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see .
"""
-from flask_restplus import Resource, fields, marshal_with_field
+from flask_restplus import Namespace, Resource, fields, marshal_with_field
from sqlalchemy import true
from accountant import db
-from .. import api
-
from ..models.accounts import Account
from ..models.operations import Operation
from ..models.scheduled_operations import ScheduledOperation
@@ -31,7 +29,7 @@ from .users import requires_auth
from ..fields import Object
-ns = api.namespace(
+ns = Namespace(
'scheduled_operation',
description='Scheduled operation management'
)
@@ -84,15 +82,15 @@ account_id_parser.add_argument(
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
responses={
401: 'Unauthorized',
})
class ScheduledOperationListResource(Resource):
@requires_auth
- @api.expect(account_id_parser)
- @api.response(200, 'OK', [scheduled_operation_model])
+ @ns.expect(account_id_parser)
+ @ns.response(200, 'OK', [scheduled_operation_model])
@marshal_with_field(fields.List(Object(scheduled_operation_model)))
def get(self):
"""
@@ -103,29 +101,29 @@ class ScheduledOperationListResource(Resource):
return ScheduledOperation.query().filter_by(**data).all(), 200
@requires_auth
- @api.expect(scheduled_operation_model)
- @api.response(200, 'OK', scheduled_operation_model)
- @api.response(404, 'Account not found')
- @api.response(406, 'Invalid operation data')
+ @ns.expect(scheduled_operation_model)
+ @ns.response(200, 'OK', scheduled_operation_model)
+ @ns.response(404, 'Account not found')
+ @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(scheduled_operation_model))
def post(self):
"""
Add a new scheduled operation.
"""
- data = api.payload
+ data = ns.apis[0].payload
account_id = data['account_id']
account = Account.query().get(account_id)
if not account:
- api.abort(
+ ns.abort(
404,
error_message='Account with id %d not found.' % account_id
)
# A new scheduled operation MUST NOT have an id;
if 'id' in data and data['id']:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided on creation.'
)
@@ -142,7 +140,7 @@ class ScheduledOperationListResource(Resource):
@ns.route('/')
-@api.doc(
+@ns.doc(
security='apikey',
params={
'id': 'Id of the scheduled operation to manage'
@@ -153,7 +151,7 @@ class ScheduledOperationListResource(Resource):
})
class ScheduledOperationResource(Resource):
@requires_auth
- @api.response(200, 'OK', scheduled_operation_model)
+ @ns.response(200, 'OK', scheduled_operation_model)
@marshal_with_field(Object(scheduled_operation_model))
def get(self, id):
"""
@@ -162,7 +160,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation:
- api.abort(
+ ns.abort(
404,
error_message='Scheduled operation with id %d not found.' % id
)
@@ -170,19 +168,19 @@ class ScheduledOperationResource(Resource):
return scheduled_operation, 200
@requires_auth
- @api.response(200, 'OK', scheduled_operation_model)
- @api.response(406, 'Invalid scheduled operation data')
- @api.expect(scheduled_operation_model)
+ @ns.response(200, 'OK', scheduled_operation_model)
+ @ns.response(406, 'Invalid scheduled operation data')
+ @ns.expect(scheduled_operation_model)
@marshal_with_field(Object(scheduled_operation_model))
def post(self, id):
"""
Update a scheduled operation.
"""
- data = api.payload
+ data = ns.apis[0].payload
# Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id:
- api.abort(
+ ns.abort(
406,
error_message='Id must not be provided or changed on update.'
)
@@ -190,7 +188,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation:
- api.abort(
+ ns.abort(
404,
error_message='Scheduled operation with id %d not found.' % id
)
@@ -210,8 +208,8 @@ class ScheduledOperationResource(Resource):
return scheduled_operation, 200
@requires_auth
- @api.response(200, 'OK', scheduled_operation_model)
- @api.response(409, 'Cannot be deleted')
+ @ns.response(200, 'OK', scheduled_operation_model)
+ @ns.response(409, 'Cannot be deleted')
@marshal_with_field(Object(scheduled_operation_model))
def delete(self, id):
"""
@@ -220,7 +218,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation:
- api.abort(
+ ns.abort(
404,
error_message='Scheduled operation with id %d not found.' % id
)
@@ -230,7 +228,7 @@ class ScheduledOperationResource(Resource):
).count()
if operations:
- api.abort(
+ ns.abort(
409,
error_message='There are still confirmed operations \
associated to this scheduled operation.')
diff --git a/accountant/views/users.py b/accountant/views/users.py
index adb140b..d4f7c7d 100644
--- a/accountant/views/users.py
+++ b/accountant/views/users.py
@@ -22,16 +22,13 @@ import arrow
from functools import wraps
from flask import request, g
-from flask_restplus import Resource, fields, marshal_with_field
-from .. import app, api
+from flask_restplus import Namespace, Resource, fields, marshal_with_field
from ..fields import Object
from ..models.users import User
-from .models import user_model, token_model, login_model
-
from accountant import db
@@ -76,14 +73,14 @@ def requires_auth(f):
g.user = user
return f(*args, **data)
- api.abort(
+ ns.abort(
401,
error_message='Please login before executing this request.'
)
return wrapped
-ns = api.namespace('user', description='User management')
+ns = Namespace('user', description='User management')
# Token with expiration time and type.
token_model = ns.model('Token', {
@@ -134,25 +131,25 @@ login_model = ns.model('Login', {
@ns.route('/login')
class LoginResource(Resource):
- @api.marshal_with(token_model)
- @api.doc(
+ @ns.marshal_with(token_model)
+ @ns.doc(
responses={
200: ('OK', token_model),
401: 'Unauthorized'
})
- @api.expect(login_model)
+ @ns.expect(login_model)
def post(self):
"""
Login to retrieve authentication token.
"""
- data = api.payload
+ data = ns.apis[0].payload
user = User.query().filter(
User.email == data['email']
).one_or_none()
if not user or not user.verify_password(data['password']):
- api.abort(401, error_message="Bad user or password.")
+ ns.abort(401, error_message="Bad user or password.")
token = user.generate_auth_token()
expiration_time = arrow.now().replace(
@@ -166,7 +163,7 @@ class LoginResource(Resource):
}, 200
@requires_auth
- @api.doc(
+ @ns.doc(
security='apikey',
responses={
200: ('OK', user_model)