Move API in views, use Namespace from flask-retplus.

This commit is contained in:
Alexis Lahouze 2017-05-13 10:12:37 +02:00
parent 86d1f942b0
commit a5eeede95e
6 changed files with 124 additions and 117 deletions

View File

@ -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)

View File

@ -14,9 +14,38 @@
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
"""
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()

View File

@ -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('/<int:id>')
@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('/<int:id>/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('/<int:id>/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("/<int:id>/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('/<int:id>/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):
"""

View File

@ -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('/<int:id>')
@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
)

View File

@ -14,14 +14,12 @@
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
"""
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('/<int:id>')
@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.')

View File

@ -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)