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)