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 import Alembic
from flask_alembic.cli.click import cli as alembic_cli 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 .models import db
from .views import api, cors
# The app # The app
app = Flask(__name__, static_folder=None, template_folder=None) app = Flask(__name__, static_folder=None, template_folder=None)
@ -42,6 +41,7 @@ alembic = Alembic(app)
app.cli.add_command(alembic_cli, 'db') app.cli.add_command(alembic_cli, 'db')
# Database initialization. # Database initialization.
@app.cli.command() @app.cli.command()
@click.pass_context @click.pass_context
@ -58,19 +58,6 @@ def initdb(ctx):
#alembic.stamp() #alembic.stamp()
click.echo("Database created.") click.echo("Database created.")
# API initialization.
authorizations = {
'apikey': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization'
}
}
api = Api(app, authorizations=authorizations, prefix='/api') api.init_app(app)
CORS(app) cors.init_app(app)
# Load all views.
# pylint: disable=wildcard-import,wrong-import-position
from .views import * # flake8: noqa

View File

@ -14,9 +14,38 @@
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>. 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__): # API initialization.
__all__.append(module_name) # 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 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 accountant import db
from .. import api
from ..models.accounts import Account from ..models.accounts import Account
from ..models.operations import Operation from ..models.operations import Operation
@ -30,7 +27,8 @@ from ..fields import Object
from .users import requires_auth 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.
account_model = ns.model('Account', { account_model = ns.model('Account', {
@ -136,14 +134,14 @@ range_parser.add_argument(
@ns.route('/') @ns.route('/')
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
401: 'Unauthorized' 401: 'Unauthorized'
}) })
class AccountListResource(Resource): class AccountListResource(Resource):
@requires_auth @requires_auth
@api.response(200, 'OK', [account_model]) @ns.response(200, 'OK', [account_model])
@marshal_with_field(fields.List(Object(account_model))) @marshal_with_field(fields.List(Object(account_model)))
def get(self): def get(self):
""" """
@ -152,19 +150,19 @@ class AccountListResource(Resource):
return Account.query().all(), 200 return Account.query().all(), 200
@requires_auth @requires_auth
@api.expect(account_model) @ns.expect(account_model)
@api.response(201, 'Account created', account_model) @ns.response(201, 'Account created', account_model)
@api.response(406, 'Invalid account data') @ns.response(406, 'Invalid account data')
@marshal_with_field(Object(account_model)) @marshal_with_field(Object(account_model))
def post(self): def post(self):
""" """
Create a new account. Create a new account.
""" """
data = api.payload data = ns.apis[0].payload
# A new account MUST NOT have an id; # A new account MUST NOT have an id;
if 'id' in data and data['id']: if 'id' in data and data['id']:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided on creation.' error_message='Id must not be provided on creation.'
) )
@ -183,7 +181,7 @@ class AccountListResource(Resource):
@ns.route('/<int:id>') @ns.route('/<int:id>')
@api.doc( @ns.doc(
security='apikey', security='apikey',
params={ params={
'id': 'Id of the account to manage' 'id': 'Id of the account to manage'
@ -194,7 +192,7 @@ class AccountListResource(Resource):
}) })
class AccountResource(Resource): class AccountResource(Resource):
@requires_auth @requires_auth
@api.response(200, 'OK', account_model) @ns.response(200, 'OK', account_model)
@marshal_with_field(Object(account_model)) @marshal_with_field(Object(account_model))
def get(self, id): def get(self, id):
""" """
@ -203,7 +201,7 @@ class AccountResource(Resource):
account = Account.query().get(id) account = Account.query().get(id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % id error_message='Account with id %d not found.' % id
) )
@ -213,19 +211,19 @@ class AccountResource(Resource):
return account, 200 return account, 200
@requires_auth @requires_auth
@api.expect(account_model) @ns.expect(account_model)
@api.response(200, 'OK', account_model) @ns.response(200, 'OK', account_model)
@api.response(406, 'Invalid account data') @ns.response(406, 'Invalid account data')
@marshal_with_field(Object(account_model)) @marshal_with_field(Object(account_model))
def post(self, id): def post(self, id):
""" """
Update an account. Update an account.
""" """
data = api.payload data = ns.apis[0].payload
# Check ID consistency. # Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id: if 'id' in data and data['id'] and data['id'] != id:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided or changed on update.' error_message='Id must not be provided or changed on update.'
) )
@ -234,7 +232,7 @@ class AccountResource(Resource):
account = Account.query().get(id) account = Account.query().get(id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % id error_message='Account with id %d not found.' % id
) )
@ -249,7 +247,7 @@ class AccountResource(Resource):
return account, 200 return account, 200
@requires_auth @requires_auth
@api.response(204, 'Account deleted', account_model) @ns.response(204, 'Account deleted', account_model)
@marshal_with_field(Object(account_model)) @marshal_with_field(Object(account_model))
def delete(self, id): def delete(self, id):
""" """
@ -260,7 +258,7 @@ class AccountResource(Resource):
account = Account.query().get(id) account = Account.query().get(id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % id error_message='Account with id %d not found.' % id
) )
@ -273,7 +271,7 @@ class AccountResource(Resource):
@ns.route('/<int:id>/solds') @ns.route('/<int:id>/solds')
class SoldsResource(Resource): class SoldsResource(Resource):
@requires_auth @requires_auth
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
200: ('OK', solds_model), 200: ('OK', solds_model),
@ -288,7 +286,7 @@ class SoldsResource(Resource):
account = Account.query().get(id) account = Account.query().get(id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % id error_message='Account with id %d not found.' % id
) )
@ -301,14 +299,14 @@ class SoldsResource(Resource):
@ns.route('/<int:id>/balance') @ns.route('/<int:id>/balance')
class BalanceResource(Resource): class BalanceResource(Resource):
@requires_auth @requires_auth
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
200: ('OK', balance_model), 200: ('OK', balance_model),
401: 'Unauthorized', 401: 'Unauthorized',
404: 'Account not found' 404: 'Account not found'
}) })
@api.expect(range_parser) @ns.expect(range_parser)
@marshal_with_field(Object(balance_model)) @marshal_with_field(Object(balance_model))
def get(self, id): def get(self, id):
""" """
@ -317,7 +315,7 @@ class BalanceResource(Resource):
account = Account.query().get(id) account = Account.query().get(id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % id error_message='Account with id %d not found.' % id
) )
@ -332,14 +330,14 @@ class BalanceResource(Resource):
@ns.route("/<int:id>/category") @ns.route("/<int:id>/category")
class CategoryResource(Resource): class CategoryResource(Resource):
@requires_auth @requires_auth
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
200: ('OK', [category_model]), 200: ('OK', [category_model]),
401: 'Unauthorized', 401: 'Unauthorized',
404: 'Account not found' 404: 'Account not found'
}) })
@api.expect(range_parser) @ns.expect(range_parser)
@marshal_with_field(fields.List(Object(category_model))) @marshal_with_field(fields.List(Object(category_model)))
def get(self, id): def get(self, id):
""" """
@ -353,14 +351,14 @@ class CategoryResource(Resource):
@ns.route('/<int:id>/ohlc') @ns.route('/<int:id>/ohlc')
class OHLCResource(Resource): class OHLCResource(Resource):
@requires_auth @requires_auth
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
200: ('OK', [ohlc_model]), 200: ('OK', [ohlc_model]),
401: 'Unauthorized', 401: 'Unauthorized',
404: 'Account not found' 404: 'Account not found'
}) })
@api.expect(range_parser) @ns.expect(range_parser)
@marshal_with_field(fields.List(Object(ohlc_model))) @marshal_with_field(fields.List(Object(ohlc_model)))
def get(self, id): def get(self, id):
""" """

View File

@ -16,12 +16,9 @@
""" """
import dateutil.parser 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 accountant import db
from .. import api
from ..models.accounts import Account from ..models.accounts import Account
from ..models.operations import Operation from ..models.operations import Operation
@ -30,7 +27,8 @@ from .users import requires_auth
from ..fields import Object 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 with sold model.
operation_model = ns.model('Operation', { operation_model = ns.model('Operation', {
@ -106,15 +104,15 @@ account_range_parser.add_argument(
@ns.route('/') @ns.route('/')
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
401: 'Unauthorized' 401: 'Unauthorized'
}) })
class OperationListResource(Resource): class OperationListResource(Resource):
@requires_auth @requires_auth
@api.response(200, 'OK', [operation_with_sold_model]) @ns.response(200, 'OK', [operation_with_sold_model])
@api.expect(parser=account_range_parser) @ns.expect(parser=account_range_parser)
@marshal_with_field(fields.List(Object(operation_with_sold_model))) @marshal_with_field(fields.List(Object(operation_with_sold_model)))
def get(self): def get(self):
""" """
@ -130,28 +128,28 @@ class OperationListResource(Resource):
).all(), 200 ).all(), 200
@requires_auth @requires_auth
@api.response(201, 'Operation created', operation_model) @ns.response(201, 'Operation created', operation_model)
@api.response(404, 'Account not found') @ns.response(404, 'Account not found')
@api.response(406, 'Invalid operation data') @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(operation_model)) @marshal_with_field(Object(operation_model))
def post(self): def post(self):
""" """
Create a new operation. Create a new operation.
""" """
data = api.payload data = ns.apis[0].payload
account_id = data['account_id'] account_id = data['account_id']
account = Account.query().get(account_id) account = Account.query().get(account_id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % account_id error_message='Account with id %d not found.' % account_id
) )
# A new operation MUST NOT have an id; # A new operation MUST NOT have an id;
if 'id' in data and data['id']: if 'id' in data and data['id']:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided on creation.' error_message='Id must not be provided on creation.'
) )
@ -164,7 +162,7 @@ class OperationListResource(Resource):
@ns.route('/<int:id>') @ns.route('/<int:id>')
@api.doc( @ns.doc(
security='apikey', security='apikey',
params={ params={
'id': 'Id of the operation to manage' 'id': 'Id of the operation to manage'
@ -175,7 +173,7 @@ class OperationListResource(Resource):
}) })
class OperationResource(Resource): class OperationResource(Resource):
@requires_auth @requires_auth
@api.response(200, 'OK', operation_model) @ns.response(200, 'OK', operation_model)
@marshal_with_field(Object(operation_model)) @marshal_with_field(Object(operation_model))
def get(self, id): def get(self, id):
""" """
@ -184,7 +182,7 @@ class OperationResource(Resource):
operation = db.session.query(Operation).get(id) operation = db.session.query(Operation).get(id)
if not operation: if not operation:
api.abort( ns.abort(
404, 404,
error_message='Operation with id %d not found.' % id error_message='Operation with id %d not found.' % id
) )
@ -192,16 +190,16 @@ class OperationResource(Resource):
return operation, 200 return operation, 200
@requires_auth @requires_auth
@api.expect(operation_model) @ns.expect(operation_model)
@api.response(200, 'OK', operation_model) @ns.response(200, 'OK', operation_model)
@api.response(406, 'Invalid operation data') @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(operation_model)) @marshal_with_field(Object(operation_model))
def post(self, id): def post(self, id):
data = api.payload data = ns.apis[0].payload
# Check ID consistency. # Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id: if 'id' in data and data['id'] and data['id'] != id:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided or changed on update.' 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) operation = db.session.query(Operation).get(id)
if not operation: if not operation:
api.abort( ns.abort(
404, 404,
error_message='Operation with id %d not found.' % id error_message='Operation with id %d not found.' % id
) )
@ -225,13 +223,13 @@ class OperationResource(Resource):
return operation, 200 return operation, 200
@requires_auth @requires_auth
@api.response(204, 'Operation deleted', operation_model) @ns.response(204, 'Operation deleted', operation_model)
@marshal_with_field(Object(operation_model)) @marshal_with_field(Object(operation_model))
def delete(self, id): def delete(self, id):
operation = db.session.query(Operation).get(id) operation = db.session.query(Operation).get(id)
if not operation: if not operation:
api.abort( ns.abort(
404, 404,
error_message='Operation with id %d not found.' % id 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 You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>. 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 sqlalchemy import true
from accountant import db from accountant import db
from .. import api
from ..models.accounts import Account from ..models.accounts import Account
from ..models.operations import Operation from ..models.operations import Operation
from ..models.scheduled_operations import ScheduledOperation from ..models.scheduled_operations import ScheduledOperation
@ -31,7 +29,7 @@ from .users import requires_auth
from ..fields import Object from ..fields import Object
ns = api.namespace( ns = Namespace(
'scheduled_operation', 'scheduled_operation',
description='Scheduled operation management' description='Scheduled operation management'
) )
@ -84,15 +82,15 @@ account_id_parser.add_argument(
@ns.route('/') @ns.route('/')
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
401: 'Unauthorized', 401: 'Unauthorized',
}) })
class ScheduledOperationListResource(Resource): class ScheduledOperationListResource(Resource):
@requires_auth @requires_auth
@api.expect(account_id_parser) @ns.expect(account_id_parser)
@api.response(200, 'OK', [scheduled_operation_model]) @ns.response(200, 'OK', [scheduled_operation_model])
@marshal_with_field(fields.List(Object(scheduled_operation_model))) @marshal_with_field(fields.List(Object(scheduled_operation_model)))
def get(self): def get(self):
""" """
@ -103,29 +101,29 @@ class ScheduledOperationListResource(Resource):
return ScheduledOperation.query().filter_by(**data).all(), 200 return ScheduledOperation.query().filter_by(**data).all(), 200
@requires_auth @requires_auth
@api.expect(scheduled_operation_model) @ns.expect(scheduled_operation_model)
@api.response(200, 'OK', scheduled_operation_model) @ns.response(200, 'OK', scheduled_operation_model)
@api.response(404, 'Account not found') @ns.response(404, 'Account not found')
@api.response(406, 'Invalid operation data') @ns.response(406, 'Invalid operation data')
@marshal_with_field(Object(scheduled_operation_model)) @marshal_with_field(Object(scheduled_operation_model))
def post(self): def post(self):
""" """
Add a new scheduled operation. Add a new scheduled operation.
""" """
data = api.payload data = ns.apis[0].payload
account_id = data['account_id'] account_id = data['account_id']
account = Account.query().get(account_id) account = Account.query().get(account_id)
if not account: if not account:
api.abort( ns.abort(
404, 404,
error_message='Account with id %d not found.' % account_id error_message='Account with id %d not found.' % account_id
) )
# A new scheduled operation MUST NOT have an id; # A new scheduled operation MUST NOT have an id;
if 'id' in data and data['id']: if 'id' in data and data['id']:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided on creation.' error_message='Id must not be provided on creation.'
) )
@ -142,7 +140,7 @@ class ScheduledOperationListResource(Resource):
@ns.route('/<int:id>') @ns.route('/<int:id>')
@api.doc( @ns.doc(
security='apikey', security='apikey',
params={ params={
'id': 'Id of the scheduled operation to manage' 'id': 'Id of the scheduled operation to manage'
@ -153,7 +151,7 @@ class ScheduledOperationListResource(Resource):
}) })
class ScheduledOperationResource(Resource): class ScheduledOperationResource(Resource):
@requires_auth @requires_auth
@api.response(200, 'OK', scheduled_operation_model) @ns.response(200, 'OK', scheduled_operation_model)
@marshal_with_field(Object(scheduled_operation_model)) @marshal_with_field(Object(scheduled_operation_model))
def get(self, id): def get(self, id):
""" """
@ -162,7 +160,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id) scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation: if not scheduled_operation:
api.abort( ns.abort(
404, 404,
error_message='Scheduled operation with id %d not found.' % id error_message='Scheduled operation with id %d not found.' % id
) )
@ -170,19 +168,19 @@ class ScheduledOperationResource(Resource):
return scheduled_operation, 200 return scheduled_operation, 200
@requires_auth @requires_auth
@api.response(200, 'OK', scheduled_operation_model) @ns.response(200, 'OK', scheduled_operation_model)
@api.response(406, 'Invalid scheduled operation data') @ns.response(406, 'Invalid scheduled operation data')
@api.expect(scheduled_operation_model) @ns.expect(scheduled_operation_model)
@marshal_with_field(Object(scheduled_operation_model)) @marshal_with_field(Object(scheduled_operation_model))
def post(self, id): def post(self, id):
""" """
Update a scheduled operation. Update a scheduled operation.
""" """
data = api.payload data = ns.apis[0].payload
# Check ID consistency. # Check ID consistency.
if 'id' in data and data['id'] and data['id'] != id: if 'id' in data and data['id'] and data['id'] != id:
api.abort( ns.abort(
406, 406,
error_message='Id must not be provided or changed on update.' error_message='Id must not be provided or changed on update.'
) )
@ -190,7 +188,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id) scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation: if not scheduled_operation:
api.abort( ns.abort(
404, 404,
error_message='Scheduled operation with id %d not found.' % id error_message='Scheduled operation with id %d not found.' % id
) )
@ -210,8 +208,8 @@ class ScheduledOperationResource(Resource):
return scheduled_operation, 200 return scheduled_operation, 200
@requires_auth @requires_auth
@api.response(200, 'OK', scheduled_operation_model) @ns.response(200, 'OK', scheduled_operation_model)
@api.response(409, 'Cannot be deleted') @ns.response(409, 'Cannot be deleted')
@marshal_with_field(Object(scheduled_operation_model)) @marshal_with_field(Object(scheduled_operation_model))
def delete(self, id): def delete(self, id):
""" """
@ -220,7 +218,7 @@ class ScheduledOperationResource(Resource):
scheduled_operation = ScheduledOperation.query().get(id) scheduled_operation = ScheduledOperation.query().get(id)
if not scheduled_operation: if not scheduled_operation:
api.abort( ns.abort(
404, 404,
error_message='Scheduled operation with id %d not found.' % id error_message='Scheduled operation with id %d not found.' % id
) )
@ -230,7 +228,7 @@ class ScheduledOperationResource(Resource):
).count() ).count()
if operations: if operations:
api.abort( ns.abort(
409, 409,
error_message='There are still confirmed operations \ error_message='There are still confirmed operations \
associated to this scheduled operation.') associated to this scheduled operation.')

View File

@ -22,16 +22,13 @@ import arrow
from functools import wraps from functools import wraps
from flask import request, g 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 ..fields import Object
from ..models.users import User from ..models.users import User
from .models import user_model, token_model, login_model
from accountant import db from accountant import db
@ -76,14 +73,14 @@ def requires_auth(f):
g.user = user g.user = user
return f(*args, **data) return f(*args, **data)
api.abort( ns.abort(
401, 401,
error_message='Please login before executing this request.' error_message='Please login before executing this request.'
) )
return wrapped return wrapped
ns = api.namespace('user', description='User management') ns = Namespace('user', description='User management')
# Token with expiration time and type. # Token with expiration time and type.
token_model = ns.model('Token', { token_model = ns.model('Token', {
@ -134,25 +131,25 @@ login_model = ns.model('Login', {
@ns.route('/login') @ns.route('/login')
class LoginResource(Resource): class LoginResource(Resource):
@api.marshal_with(token_model) @ns.marshal_with(token_model)
@api.doc( @ns.doc(
responses={ responses={
200: ('OK', token_model), 200: ('OK', token_model),
401: 'Unauthorized' 401: 'Unauthorized'
}) })
@api.expect(login_model) @ns.expect(login_model)
def post(self): def post(self):
""" """
Login to retrieve authentication token. Login to retrieve authentication token.
""" """
data = api.payload data = ns.apis[0].payload
user = User.query().filter( user = User.query().filter(
User.email == data['email'] User.email == data['email']
).one_or_none() ).one_or_none()
if not user or not user.verify_password(data['password']): 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() token = user.generate_auth_token()
expiration_time = arrow.now().replace( expiration_time = arrow.now().replace(
@ -166,7 +163,7 @@ class LoginResource(Resource):
}, 200 }, 200
@requires_auth @requires_auth
@api.doc( @ns.doc(
security='apikey', security='apikey',
responses={ responses={
200: ('OK', user_model) 200: ('OK', user_model)