Skip to content

Commit

Permalink
Merge pull request inspirehep#9 from vbalbp/use_flask_restful
Browse files Browse the repository at this point in the history
Use flask restful
  • Loading branch information
vbalbp authored Oct 10, 2018
2 parents 7b6d03d + 3a6430e commit 3bef7ab
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 121 deletions.
56 changes: 12 additions & 44 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,26 @@
# -*- coding: utf-8 -*-
#
# This file is part of INSPIRE.
# Copyright (C) 2014-2018 CERN.
#
# INSPIRE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# INSPIRE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with INSPIRE. If not, see <http://www.gnu.org/licenses/>.
#
# In applying this license, CERN does not waive the privileges and immunities
# granted to it by virtue of its status as an Intergovernmental Organization
# or submit itself to any jurisdiction.

sudo: required

language: python

dist: trusty
services:
- docker

python:
- 3.6

env:
global:
- DOCKER_DATA=/tmp/data

before_install:
- travis_retry pip install --upgrade pip setuptools
- travis_retry pip install coveralls

- travis_retry pip install --upgrade pip setuptools
- travis_retry pip install coveralls
install:
- travis_retry pip install -e .[tests]

script:
- chmod +x *.sh
- ./run-tests.sh

- "./run-tests.sh"
deploy:
provider: pypi
user: #
user: inspirehep
password:
secure:
secure: QSPeMz4iQOMwqvLUwWpeJsvsTx7OdhdGVnvDClaMzb9nB2rYcXVY9j9rr/0jg+ocYPcv4yxIa7yxHPf/w0Gv2J3l6A1KWsuk94ZfMnAHvDspytTb4KSt/pqZ/QUoLjUn8SvmnJR3ifZeBi55PFwTpoS/iMI1VuRRi9kFNkNodavct/f8MPMVDvpoqHp8IfA/WSIZ8w0HXLYAia1HI9qVnZzs9OeMfpWVkfPbh2F1VJpDzkFnP9989+21FlpoluKiRAGZzC3kfqNCtPTujXqvsroToe/LR4XEnk27YDdZ4KEXKE8g23/wtou//PZV1Ohd5nz4cPaada7y76mHHhdr76O7OMlaFzY6UZdZe0dqpyfRk5HISQm3/BgjlMSdus8dSc8iakQduX8gHuf1vSDga2aJKespL6lPd46YWAH/y+lZKZ18DEdIEJIBxLcWS8Q1sEE6lUrVnP6ykI5R1FCZoJ7tNKNwaxCcGEqYf1WtYbpcFt5uEij13DErFVDWomauvfXcWN7qPOqLxNfA9IBcPHdm9k+GDVq6AcujcYbMQzi8n40/K5n7D7Ij2xZqp6F1obGE/3GM0t61f6fdeo2dCmcgUjhaiMsftixmi2xLqmUFezvDUBQ92Lq19MiQmFAkX21iH3jaGd/vwFKENGMfw5eD9WDBaMRtCDGA8dTHEKo=
on:
branch: master

after_deploy:
- ./deploy_to_openshift.sh
- "./deploy_to_openshift.sh"
env:
global:
#DEPLOY_TOKEN
- secure: iE2eL+kq5mddntryu73RI1eLSH4yjGSMzm4BmDPasZHJWlzcTn25wUPQpTD+xF5n52DuKzbuEqTYmJVbiHt2gLPkFfRKRg7ZJcjNfzV47sduerwehRqXW7LJHBHtW4Zwsy2i5k4Ha5wDLLHzr3HL7hwu3Mu2TF2VKm2bGx1IIk63RN0HrawHKkoi/biKA88DCsMCXz4LlblvWEARgAorzxF7at+om4BrQaCdrOz3f036dUuC+z0ITDH2hM8HFlNkb+vvyEvYyWExqqpBC7YXe1X86ItoEr+y0Oo2ojtn+meqD1johr9I+kCOeuITAN8AeS+4dnVH4CEHfb6Fa3MzcSwb7nkHNYyEE7xcHXTfLdzUtDc4v15c2dFCH7YcN3btS30k3U+5pYmCWRPZlPxa92aVWB0wD1UYREa304NirnLId6kMJEKeIB6uFzqaFrLKz7RXKRjUARuhYHV1pFMo5f4sYkYx95AbQYJfZQi+HmTk7X3TAlIKz/xutevsiH8nQDEJYIfnRYyEx4fhkwaYw7PN9MjBsd2ww7ssYz03PaWEIJF0Ib+PZA8Z6KPha3Lc+PQqIzaGUfyT+jD0rO86J5E1dq1pPslPpCNNAnGm49H5GInpBSwnfTWdmDoqyEnS8t495uCq1No6WWz4SuGmX0X2x5ugsKikOQQe+sxNpnc=
#DEPLOY_URL
- secure: YBs1O1ghospU7PhRVQgTIkgtRbNG4Fxfe/H1fNWyb7NbIegZ5O9x7zb2vjDuY3M+stpxNTCs6V0m4IxaG/3wJtcEhJBUC1BJ3khN4kaLp+EDaVuI7mHEFnYiPPsRLVGkPDYTH25IpNKl/67XirnydAYeF0TsP1iRhRQyV9u4cu+WwmTMauT1Ye/bXvd4FU6vuUb6uyNcgYNNXHO1rUOWwiwcm2slB7vAu3UBCesGsmqoFK73+uq9dXv6b/b8z7f8ZnvPvBtyRRQk3qbhafLVH3HvWWXdgO27lCqZpKJbeg775WKnsvmmrFZvGKzpZeRo4ZPTNWSWvCz0KjfVQtVGdA9rEwRb/NxD24NEAsuymSDNW5qbKZXQiGGezKxAwTvuMd3OjoNdWU3qUF7+z23vlsxSRG7HB4qKoC401tAM1n2i5ys4YGGxsf1fGvJMGIoBGFGwQR8CyUXUIWLx+qaIksed0m+9VRPIPvXGmvANlfX87ucsRZJrS9VrioejwTvsOKsLf4Exn0/+p0FNcHSeEhBPzO5xGlfuwjnwRDKtlBRbP0VkPNhXwKXMaPP6zjBofIB0Loq0VNMLbGbUh7kMqCpQmdn82E6BjYQnfIyXJiBLXHZgqqOZlbRxHABFxS9e5e27xFMGKgzFEFDwWoeavU/pDywTNbvf/rt+1kqDJ2c=
2 changes: 0 additions & 2 deletions boot.sh

This file was deleted.

2 changes: 1 addition & 1 deletion deploy_to_openshift.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash -xe
curl -X POST "${DEPLOY_URL}" \
-F token=${DEPLOY_TOKEN} \
-F ref=qa \
-F ref=master \
-F "variables[CACHE_DATE]=$(date +%Y-%m-%d:%H:%M:%S)"
63 changes: 28 additions & 35 deletions inspire_classifier/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@

import datetime

from flask import Flask, jsonify, request, Response
from marshmallow.exceptions import ValidationError
from flask import Flask, jsonify, Response
from flask_apispec import use_kwargs, marshal_with, FlaskApiSpec
from marshmallow import fields

from . import serializers
from .domain.models import CoreClassifier
Expand All @@ -41,51 +42,43 @@ def force_type(cls, rv, environ=None):
return super(JsonResponse, cls).force_type(rv, environ)


classifier = CoreClassifier()
Flask.response_class = JsonResponse
app = Flask(__name__)
def create_app():
classifier = CoreClassifier()
Flask.response_class = JsonResponse
app = Flask(__name__)
docs = FlaskApiSpec(app)

@app.route("/api/health")
def date():
"""Basic endpoint that returns the date, used to check if everything is up and working."""
now = datetime.datetime.now()
return jsonify(now)

@app.route("/api/health")
def date():
"""Basic endpoint that returns the date, used to check if everything is up and working"""
now = datetime.datetime.now()
return jsonify(now)
docs.register(date)

@app.route("/api/classifier", methods=["POST"])
@use_kwargs({'title': fields.Str(required=True), 'abstract': fields.Str(required=True)})
@marshal_with(serializers.ClassifierOutputSerializer)
def core_classifier(**kwargs):
"""Endpoint for the CORE classifier."""

@app.route("/api/classifier", methods=["POST"])
def core_classifier():
"""Endpoint for the CORE classifier.
classifier.predict(kwargs['title'], kwargs['abstract'])

Accepts only POST requests, as we have to send data (title and abstract) to the classifier.
return classifier

Returns an array with three float values that correspond to the probability of the record being Rejected, Non-Core and Core."""
docs.register(core_classifier)

input_serializer = serializers.ClassifierInputSerializer()
output_serializer = serializers.ClassifierOutputSerializer()
return app

try:
data = input_serializer.load(request.get_json(force=True))
except ValidationError as exc:
@app.errorhandler(404)
def page_not_found(e):
return {
"errors": [
exc.messages
str(e)
]
}, 400

classifier.predict(data['title'], data['abstract'])

return output_serializer.dump(classifier)


@app.errorhandler(404)
def page_not_found(e):
return {
"errors": [
str(e)
]
}, 404
}, 404


if __name__ == '__main__':
app = create_app()
app.run(host='0.0.0.0')
2 changes: 2 additions & 0 deletions inspire_classifier/domain/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@

class CoreClassifier(object):
def __init__(self):
self.prediction = None
self.score_a = None
self.score_b = None
self.score_c = None

def predict(self, title, abstract):
self.prediction = 'core'
self.score_a = 0.1
self.score_b = 0.2
self.score_c = 0.7
11 changes: 3 additions & 8 deletions inspire_classifier/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,12 @@

from __future__ import absolute_import, division, print_function

from marshmallow import Schema, fields, INCLUDE


class ClassifierInputSerializer(Schema):
class Meta:
unknown = INCLUDE
title = fields.Str(required=True)
abstract = fields.Str(required=True)
from marshmallow import Schema, fields
from marshmallow.validate import OneOf


class ClassifierOutputSerializer(Schema):
prediction = fields.Str(validate=OneOf(['core', 'non-core', 'rejected']), required=True)
score1 = fields.Float(attribute='score_a', required=True)
score2 = fields.Float(attribute='score_b', required=True)
score3 = fields.Float(attribute='score_c', required=True)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

install_requires = [
'Flask~=1.0,>=1.0.2',
'flask-apispec~=0.0,>=0.7.0',
'marshmallow~=3.0.0b13,>=3.0.0b13'
]

Expand Down
43 changes: 43 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# This file is part of INSPIRE.
# Copyright (C) 2014-2017 CERN.
#
# INSPIRE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# INSPIRE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with INSPIRE. If not, see <http://www.gnu.org/licenses/>.
#
# In applying this license, CERN does not waive the privileges and immunities
# granted to it by virtue of its status as an Intergovernmental Organization
# or submit itself to any jurisdiction.

from __future__ import absolute_import, division, print_function

import pytest
from inspire_classifier.app import create_app


@pytest.fixture(autouse=True, scope='session')
def app():
app = create_app()
with app.app_context():
yield app


# TODO: all fixtures using ``app`` must be replaced by ones that use ``isolated_app``.
@pytest.fixture()
def app_client(app):
"""Flask test client for the application.
See: http://flask.pocoo.org/docs/0.12/testing/#keeping-the-context-around.
"""
with app.test_client() as client:
yield client
59 changes: 59 additions & 0 deletions tests/integration/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#
# This file is part of INSPIRE.
# Copyright (C) 2014-2018 CERN.
#
# INSPIRE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# INSPIRE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with INSPIRE. If not, see <http://www.gnu.org/licenses/>.
#
# In applying this license, CERN does not waive the privileges and immunities
# granted to it by virtue of its status as an Intergovernmental Organization
# or submit itself to any jurisdiction.

from __future__ import absolute_import, division, print_function

import json


def test_health_check(app_client):
assert app_client.get('/api/health').status_code == 200


def test_classifier_accepts_only_post(app_client):
assert app_client.post('/api/classifier', data=dict(title='foo bar', abstract='foobar foobar')).status_code == 200
assert app_client.get('/api/classifier').status_code == 405


def test_classifier(app_client):
response = app_client.post('/api/classifier', data=dict(title='foo bar', abstract='foobar foobar'))

result = json.loads(response.data)

expected = {
"prediction": "core",
"score1": 0.1,
"score2": 0.2,
"score3": 0.7,
}

assert response.status_code == 200
assert expected == result


def test_classifier_serializes_input(app_client):
assert app_client.post('/api/classifier', data=dict(title='foo bar')).status_code == 422
assert app_client.post('/api/classifier', data=dict(abstract='foo bar')).status_code == 422


def test_classifier_accepts_extra_fields(app_client):
assert app_client.post('/api/classifier', data=dict(title='foo bar', abstract='foo bar', author='foo')).status_code == 200
Loading

0 comments on commit 3bef7ab

Please sign in to comment.