Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e9b6e3f
chore(models): add public_sharing attribute to applications model
brionmario Apr 18, 2019
0d3ed7e
chore(models): change questionnaire marshalling
brionmario Apr 18, 2019
964c509
refactor(routes): remove unwanted imports
brionmario Apr 18, 2019
8b56aa4
chore(migrations): update migrations :alembic:
brionmario Apr 18, 2019
53b521c
fix(models): fix data type issues in models :bug:
brionmario Apr 18, 2019
1c1ed8f
chore(migrations): update migrations :alembic:
brionmario Apr 18, 2019
68d52b9
Merge pull request #8 from brionmario/feature/applications
brionmario Apr 18, 2019
a4ca3cc
chore(models): update session model schema
brionmario Apr 18, 2019
56b39e6
fix(models): fix minor validation issues in models
brionmario Apr 18, 2019
e7d2a4d
refactor(routes): remove initial validation in application GET
brionmario Apr 18, 2019
b6147eb
chore(routes): change questionnaire attrib in session POST :boom:
brionmario Apr 18, 2019
4ad8a7c
chore(migrations): update migrations :alembic:
brionmario Apr 18, 2019
760d67b
Merge pull request #9 from brionmario/feature/sessions
brionmario Apr 18, 2019
d23ff41
feat(sessions): add session status recording ability :sparkles:
brionmario Apr 18, 2019
ea83425
chore(migrations): update migrations :alembic:
brionmario Apr 18, 2019
ec94d60
Merge pull request #10 from brionmario/feature/sessions
brionmario Apr 18, 2019
deb693d
chore(app): add socket io and celery init logic to app module
brionmario Apr 21, 2019
8dbcb2b
chore(config): set celery and socket io configs in config file :wrench:
brionmario Apr 21, 2019
fb4be57
chore(celery): add celery config :wrench:
brionmario Apr 21, 2019
9977d7f
build(manager): override current runserver to start up celery :boom:
brionmario Apr 21, 2019
abe9dde
chore(migrations): update migrations :alembic:
brionmario Apr 21, 2019
fdfbfe0
feat(core): add websocket support :sparkles:
brionmario Apr 21, 2019
7182f4e
Merge pull request #11 from brionmario/feature/websocket-support
brionmario Apr 21, 2019
3f02fbe
chore(models): change Session table column attributes
brionmario Apr 24, 2019
bd0b739
refactor(routes): refactor application routes
brionmario Apr 24, 2019
5f772de
refactor(core): refactor entire codebase :recycle:
brionmario Apr 24, 2019
c21558e
chore(migrations): update migrations :alembic:
brionmario Apr 24, 2019
5a17ae4
chore(vcs): ignore shape predictor and add readme for download info :…
brionmario Apr 24, 2019
9801db1
chore(events): add sentiment score persistance
brionmario Apr 24, 2019
1a21a62
refactor(events): move celery tasks to a separate file :truck:
brionmario Apr 24, 2019
9df3676
chore(core): import tasks to the base file
brionmario Apr 24, 2019
45b351b
chore(questionnaire): add PATCH request to update questionnaire
brionmario May 3, 2019
a8e47c7
chore(utils): add function to decode base63 strings
brionmario May 3, 2019
4d33cde
chore(tasks): add logic to persist stream of frames on Redis
brionmario May 3, 2019
614658a
refactor(events): modify celery task init logic
brionmario May 3, 2019
2a4c30b
chore(deps): add numpy dependency :heavy_plus_sign:
brionmario May 3, 2019
fe32273
chore(config): add cssi config :wrench:
brionmario May 3, 2019
a80df96
refactcor: rename cssi config file param
brionmario May 3, 2019
a20ef01
chore(sessions): add PUT method for session complete handling
brionmario May 3, 2019
941e74c
chore(sessions): update session PUT to include questionnaire breakdown
brionmario May 3, 2019
331624b
chore(sessions): add PATCH request to update session status
brionmario May 3, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Ignoring shape predictor
app/data/classifiers/shape_predictor_68_face_landmarks.dat

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
57 changes: 47 additions & 10 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import os
import logging.config

from celery import Celery
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_socketio import SocketIO
from cssi.core import CSSI

from config import CONFIG

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
LOG_FILES_PATH = os.path.split(BASE_DIR)[0] + '/logs'
LOG_FILES_PATH = os.path.join(os.path.split(BASE_DIR)[0], "logs")
LOGGER_CONFIG_PATH = os.path.join(os.path.split(BASE_DIR)[0], "config", "logging.conf")

# Try to create a log folder
try:
Expand All @@ -17,31 +22,63 @@
except OSError:
pass

# load logging config file
logging.config.fileConfig('config/logging.conf', disable_existing_loggers=False)
# init file logger
logger = logging.getLogger('CSSI_REST_API')
# Load logging config file
logging.config.fileConfig(LOGGER_CONFIG_PATH, disable_existing_loggers=False)
# Init file logger
logger = logging.getLogger('cssi.api')

# set `socketio` and `engineio` log level to `ERROR`
logging.getLogger('socketio').setLevel(logging.ERROR)
logging.getLogger('engineio').setLevel(logging.ERROR)

cssi = CSSI(shape_predictor="app/data/classifiers/shape_predictor_68_face_landmarks.dat", debug=False, config_file="config.cssi")
db = SQLAlchemy()
ma = Marshmallow()
socketio = SocketIO()
celery = Celery(__name__,
broker=os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
backend=os.environ.get('CELERY_BACKEND', 'redis://localhost:6379/0'))
celery.config_from_object('celeryconfig')

# Import models to register them with SQLAlchemy
from app.models import * # noqa

# Import celery task to register them with Celery workers
from . import tasks # noqa

# Import Socket.IO events to register them with Flask-SocketIO
from . import events # noqa


def create_app(config_name):
def create_app(config_name=None, main=True):
if config_name is None:
config_name = os.environ.get('CSSI_CONFIG', 'default')
app = Flask(__name__)
CORS(app, support_credentials=True)
app.config.from_object(CONFIG[config_name])

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # disabling sqlalchemy event system

CORS(app, support_credentials=True) # Add CORS support

CONFIG[config_name].init_app(app)

root = CONFIG[config_name].APPLICATION_ROOT

# flask migrate doesn't recognize the tables without this import
from app.models import Application, Genre, ApplicationType, Session, Questionnaire

# Set up extensions
db.init_app(app)

if main:
# Initialize socketio server and attach it to the message queue.
socketio.init_app(app,
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'])
else:
# Initialize socketio to emit events through through the message queue.
socketio.init_app(None,
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'],
async_mode='threading')

celery.conf.update(CONFIG[config_name].CELERY_CONFIG)

# Create app blueprints
from app.routes.v1 import main as main_blueprint
app.register_blueprint(main_blueprint, url_prefix=root + '/')
Expand Down
2 changes: 2 additions & 0 deletions app/data/classifiers/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
use the bellow link to download the shape predictor file.
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
64 changes: 64 additions & 0 deletions app/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging

from app.models import Session
from . import socketio, celery, db
from .tasks import calculate_latency, persist_frames, record_sentiment
from .utils import decode_base64

logger = logging.getLogger('cssi.api')


@socketio.on("test/init")
def on_test_init(session_id):
from .wsgi_aux import app
with app.app_context():
session = Session.query.filter_by(id=session_id).first()
if session is not None:
if session.status == 'initialized':
session.status = 'started'
db.session.commit()
socketio.send({"status": "success", "message": "The test session started successfully."}, json=True)
logger.info("Successfully initialized the test session. ID: {0}".format(session_id))


@socketio.on("test/start")
def on_test_start(head_frame, scene_frame, session_id, latency_interval=2):
_head_frame = head_frame["head_frame"]
_scene_frame = scene_frame["scene_frame"]
latency_frame_count = 10

# decoding head-frame image(base64) string to OpenCV compatible format
_head_frame_decoded = decode_base64(_head_frame)

# decoding scene-frame image(base64) string to OpenCV compatible format
_scene_frame_decoded = decode_base64(_scene_frame)

# chain the two tasks which persist the frames and pass them to
# the latency worker after the specified time interval.
result = persist_frames.delay(head_frame=_head_frame, scene_frame=_scene_frame, limit=latency_frame_count)

if result:
calculate_latency.delay(session_id["session_id"], limit=latency_frame_count)

record_sentiment.apply_async(args=[_head_frame_decoded, session_id["session_id"]], expires=10)


@socketio.on("test/stop")
def on_test_stop(session_id):
from .wsgi_aux import app
with app.app_context():
session = Session.query.filter_by(id=session_id).first()
if session is not None:
if session.status == 'started':
session.status = 'completed'
db.session.commit()
socketio.send({"status": "success", "message": "The test session completed successfully."}, json=True)
celery.control.purge() # stop all celery workers
logger.info("The test session was terminated successfully. ID: {0}".format(session_id))


@socketio.on("disconnect")
def on_disconnect():
"""A Socket.IO client has disconnected."""
celery.control.purge() # stop all celery workers
logger.info("The Socket.IO client disconnected.")
4 changes: 3 additions & 1 deletion app/models/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Application(db.Model):
developer = db.Column(db.String(100), nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey('application_type.id', use_alter=True, name='fk_type_id'), nullable=False)
description = db.Column(db.String(250), nullable=False)
public_sharing = db.Column(db.Boolean, nullable=False, default=False)
creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
genre_id = db.Column(db.Integer, db.ForeignKey('genre.id', use_alter=True, name='fk_genre_id'), nullable=False)
sessions = db.relationship('Session', backref='app', lazy='dynamic')
Expand All @@ -36,5 +37,6 @@ class ApplicationSchema(ma.Schema):
developer = fields.String(required=True, validate=validate.Length(1, 100))
type = fields.Nested(ApplicationTypeSchema, dump_only=True)
description = fields.String(required=True, validate=validate.Length(1, 250))
creation_date = fields.DateTime()
creation_date = fields.DateTime(dump_only=True)
genre = fields.Nested(GenreSchema, dump_only=True)
public_sharing = fields.Boolean(required=True)
2 changes: 1 addition & 1 deletion app/models/application_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __repr__(self):


class ApplicationTypeSchema(ma.Schema):
id = fields.Integer()
id = fields.Integer(dump_only=True)
name = fields.String(required=True)
display_name = fields.String(required=True)
display_name_full = fields.String(required=True)
2 changes: 1 addition & 1 deletion app/models/genre.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ def __repr__(self):


class GenreSchema(ma.Schema):
id = fields.Integer()
id = fields.Integer(dump_only=True)
name = fields.String(required=True)
display_name = fields.String(required=True)
14 changes: 4 additions & 10 deletions app/models/questionnaire.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from marshmallow import fields, validate
from marshmallow import fields
from .. import db, ma


Expand All @@ -19,14 +19,8 @@ def __repr__(self):
return '<Questionnaire %r>' % self.id


class SymptomSchema(ma.Schema):
name = fields.String(required=False)
display_name = fields.String(required=False)
score = fields.String(required=False)


class QuestionnaireSchema(ma.Schema):
id = fields.Integer(dump_only=True)
pre = fields.List(fields.Nested(SymptomSchema), dump_only=True)
post = fields.List(fields.Nested(SymptomSchema), dump_only=True)
creation_date = fields.DateTime()
pre = fields.Dict(required=True)
post = fields.Dict(required=False)
creation_date = fields.DateTime(dump_only=True)
19 changes: 15 additions & 4 deletions app/models/session.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from marshmallow import fields, validate
from sqlalchemy.ext.mutable import MutableList
from .. import db, ma
from .application import ApplicationSchema
from .questionnaire import QuestionnaireSchema
Expand All @@ -8,14 +9,15 @@ class Session(db.Model):
__tablename__ = 'session'

id = db.Column(db.Integer, primary_key=True)
status = db.Column(db.String(25), nullable=False, default='initialised')
app_id = db.Column(db.Integer, db.ForeignKey('application.id', use_alter=True, name='fk_app_id'), nullable=False)
creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
expected_emotions = db.Column(db.JSON, nullable=False)
questionnaire_id = db.Column(db.Integer, db.ForeignKey('questionnaire.id', use_alter=True, name='fk_questionnaire_id'), nullable=False)
cssi_score = db.Column(db.Float, nullable=False, default=0)
latency_scores = db.Column(db.JSON, nullable=False, default={})
latency_scores = db.Column(MutableList.as_mutable(db.JSON), nullable=False, default=[])
total_latency_score = db.Column(db.Float, nullable=False, default=0)
sentiment_scores = db.Column(db.JSON, nullable=False, default={})
sentiment_scores = db.Column(MutableList.as_mutable(db.JSON), nullable=False, default=[])
total_sentiment_score = db.Column(db.Float, nullable=False, default=0)
questionnaire_scores = db.Column(db.JSON, nullable=True, default={})
total_questionnaire_score = db.Column(db.Float, nullable=False, default=0)
Expand All @@ -31,7 +33,16 @@ def __repr__(self):

class SessionSchema(ma.Schema):
id = fields.Integer(dump_only=True)
creation_date = fields.DateTime()
expected_emotions = fields.List(fields.String(), dump_only=True)
status = fields.String()
creation_date = fields.DateTime(dump_only=True)
expected_emotions = fields.List(fields.String(), required=True)
app = fields.Nested(ApplicationSchema, dump_only=True)
questionnaire = fields.Nested(QuestionnaireSchema, dump_only=True)
cssi_score = fields.Float()
latency_scores = fields.List(fields.Dict())
total_latency_score = fields.Float()
sentiment_scores = fields.List(fields.Dict())
total_sentiment_score = fields.Float()
questionnaire_scores = fields.Dict()
total_questionnaire_score = fields.Float()

24 changes: 8 additions & 16 deletions app/routes/v1/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import traceback
from flask_cors import cross_origin
from flask import Blueprint, jsonify, request
from app.models import Application, ApplicationType,ApplicationTypeSchema, ApplicationSchema, Genre, GenreSchema
from app.models import Application, ApplicationType, ApplicationTypeSchema, ApplicationSchema, Genre, GenreSchema
from app import db

logger = logging.getLogger('CSSI_REST_API')
logger = logging.getLogger('cssi.api')

application = Blueprint('application', __name__)

Expand Down Expand Up @@ -73,17 +73,6 @@ def get_application_genres():
@cross_origin(supports_credentials=True)
def create_application():
"""Create a new Application"""

json_data = request.get_json(force=True)

if not json_data:
return jsonify({'status': 'error', 'message': 'No input was provided.'}), 400

# Validate and deserialize input
data, errors = application_schema.load(json_data)
if errors:
return jsonify({'status': 'error', 'message': 'Incorrect format of data provided.', 'data': errors}), 422

name = request.json['name']
identifier = str(uuid.uuid4().hex)
developer = request.json['developer']
Expand All @@ -99,7 +88,8 @@ def create_application():
if not genre:
return {'status': 'error', 'message': 'Invalid Genre Type'}, 400

new_application = Application(name=name, identifier=identifier, developer=developer, type=type, description=description, genre=genre)
new_application = Application(name=name, identifier=identifier,
developer=developer, type=type, description=description, genre=genre)

db.session.add(new_application)
db.session.commit()
Expand All @@ -112,13 +102,15 @@ def create_application():
@application.after_request
def after_request(response):
"""Logs a debug message on every successful request."""
logger.debug('%s %s %s %s %s', request.remote_addr, request.method, request.scheme, request.full_path, response.status)
logger.debug('%s %s %s %s %s', request.remote_addr, request.method,
request.scheme, request.full_path, response.status)
return response


@application.errorhandler(Exception)
def exceptions(e):
"""Logs an error message and stacktrace if a request ends in error."""
tb = traceback.format_exc()
logger.error('%s %s %s %s 5xx INTERNAL SERVER ERROR\n%s', request.remote_addr, request.method, request.scheme, request.full_path, tb)
logger.error('%s %s %s %s 5xx INTERNAL SERVER ERROR\n%s', request.remote_addr,
request.method, request.scheme, request.full_path, tb)
return e.status_code
13 changes: 12 additions & 1 deletion app/routes/v1/questionnaire.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from flask import Blueprint, jsonify, request
from flask_cors import cross_origin
from app.models import Questionnaire, ApplicationType, QuestionnaireSchema, Genre
from app.models import Questionnaire, QuestionnaireSchema
from app import db

questionnaire = Blueprint('questionnaire', __name__)
Expand All @@ -44,6 +44,17 @@ def get_questionnaire(id):
return jsonify({'status': 'success', 'message': None, 'data': result}), 200


@questionnaire.route('/<int:id>/post', methods=['PATCH'])
@cross_origin(supports_credentials=True)
def update_questionnaire(id):
"""Get questionnaire when an id is passed in"""
questionnaire = Questionnaire.query.get(id)
questionnaire.post = request.json['post']
db.session.commit()
result = questionnaire_schema.dump(questionnaire).data
return jsonify({'status': 'success', 'message': 'Successfully added the post questionnaire', 'data': result}), 200


@questionnaire.route('/', methods=['POST'])
@cross_origin(supports_credentials=True)
def create_questionnaire():
Expand Down
Loading