From e9b6e3f8a0b474a3981a00f724d332024a444d31 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Thu, 18 Apr 2019 18:39:02 +0530 Subject: [PATCH 01/37] chore(models): add public_sharing attribute to applications model --- app/models/application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/application.py b/app/models/application.py index 50b2754..072c094 100644 --- a/app/models/application.py +++ b/app/models/application.py @@ -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') @@ -38,3 +39,4 @@ class ApplicationSchema(ma.Schema): description = fields.String(required=True, validate=validate.Length(1, 250)) creation_date = fields.DateTime() genre = fields.Nested(GenreSchema, dump_only=True) + public_sharing = fields.Boolean(required=True) From 0d3ed7e113c2b111970f96f770a2dbdc77e6e8b4 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Thu, 18 Apr 2019 18:40:16 +0530 Subject: [PATCH 02/37] chore(models): change questionnaire marshalling --- app/models/questionnaire.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/models/questionnaire.py b/app/models/questionnaire.py index 3ae3522..4e5036c 100644 --- a/app/models/questionnaire.py +++ b/app/models/questionnaire.py @@ -1,4 +1,4 @@ -from marshmallow import fields, validate +from marshmallow import fields from .. import db, ma @@ -19,14 +19,8 @@ def __repr__(self): return '' % 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) + pre = fields.Raw(required=True) + post = fields.Raw(required=False) creation_date = fields.DateTime() From 964c509830f5b631dd5eb930c7c3ce3433813d59 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Thu, 18 Apr 2019 18:40:50 +0530 Subject: [PATCH 03/37] refactor(routes): remove unwanted imports --- app/routes/v1/questionnaire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/v1/questionnaire.py b/app/routes/v1/questionnaire.py index 1370997..b4f9e0d 100644 --- a/app/routes/v1/questionnaire.py +++ b/app/routes/v1/questionnaire.py @@ -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__) From 8b56aa4cf9fd45c2b4cfd019a2098d4bd0d59f75 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Thu, 18 Apr 2019 18:41:05 +0530 Subject: [PATCH 04/37] chore(migrations): update migrations :alembic: --- migrations/versions/1152279008aa_.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 migrations/versions/1152279008aa_.py diff --git a/migrations/versions/1152279008aa_.py b/migrations/versions/1152279008aa_.py new file mode 100644 index 0000000..808fe36 --- /dev/null +++ b/migrations/versions/1152279008aa_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 1152279008aa +Revises: 89ee48680e79 +Create Date: 2019-04-17 03:10:46.308000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '1152279008aa' +down_revision = '89ee48680e79' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('application', sa.Column('public_sharing', sa.Boolean(), nullable=False)) + op.alter_column('questionnaire', 'post', + existing_type=mysql.TEXT(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('questionnaire', 'post', + existing_type=mysql.TEXT(), + nullable=True) + op.drop_column('application', 'public_sharing') + # ### end Alembic commands ### From 53b521cef7201af70994b9d25e33256f4045dd85 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 01:42:35 +0530 Subject: [PATCH 05/37] fix(models): fix data type issues in models :bug: --- app/models/application_type.py | 2 +- app/models/genre.py | 2 +- app/models/questionnaire.py | 4 ++-- app/models/session.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/application_type.py b/app/models/application_type.py index c93015a..0ddb96a 100644 --- a/app/models/application_type.py +++ b/app/models/application_type.py @@ -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) diff --git a/app/models/genre.py b/app/models/genre.py index 89a0ecf..88fd884 100644 --- a/app/models/genre.py +++ b/app/models/genre.py @@ -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) diff --git a/app/models/questionnaire.py b/app/models/questionnaire.py index 4e5036c..51cf7ae 100644 --- a/app/models/questionnaire.py +++ b/app/models/questionnaire.py @@ -21,6 +21,6 @@ def __repr__(self): class QuestionnaireSchema(ma.Schema): id = fields.Integer(dump_only=True) - pre = fields.Raw(required=True) - post = fields.Raw(required=False) + pre = fields.Dict(required=True) + post = fields.Dict(required=False) creation_date = fields.DateTime() diff --git a/app/models/session.py b/app/models/session.py index d71089b..26bebed 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -10,7 +10,7 @@ class Session(db.Model): id = db.Column(db.Integer, primary_key=True) 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) + expected_emotions = db.Column(db.PickleType, 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={}) From 1c1ed8fbcde720dd8ebe32a1731db90106d19668 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 01:43:38 +0530 Subject: [PATCH 06/37] chore(migrations): update migrations :alembic: --- migrations/versions/1152279008aa_.py | 34 --------------- migrations/versions/89ee48680e79_.py | 42 ------------------- .../{cb018c771a53_.py => d874df9a1f85_.py} | 23 +++++----- 3 files changed, 11 insertions(+), 88 deletions(-) delete mode 100644 migrations/versions/1152279008aa_.py delete mode 100644 migrations/versions/89ee48680e79_.py rename migrations/versions/{cb018c771a53_.py => d874df9a1f85_.py} (83%) diff --git a/migrations/versions/1152279008aa_.py b/migrations/versions/1152279008aa_.py deleted file mode 100644 index 808fe36..0000000 --- a/migrations/versions/1152279008aa_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""empty message - -Revision ID: 1152279008aa -Revises: 89ee48680e79 -Create Date: 2019-04-17 03:10:46.308000 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '1152279008aa' -down_revision = '89ee48680e79' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('application', sa.Column('public_sharing', sa.Boolean(), nullable=False)) - op.alter_column('questionnaire', 'post', - existing_type=mysql.TEXT(), - nullable=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('questionnaire', 'post', - existing_type=mysql.TEXT(), - nullable=True) - op.drop_column('application', 'public_sharing') - # ### end Alembic commands ### diff --git a/migrations/versions/89ee48680e79_.py b/migrations/versions/89ee48680e79_.py deleted file mode 100644 index 4f39656..0000000 --- a/migrations/versions/89ee48680e79_.py +++ /dev/null @@ -1,42 +0,0 @@ -"""empty message - -Revision ID: 89ee48680e79 -Revises: cb018c771a53 -Create Date: 2019-04-16 04:22:10.614121 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '89ee48680e79' -down_revision = 'cb018c771a53' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_foreign_key('fk_type_id', 'application', 'application_type', ['type_id'], ['id'], use_alter=True) - op.create_foreign_key('fk_genre_id', 'application', 'genre', ['genre_id'], ['id'], use_alter=True) - op.alter_column('questionnaire', 'post', - existing_type=mysql.TEXT(), - nullable=True) - op.drop_column('questionnaire', 'session_id') - op.create_foreign_key('fk_app_id', 'session', 'application', ['app_id'], ['id'], use_alter=True) - op.create_foreign_key('fk_questionnaire_id', 'session', 'questionnaire', ['questionnaire_id'], ['id'], use_alter=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('fk_questionnaire_id', 'session', type_='foreignkey') - op.drop_constraint('fk_app_id', 'session', type_='foreignkey') - op.add_column('questionnaire', sa.Column('session_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False)) - op.alter_column('questionnaire', 'post', - existing_type=mysql.TEXT(), - nullable=False) - op.drop_constraint('fk_genre_id', 'application', type_='foreignkey') - op.drop_constraint('fk_type_id', 'application', type_='foreignkey') - # ### end Alembic commands ### diff --git a/migrations/versions/cb018c771a53_.py b/migrations/versions/d874df9a1f85_.py similarity index 83% rename from migrations/versions/cb018c771a53_.py rename to migrations/versions/d874df9a1f85_.py index bd2e305..87af537 100644 --- a/migrations/versions/cb018c771a53_.py +++ b/migrations/versions/d874df9a1f85_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: cb018c771a53 +Revision ID: d874df9a1f85 Revises: -Create Date: 2019-04-16 01:05:44.197617 +Create Date: 2019-04-19 01:39:08.576426 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'cb018c771a53' +revision = 'd874df9a1f85' down_revision = None branch_labels = None depends_on = None @@ -25,6 +25,7 @@ def upgrade(): sa.Column('developer', sa.String(length=100), nullable=False), sa.Column('type_id', sa.Integer(), nullable=False), sa.Column('description', sa.String(length=250), nullable=False), + sa.Column('public_sharing', sa.Boolean(), nullable=False), sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), sa.Column('genre_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['genre_id'], ['genre.id'], name='fk_genre_id', use_alter=True), @@ -46,25 +47,23 @@ def upgrade(): ) op.create_table('questionnaire', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pre', sa.TEXT(), nullable=False), - sa.Column('post', sa.TEXT(), nullable=False), + sa.Column('pre', sa.JSON(), nullable=False), + sa.Column('post', sa.JSON(), nullable=False), sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('session_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['session_id'], ['session.id'], name='fk_session_id', use_alter=True), sa.PrimaryKeyConstraint('id') ) op.create_table('session', sa.Column('id', sa.Integer(), nullable=False), sa.Column('app_id', sa.Integer(), nullable=False), sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('expected_emotions', sa.TEXT(), nullable=False), + sa.Column('expected_emotions', sa.PickleType(), nullable=False), + sa.Column('questionnaire_id', sa.Integer(), nullable=False), sa.Column('cssi_score', sa.Float(), nullable=False), - sa.Column('latency_scores', sa.TEXT(), nullable=False), + sa.Column('latency_scores', sa.JSON(), nullable=False), sa.Column('total_latency_score', sa.Float(), nullable=False), - sa.Column('sentiment_scores', sa.TEXT(), nullable=False), + sa.Column('sentiment_scores', sa.JSON(), nullable=False), sa.Column('total_sentiment_score', sa.Float(), nullable=False), - sa.Column('questionnaire_id', sa.Integer(), nullable=False), - sa.Column('questionnaire_scores', sa.TEXT(), nullable=True), + sa.Column('questionnaire_scores', sa.JSON(), nullable=True), sa.Column('total_questionnaire_score', sa.Float(), nullable=False), sa.ForeignKeyConstraint(['app_id'], ['application.id'], name='fk_app_id', use_alter=True), sa.ForeignKeyConstraint(['questionnaire_id'], ['questionnaire.id'], name='fk_questionnaire_id', use_alter=True), From a4ca3cc11be6875501f932d16ef8ab259d073dc6 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 02:21:47 +0530 Subject: [PATCH 07/37] chore(models): update session model schema --- app/models/session.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/models/session.py b/app/models/session.py index 26bebed..810f670 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -10,7 +10,7 @@ class Session(db.Model): id = db.Column(db.Integer, primary_key=True) 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.PickleType, 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={}) @@ -31,7 +31,15 @@ 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) + 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.Dict() + total_latency_score = fields.Float() + sentiment_scores = fields.Dict() + total_sentiment_score = fields.Float() + questionnaire_scores = fields.Dict() + total_questionnaire_score = fields.Float() + From 56b39e69794eb9be68e0c9d092fca9a44fd745a4 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 02:22:25 +0530 Subject: [PATCH 08/37] fix(models): fix minor validation issues in models --- app/models/application.py | 2 +- app/models/questionnaire.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/application.py b/app/models/application.py index 072c094..a5e0fb4 100644 --- a/app/models/application.py +++ b/app/models/application.py @@ -37,6 +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) diff --git a/app/models/questionnaire.py b/app/models/questionnaire.py index 51cf7ae..b9a368d 100644 --- a/app/models/questionnaire.py +++ b/app/models/questionnaire.py @@ -23,4 +23,4 @@ class QuestionnaireSchema(ma.Schema): id = fields.Integer(dump_only=True) pre = fields.Dict(required=True) post = fields.Dict(required=False) - creation_date = fields.DateTime() + creation_date = fields.DateTime(dump_only=True) From e7d2a4d5d8a2509134a3b0f3142659066d4f1771 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 02:23:09 +0530 Subject: [PATCH 09/37] refactor(routes): remove initial validation in application GET --- app/routes/v1/application.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/routes/v1/application.py b/app/routes/v1/application.py index 32a82fb..7e7d0ed 100644 --- a/app/routes/v1/application.py +++ b/app/routes/v1/application.py @@ -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'] From b6147ebaa008adc027e55e33b644b5825f5674e7 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 02:24:08 +0530 Subject: [PATCH 10/37] chore(routes): change questionnaire attrib in session POST :boom: --- app/routes/v1/session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py index e116a12..00fca15 100644 --- a/app/routes/v1/session.py +++ b/app/routes/v1/session.py @@ -48,9 +48,8 @@ def get_session(id): @cross_origin(supports_credentials=True) def create_session(): """Create a new Session""" - app = Application.query.filter_by(id=request.json['app']).first() - questionnaire = Questionnaire.query.filter_by(id=request.json['questionnaire_id']).first() + questionnaire = Questionnaire.query.filter_by(id=request.json['questionnaire']).first() expected_emotions = request.json['expected_emotions'] print(questionnaire) From 4ad8a7c8d0d917d0b5e38137cde6f61ac1e7e5a7 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 02:24:27 +0530 Subject: [PATCH 11/37] chore(migrations): update migrations :alembic: --- .../versions/{d874df9a1f85_.py => 06614ec763fe_.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename migrations/versions/{d874df9a1f85_.py => 06614ec763fe_.py} (95%) diff --git a/migrations/versions/d874df9a1f85_.py b/migrations/versions/06614ec763fe_.py similarity index 95% rename from migrations/versions/d874df9a1f85_.py rename to migrations/versions/06614ec763fe_.py index 87af537..7935d66 100644 --- a/migrations/versions/d874df9a1f85_.py +++ b/migrations/versions/06614ec763fe_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: d874df9a1f85 +Revision ID: 06614ec763fe Revises: -Create Date: 2019-04-19 01:39:08.576426 +Create Date: 2019-04-19 02:19:26.253782 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'd874df9a1f85' +revision = '06614ec763fe' down_revision = None branch_labels = None depends_on = None @@ -56,7 +56,7 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('app_id', sa.Integer(), nullable=False), sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('expected_emotions', sa.PickleType(), nullable=False), + sa.Column('expected_emotions', sa.JSON(), nullable=False), sa.Column('questionnaire_id', sa.Integer(), nullable=False), sa.Column('cssi_score', sa.Float(), nullable=False), sa.Column('latency_scores', sa.JSON(), nullable=False), From d23ff411cd1aec18bade6609cc8b3304813caa69 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 04:12:51 +0530 Subject: [PATCH 12/37] feat(sessions): add session status recording ability :sparkles: --- app/models/session.py | 2 ++ app/routes/v1/session.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/session.py b/app/models/session.py index 810f670..f195c5c 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -8,6 +8,7 @@ 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) @@ -31,6 +32,7 @@ def __repr__(self): class SessionSchema(ma.Schema): id = fields.Integer(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) diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py index 00fca15..69b3735 100644 --- a/app/routes/v1/session.py +++ b/app/routes/v1/session.py @@ -52,8 +52,6 @@ def create_session(): questionnaire = Questionnaire.query.filter_by(id=request.json['questionnaire']).first() expected_emotions = request.json['expected_emotions'] - print(questionnaire) - # validate application type if not app: return {'status': 'error', 'message': 'Invalid application.'}, 400 From ea83425513201c508309887b3db20eac068c5758 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 19 Apr 2019 04:13:11 +0530 Subject: [PATCH 13/37] chore(migrations): update migrations :alembic: --- migrations/versions/64702ea1834d_.py | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 migrations/versions/64702ea1834d_.py diff --git a/migrations/versions/64702ea1834d_.py b/migrations/versions/64702ea1834d_.py new file mode 100644 index 0000000..e823621 --- /dev/null +++ b/migrations/versions/64702ea1834d_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: 64702ea1834d +Revises: 06614ec763fe +Create Date: 2019-04-19 03:56:38.990243 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '64702ea1834d' +down_revision = '06614ec763fe' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key('fk_genre_id', 'application', 'genre', ['genre_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_type_id', 'application', 'application_type', ['type_id'], ['id'], use_alter=True) + op.add_column('session', sa.Column('status', sa.String(length=25), nullable=False)) + op.create_foreign_key('fk_app_id', 'session', 'application', ['app_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_questionnaire_id', 'session', 'questionnaire', ['questionnaire_id'], ['id'], use_alter=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_questionnaire_id', 'session', type_='foreignkey') + op.drop_constraint('fk_app_id', 'session', type_='foreignkey') + op.drop_column('session', 'status') + op.drop_constraint('fk_type_id', 'application', type_='foreignkey') + op.drop_constraint('fk_genre_id', 'application', type_='foreignkey') + # ### end Alembic commands ### From deb693ddefe6410f50ebe3feac131fdb7cc26b67 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:10:55 +0530 Subject: [PATCH 14/37] chore(app): add socket io and celery init logic to app module --- app/__init__.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 7fe3a92..b2088b1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,13 @@ 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 config import CONFIG BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -17,31 +20,52 @@ except OSError: pass -# load logging config file +# Load logging config file logging.config.fileConfig('config/logging.conf', disable_existing_loggers=False) -# init file logger +# Init file logger logger = logging.getLogger('CSSI_REST_API') db = SQLAlchemy() ma = Marshmallow() +socketio = SocketIO() +celery = Celery(__name__, + broker=os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379'), + backend=os.environ.get('CELERY_BACKEND', 'redis://localhost:6379')) +celery.config_from_object('celeryconfig') + +# Import models to register them with SQLAlchemy +from app.models import Application, Genre, ApplicationType, Session, Questionnaire -def create_app(config_name): +def create_app(config_name=None, main=True): 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 + '/') From 8dbcb2b34d0c791fe7a265a40c27f3bb5611b8cf Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:11:38 +0530 Subject: [PATCH 15/37] chore(config): set celery and socket io configs in config file :wrench: --- app/wsgi_aux.py | 7 +++++++ config/config.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 app/wsgi_aux.py diff --git a/app/wsgi_aux.py b/app/wsgi_aux.py new file mode 100644 index 0000000..b8b0fcb --- /dev/null +++ b/app/wsgi_aux.py @@ -0,0 +1,7 @@ +import os + +from . import create_app + +# Create an application instance that auxiliary processes such as Celery +# workers can use +app = create_app(os.environ.get('CSSI_CONFIG', 'production'), main=False) \ No newline at end of file diff --git a/config/config.py b/config/config.py index dd7f2c2..e5cd895 100644 --- a/config/config.py +++ b/config/config.py @@ -65,6 +65,10 @@ class Config: APP_NAME = os.environ.get('APP_NAME') or 'CSSI_REST_API' APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT') or '/api/v1' + CELERY_CONFIG = {} + SOCKETIO_MESSAGE_QUEUE = os.environ.get( + 'SOCKETIO_MESSAGE_QUEUE', os.environ.get('CELERY_BROKER_URL', + 'redis://')) if os.environ.get('SECRET_KEY'): SECRET_KEY = os.environ.get('SECRET_KEY') @@ -93,6 +97,8 @@ class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-dev.sqlite') + CELERY_BACKEND = os.environ.get('DEV_CELERY_BACKEND') or \ + 'sqlite:///' + os.path.join(BASE_DIR, 'celery-dev.sqlite') @classmethod def init_app(cls, app): @@ -114,6 +120,10 @@ class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-test.sqlite') + CELERY_BACKEND = os.environ.get('TEST_CELERY_BACKEND') or \ + 'sqlite:///' + os.path.join(BASE_DIR, 'celery-test.sqlite') + CELERY_CONFIG = {'CELERY_ALWAYS_EAGER': True} + SOCKETIO_MESSAGE_QUEUE = None @classmethod def init_app(cls, app): @@ -134,6 +144,8 @@ class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(BASE_DIR, 'cssi.sqlite') + CELERY_BACKEND = os.environ.get('CELERY_BACKEND') or \ + 'sqlite:///' + os.path.join(BASE_DIR, 'celery.sqlite') SSL_DISABLE = (os.environ.get('SSL_DISABLE') or 'True') == 'True' @classmethod From fb4be57e28bb539b848a016744df71cfbc106172 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:12:09 +0530 Subject: [PATCH 16/37] chore(celery): add celery config :wrench: --- celeryconfig.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 celeryconfig.py diff --git a/celeryconfig.py b/celeryconfig.py new file mode 100644 index 0000000..a77fc13 --- /dev/null +++ b/celeryconfig.py @@ -0,0 +1,7 @@ + +# global Celery options that apply to all configurations + +# enable the pickle serializer +task_serializer = 'pickle' +result_serializer = 'pickle' +accept_content = ['pickle'] \ No newline at end of file From 9977d7ff556887e76619d9cad948ab5e095a3de6 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:14:26 +0530 Subject: [PATCH 17/37] build(manager): override current runserver to start up celery :boom: --- manage.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/manage.py b/manage.py index e7c3b85..ee12068 100644 --- a/manage.py +++ b/manage.py @@ -13,19 +13,98 @@ """ import os +import subprocess +import sys +import eventlet from flask_migrate import Migrate, MigrateCommand -from flask_script import Manager +from flask_script import Manager, Command, Server as _Server, Option -from app import create_app, db +from app import create_app, db, socketio -app = create_app(os.getenv('FLASK_CONFIG') or 'default') +eventlet.monkey_patch() + +app = create_app(os.getenv('CSSI_CONFIG') or 'default') manager = Manager(app) migrate = Migrate(app, db) manager.add_command('db', MigrateCommand) + +class Server(_Server): + help = description = 'Runs the Socket.IO web server' + + def get_options(self): + options = ( + Option('-h', '--host', + dest='host', + default=self.host), + + Option('-p', '--port', + dest='port', + type=int, + default=self.port), + + Option('-d', '--debug', + action='store_true', + dest='use_debugger', + help=('enable the Werkzeug debugger (DO NOT use in ' + 'production code)'), + default=self.use_debugger), + Option('-D', '--no-debug', + action='store_false', + dest='use_debugger', + help='disable the Werkzeug debugger', + default=self.use_debugger), + + Option('-r', '--reload', + action='store_true', + dest='use_reloader', + help=('monitor Python files for changes (not 100%% safe ' + 'for production use)'), + default=self.use_reloader), + Option('-R', '--no-reload', + action='store_false', + dest='use_reloader', + help='do not monitor Python files for changes', + default=self.use_reloader), + ) + return options + + def __call__(self, app, host, port, use_debugger, use_reloader): + # override the default runserver command to start a Socket.IO server + if use_debugger is None: + use_debugger = app.debug + if use_debugger is None: + use_debugger = True + if use_reloader is None: + use_reloader = app.debug + socketio.run(app, + host=host, + port=port, + debug=use_debugger, + use_reloader=use_reloader, + **self.server_options) + + +manager.add_command("runserver", Server()) + + +class CeleryWorker(Command): + """Starts the celery worker.""" + name = 'celery' + capture_all_args = True + + def run(self, argv): + ret = subprocess.call( + ['celery', 'worker', '-A', 'app.celery', '--loglevel=info'] + argv) + sys.exit(ret) + + +manager.add_command("celery", CeleryWorker()) + + @manager.command def create_metadata(): """Create the table metadata. @@ -38,6 +117,7 @@ def create_metadata(): Genre.seed() ApplicationType.seed() + @manager.command def test(): """Run the unit tests. @@ -51,15 +131,20 @@ def test(): @manager.command -def recreate_db(): +def recreate_db(drop_first=False): """Recreates a local database Not safe to use in production. """ - db.drop_all() + if drop_first: + db.drop_all() db.create_all() db.session.commit() if __name__ == '__main__': + if sys.argv[1] == 'test' or sys.argv[1] == 'lint': + # small hack, to ensure that Flask-Script uses the testing + # configuration if we are going to run the tests + os.environ['CSSI_CONFIG'] = 'testing' manager.run() From abe9ddefee200b362f5d9a5af6fddf92a82e322a Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:35:44 +0530 Subject: [PATCH 18/37] chore(migrations): update migrations :alembic: --- migrations/versions/64702ea1834d_.py | 36 ------------------- .../{06614ec763fe_.py => 883bbdba07f8_.py} | 7 ++-- 2 files changed, 4 insertions(+), 39 deletions(-) delete mode 100644 migrations/versions/64702ea1834d_.py rename migrations/versions/{06614ec763fe_.py => 883bbdba07f8_.py} (95%) diff --git a/migrations/versions/64702ea1834d_.py b/migrations/versions/64702ea1834d_.py deleted file mode 100644 index e823621..0000000 --- a/migrations/versions/64702ea1834d_.py +++ /dev/null @@ -1,36 +0,0 @@ -"""empty message - -Revision ID: 64702ea1834d -Revises: 06614ec763fe -Create Date: 2019-04-19 03:56:38.990243 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '64702ea1834d' -down_revision = '06614ec763fe' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_foreign_key('fk_genre_id', 'application', 'genre', ['genre_id'], ['id'], use_alter=True) - op.create_foreign_key('fk_type_id', 'application', 'application_type', ['type_id'], ['id'], use_alter=True) - op.add_column('session', sa.Column('status', sa.String(length=25), nullable=False)) - op.create_foreign_key('fk_app_id', 'session', 'application', ['app_id'], ['id'], use_alter=True) - op.create_foreign_key('fk_questionnaire_id', 'session', 'questionnaire', ['questionnaire_id'], ['id'], use_alter=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('fk_questionnaire_id', 'session', type_='foreignkey') - op.drop_constraint('fk_app_id', 'session', type_='foreignkey') - op.drop_column('session', 'status') - op.drop_constraint('fk_type_id', 'application', type_='foreignkey') - op.drop_constraint('fk_genre_id', 'application', type_='foreignkey') - # ### end Alembic commands ### diff --git a/migrations/versions/06614ec763fe_.py b/migrations/versions/883bbdba07f8_.py similarity index 95% rename from migrations/versions/06614ec763fe_.py rename to migrations/versions/883bbdba07f8_.py index 7935d66..be763e4 100644 --- a/migrations/versions/06614ec763fe_.py +++ b/migrations/versions/883bbdba07f8_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 06614ec763fe +Revision ID: 883bbdba07f8 Revises: -Create Date: 2019-04-19 02:19:26.253782 +Create Date: 2019-04-21 13:31:10.379141 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '06614ec763fe' +revision = '883bbdba07f8' down_revision = None branch_labels = None depends_on = None @@ -54,6 +54,7 @@ def upgrade(): ) op.create_table('session', sa.Column('id', sa.Integer(), nullable=False), + sa.Column('status', sa.String(length=25), nullable=False), sa.Column('app_id', sa.Integer(), nullable=False), sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), sa.Column('expected_emotions', sa.JSON(), nullable=False), From fdfbfe05e315a5c3b109a269bc077d904594ce22 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sun, 21 Apr 2019 13:36:00 +0530 Subject: [PATCH 19/37] feat(core): add websocket support :sparkles: --- app/__init__.py | 14 +++++++++--- app/events.py | 21 ++++++++++++++++++ app/tasks.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ app/utils.py | 24 +++++++++++++++++++++ requirements.txt | 6 +++++- 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 app/events.py create mode 100644 app/tasks.py create mode 100644 app/utils.py diff --git a/app/__init__.py b/app/__init__.py index b2088b1..a2d78e5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -34,7 +34,13 @@ celery.config_from_object('celeryconfig') # Import models to register them with SQLAlchemy -from app.models import Application, Genre, ApplicationType, Session, Questionnaire +from app.models import * # noqa + +# Import celery task to register them with Celery workers +from .tasks import run_flask_request # noqa + +# Import Socket.IO events to register them with Flask-SocketIO +from . import events # noqa def create_app(config_name=None, main=True): @@ -49,8 +55,6 @@ def create_app(config_name=None, main=True): root = CONFIG[config_name].APPLICATION_ROOT - - # Set up extensions db.init_app(app) @@ -79,4 +83,8 @@ def create_app(config_name=None, main=True): from app.routes.v1 import questionnaire as questionnaire_blueprint app.register_blueprint(questionnaire_blueprint, url_prefix=root + '/questionnaires') + # Register async tasks support + from .tasks import tasks as tasks_blueprint + app.register_blueprint(tasks_blueprint, url_prefix='/tasks') + return app diff --git a/app/events.py b/app/events.py new file mode 100644 index 0000000..bcf314e --- /dev/null +++ b/app/events.py @@ -0,0 +1,21 @@ +from . import socketio, celery + +@celery.task +def calculate_latency(): + """Sample celery task that posts a message.""" + from .wsgi_aux import app + with app.app_context(): + print('hi from celery') + + +@socketio.on('post_message') +def on_post_message(): + """Sample post message.""" + calculate_latency.apply_async() + print('post message') + + +@socketio.on('disconnect') +def on_disconnect(): + """A Socket.IO client has disconnected.""" + print('connection hi disconnected') diff --git a/app/tasks.py b/app/tasks.py new file mode 100644 index 0000000..96190f7 --- /dev/null +++ b/app/tasks.py @@ -0,0 +1,56 @@ +from io import BytesIO +from flask import Blueprint, abort, g +from werkzeug.exceptions import InternalServerError +from celery import states + +from . import celery +from .utils import url_for + +text_types = (str, bytes) +try: + text_types += (unicode,) +except NameError: + # no unicode on Python 3 + pass + +tasks = Blueprint('tasks', __name__) + + +@celery.task +def run_flask_request(environ): + from .wsgi_aux import app + + if '_wsgi.input' in environ: + environ['wsgi.input'] = BytesIO(environ['_wsgi.input']) + + # Create a request context similar to that of the original request + # so that the task can have access to flask.g, flask.request, etc. + with app.request_context(environ): + # Record the fact that we are running in the Celery worker now + g.in_celery = True + + # Run the route function and record the response + try: + rv = app.full_dispatch_request() + except: + # If we are in debug mode we want to see the exception + # Else, return a 500 error + if app.debug: + raise + rv = app.make_response(InternalServerError()) + return (rv.get_data(), rv.status_code, rv.headers) + + +@tasks.route('/status/', methods=['GET']) +def get_status(id): + """ + Return status about an asynchronous task. If this request returns a 202 + status code, it means that task hasn't finished yet. Else, the response + from the task is returned. + """ + task = run_flask_request.AsyncResult(id) + if task.state == states.PENDING: + abort(404) + if task.state == states.RECEIVED or task.state == states.STARTED: + return '', 202, {'Location': url_for('tasks.get_status', id=id)} + return task.info diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..d757746 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,24 @@ +import time + +from flask import url_for as _url_for, current_app, _request_ctx_stack + + +def timestamp(): + """Return the current timestamp as an integer.""" + return int(time.time()) + + +def url_for(*args, **kwargs): + """ + url_for replacement that works even when there is no request context. + """ + if '_external' not in kwargs: + kwargs['_external'] = False + reqctx = _request_ctx_stack.top + if reqctx is None: + if kwargs['_external']: + raise RuntimeError('Cannot generate external URLs without a ' + 'request context.') + with current_app.test_request_context(): + return _url_for(*args, **kwargs) + return _url_for(*args, **kwargs) diff --git a/requirements.txt b/requirements.txt index c901e77..5f71db1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +eventlet==0.19.0 flask==0.12.2 flask_script==2.0.6 flask_migrate==2.1.1 @@ -6,4 +7,7 @@ flask_sqlalchemy==2.3.2 flask_marshmallow==0.8.0 marshmallow-sqlalchemy==0.13.2 PyMySQL==0.9.3 -flask-cors \ No newline at end of file +flask-cors +flask-socketio +celery==4.3.0 +redis==3.2.0 \ No newline at end of file From 3f02fbec9442bfbc61fc56a3d718e55ef09ba628 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 17:44:17 +0530 Subject: [PATCH 20/37] chore(models): change Session table column attributes --- app/models/session.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/session.py b/app/models/session.py index f195c5c..ee8319b 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -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 @@ -14,9 +15,9 @@ class Session(db.Model): 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) @@ -38,9 +39,9 @@ class SessionSchema(ma.Schema): app = fields.Nested(ApplicationSchema, dump_only=True) questionnaire = fields.Nested(QuestionnaireSchema, dump_only=True) cssi_score = fields.Float() - latency_scores = fields.Dict() + latency_scores = fields.List(fields.Dict()) total_latency_score = fields.Float() - sentiment_scores = fields.Dict() + sentiment_scores = fields.List(fields.Dict()) total_sentiment_score = fields.Float() questionnaire_scores = fields.Dict() total_questionnaire_score = fields.Float() From bd0b73953bcf1054a4f3f9ed00616b7ac2dfec14 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 17:44:57 +0530 Subject: [PATCH 21/37] refactor(routes): refactor application routes --- app/routes/v1/application.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/routes/v1/application.py b/app/routes/v1/application.py index 7e7d0ed..a0f327a 100644 --- a/app/routes/v1/application.py +++ b/app/routes/v1/application.py @@ -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__) @@ -88,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() @@ -101,7 +102,8 @@ 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 @@ -109,5 +111,6 @@ def after_request(response): 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 From 5f772de4db0e0c97c1ab66abde1b7bed54226078 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 17:45:40 +0530 Subject: [PATCH 22/37] refactor(core): refactor entire codebase :recycle: --- app/__init__.py | 19 +++++++--- app/events.py | 89 ++++++++++++++++++++++++++++++++++++++------- app/tasks.py | 43 +++++++++++++++++++++- app/wsgi_aux.py | 2 +- config/config.py | 22 +++++++---- config/logging.conf | 8 ++-- requirements.txt | 12 +++--- 7 files changed, 158 insertions(+), 37 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index a2d78e5..973f4f4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,11 +7,13 @@ 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: @@ -21,16 +23,21 @@ pass # Load logging config file -logging.config.fileConfig('config/logging.conf', disable_existing_loggers=False) +logging.config.fileConfig(LOGGER_CONFIG_PATH, disable_existing_loggers=False) # Init file logger -logger = logging.getLogger('CSSI_REST_API') +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="cssi.rc") db = SQLAlchemy() ma = Marshmallow() socketio = SocketIO() celery = Celery(__name__, - broker=os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379'), - backend=os.environ.get('CELERY_BACKEND', 'redis://localhost:6379')) + 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 @@ -44,6 +51,8 @@ def create_app(config_name=None, main=True): + if config_name is None: + config_name = os.environ.get('CSSI_CONFIG', 'default') app = Flask(__name__) app.config.from_object(CONFIG[config_name]) diff --git a/app/events.py b/app/events.py index bcf314e..f4f7b8e 100644 --- a/app/events.py +++ b/app/events.py @@ -1,21 +1,84 @@ -from . import socketio, celery +import time +import logging +import base64 +import numpy as np +from datetime import datetime +from io import BytesIO +from PIL import Image +from celery import chain +from app.models import Session +from . import socketio, celery, cssi, db + +logger = logging.getLogger('cssi.api') + + +@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"] + + # decoding base64 string to opencv compatible format + _head_frame_starter = _head_frame.find(',') + _head_frame_image_data = _head_frame[_head_frame_starter + 1:] + _head_frame_image_data = bytes(_head_frame_image_data, encoding="ascii") + _head_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_head_frame_image_data)))) + + _scene_frame_starter = _scene_frame.find(',') + _scene_frame_image_data = _scene_frame[_scene_frame_starter + 1:] + _scene_frame_image_data = bytes(_scene_frame_image_data, encoding="ascii") + _scene_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_scene_frame_image_data)))) + + latency_workflow = chain(persist_prev_frames.s(_head_frame_decoded, _scene_frame_decoded, latency_interval), calculate_latency.s(_head_frame_decoded, _scene_frame_decoded, session_id["session_id"])).apply_async(expires=10) + + sentiment_workflow = record_sentiment.apply_async(args=[_head_frame_decoded], expires=10) + + +@socketio.on("disconnect") +def on_disconnect(): + """A Socket.IO client has disconnected.""" + print("Socket.IO Client Disconnected") + @celery.task -def calculate_latency(): - """Sample celery task that posts a message.""" +def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id): + print('Session id: {}'.format(session_id)) from .wsgi_aux import app with app.app_context(): - print('hi from celery') + pre_head_frame, prev_scene_frame = pre_frames + _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=pre_head_frame) + _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=curr_head_frame) + _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, second_frame=curr_scene_frame, crop=True, crop_direction='horizontal') -@socketio.on('post_message') -def on_post_message(): - """Sample post message.""" - calculate_latency.apply_async() - print('post message') + head_angles = [[phf_pitch, phf_yaw, phf_roll], [chf_pitch, chf_yaw, chf_roll]] + camera_angles = [sf_pitch, sf_yaw, sf_roll] + latency_score = cssi.latency.generate_score(head_angles=head_angles, camera_angles=camera_angles) + + print( + 'Celery calculate_latency Task - Previous Head Frame : {0}, {1}, {2}'.format(phf_pitch, phf_yaw, phf_roll)) + print('Celery calculate_latency Task - Current Head Frame : {0}, {1}, {2}'.format(chf_pitch, chf_yaw, chf_roll)) + print('Celery calculate_latency Task - Scene Frame : {0}, {1}, {2}'.format(sf_pitch, sf_yaw, sf_roll)) + print('Celery calculate_latency Task - Latency Score : {0}'.format(latency_score)) + + new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'score': latency_score} + + session = Session.query.filter_by(id=session_id).first() + if session is not None: + session.latency_scores.append(new_score) + print('New Scores: {}'.format(session.latency_scores)) + db.session.commit() + + +@celery.task +def record_sentiment(head_frame): + """Sample celery task that posts a message.""" + sentiment = cssi.sentiment.detect_emotions(frame=head_frame) + print('From celery evaluate_sentiment: {0}'.format(sentiment)) + + +@celery.task +def persist_prev_frames(head_frame, scene_frame, interval): + time.sleep(interval) + return head_frame, scene_frame -@socketio.on('disconnect') -def on_disconnect(): - """A Socket.IO client has disconnected.""" - print('connection hi disconnected') diff --git a/app/tasks.py b/app/tasks.py index 96190f7..9665aed 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -1,5 +1,10 @@ -from io import BytesIO -from flask import Blueprint, abort, g +from functools import wraps +try: + from io import BytesIO +except ImportError: # pragma: no cover + from cStringIO import StringIO as BytesIO + +from flask import Blueprint, abort, g, request from werkzeug.exceptions import InternalServerError from celery import states @@ -41,6 +46,40 @@ def run_flask_request(environ): return (rv.get_data(), rv.status_code, rv.headers) +def async_task(f): + """ + This decorator transforms a sync route to asynchronous by running it + in a background thread. + """ + @wraps(f) + def wrapped(*args, **kwargs): + # If we are already running the request on the celery side, then we + # just call the wrapped function to allow the request to execute. + if getattr(g, 'in_celery', False): + return f(*args, **kwargs) + + # If we are on the Flask side, we need to launch the Celery task, + # passing the request environment, which will be used to reconstruct + # the request object. The request body has to be handled as a special + # case, since WSGI requires it to be provided as a file-like object. + environ = {k: v for k, v in request.environ.items() + if isinstance(v, text_types)} + if 'wsgi.input' in request.environ: + environ['_wsgi.input'] = request.get_data() + t = run_flask_request.apply_async(args=(environ,)) + + # Return a 202 response, with a link that the client can use to + # obtain task status that is based on the Celery task id. + if t.state == states.PENDING or t.state == states.RECEIVED or \ + t.state == states.STARTED: + return '', 202, {'Location': url_for('tasks.get_status', id=t.id)} + + # If the task already finished, return its return value as response. + # This would be the case when CELERY_ALWAYS_EAGER is set to True. + return t.info + return wrapped + + @tasks.route('/status/', methods=['GET']) def get_status(id): """ diff --git a/app/wsgi_aux.py b/app/wsgi_aux.py index b8b0fcb..3755ae6 100644 --- a/app/wsgi_aux.py +++ b/app/wsgi_aux.py @@ -4,4 +4,4 @@ # Create an application instance that auxiliary processes such as Celery # workers can use -app = create_app(os.environ.get('CSSI_CONFIG', 'production'), main=False) \ No newline at end of file +app = create_app(os.environ.get('CSSI_CONFIG', 'default'), main=False) \ No newline at end of file diff --git a/config/config.py b/config/config.py index e5cd895..ded27de 100644 --- a/config/config.py +++ b/config/config.py @@ -34,7 +34,7 @@ import logging import os -logger = logging.getLogger('CSSI_REST_API') +logger = logging.getLogger('cssi.api') ENVIRONMENT_FILE_NAME = '.env' BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -65,6 +65,8 @@ class Config: APP_NAME = os.environ.get('APP_NAME') or 'CSSI_REST_API' APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT') or '/api/v1' + CELERY_BROKER_URL = os.environ.get( + 'CELERY_BROKER_URL', 'redis://localhost:6379') CELERY_CONFIG = {} SOCKETIO_MESSAGE_QUEUE = os.environ.get( 'SOCKETIO_MESSAGE_QUEUE', os.environ.get('CELERY_BROKER_URL', @@ -96,9 +98,11 @@ class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-dev.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-dev.sqlite') + CELERY_BROKER_URL = os.environ.get( + 'DEV_CELERY_BROKER_URL', 'redis://localhost:6379') CELERY_BACKEND = os.environ.get('DEV_CELERY_BACKEND') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'celery-dev.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'celery-dev.sqlite') @classmethod def init_app(cls, app): @@ -119,9 +123,11 @@ class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-test.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'cssi-test.sqlite') + CELERY_BROKER_URL = os.environ.get( + 'TEST_CELERY_BROKER_URL', 'redis://localhost:6379') CELERY_BACKEND = os.environ.get('TEST_CELERY_BACKEND') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'celery-test.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'celery-test.sqlite') CELERY_CONFIG = {'CELERY_ALWAYS_EAGER': True} SOCKETIO_MESSAGE_QUEUE = None @@ -143,9 +149,11 @@ class ProductionConfig(Config): """ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'cssi.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'cssi.sqlite') + CELERY_BROKER_URL = os.environ.get( + 'CELERY_BROKER_URL', 'redis://localhost:6379') CELERY_BACKEND = os.environ.get('CELERY_BACKEND') or \ - 'sqlite:///' + os.path.join(BASE_DIR, 'celery.sqlite') + 'sqlite:///' + os.path.join(BASE_DIR, 'celery.sqlite') SSL_DISABLE = (os.environ.get('SSL_DISABLE') or 'True') == 'True' @classmethod diff --git a/config/logging.conf b/config/logging.conf index 9e8042c..9b6433c 100644 --- a/config/logging.conf +++ b/config/logging.conf @@ -1,5 +1,5 @@ [loggers] -keys=root,cssRestApi +keys=root,cssiapi [handlers] keys=consoleHandler, fileHandler @@ -11,10 +11,10 @@ keys=formatter level=DEBUG handlers=consoleHandler -[logger_cssRestApi] +[logger_cssiapi] level=DEBUG handlers=fileHandler -qualname=CSSI_REST_API +qualname=cssi.api propagate=0 [handler_consoleHandler] @@ -30,4 +30,4 @@ formatter=formatter args=('logs/api.log','a+', 5*1024*1024, 2, None, 0) [formatter_formatter] -format=%(asctime)s - %(name)s - %(levelname)-8s - [%(module)s.%(filename)s:%(lineno)d] - %(message)s \ No newline at end of file +format=%(asctime)s - %(name)s - %(levelname)-8s - [%(module)s.%(filename)s:%(lineno)d] - %(message)s \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f71db1..824d08f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,15 @@ +celery==4.3.0 eventlet==0.19.0 flask==0.12.2 flask_script==2.0.6 flask_migrate==2.1.1 -marshmallow==2.14.0 flask_sqlalchemy==2.3.2 flask_marshmallow==0.8.0 +marshmallow==2.14.0 marshmallow-sqlalchemy==0.13.2 -PyMySQL==0.9.3 +flake8==2.5.4 flask-cors -flask-socketio -celery==4.3.0 -redis==3.2.0 \ No newline at end of file +flask-socketio==3.3.2 +PyMySQL==0.9.3 +redis==3.2.1 +Pillow \ No newline at end of file From c21558e9d7a456318f6790986d9b1f4dc88635f3 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 17:45:56 +0530 Subject: [PATCH 23/37] chore(migrations): update migrations :alembic: --- migrations/versions/926a10218c82_.py | 34 +++++++++++++++++++ .../{883bbdba07f8_.py => e8e7340162d7_.py} | 6 ++-- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/926a10218c82_.py rename migrations/versions/{883bbdba07f8_.py => e8e7340162d7_.py} (97%) diff --git a/migrations/versions/926a10218c82_.py b/migrations/versions/926a10218c82_.py new file mode 100644 index 0000000..3710bc6 --- /dev/null +++ b/migrations/versions/926a10218c82_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 926a10218c82 +Revises: e8e7340162d7 +Create Date: 2019-04-24 17:38:12.147730 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '926a10218c82' +down_revision = 'e8e7340162d7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key('fk_type_id', 'application', 'application_type', ['type_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_genre_id', 'application', 'genre', ['genre_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_questionnaire_id', 'session', 'questionnaire', ['questionnaire_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_app_id', 'session', 'application', ['app_id'], ['id'], use_alter=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_app_id', 'session', type_='foreignkey') + op.drop_constraint('fk_questionnaire_id', 'session', type_='foreignkey') + op.drop_constraint('fk_genre_id', 'application', type_='foreignkey') + op.drop_constraint('fk_type_id', 'application', type_='foreignkey') + # ### end Alembic commands ### diff --git a/migrations/versions/883bbdba07f8_.py b/migrations/versions/e8e7340162d7_.py similarity index 97% rename from migrations/versions/883bbdba07f8_.py rename to migrations/versions/e8e7340162d7_.py index be763e4..409d2b9 100644 --- a/migrations/versions/883bbdba07f8_.py +++ b/migrations/versions/e8e7340162d7_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 883bbdba07f8 +Revision ID: e8e7340162d7 Revises: -Create Date: 2019-04-21 13:31:10.379141 +Create Date: 2019-04-24 17:10:02.488638 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '883bbdba07f8' +revision = 'e8e7340162d7' down_revision = None branch_labels = None depends_on = None From 5a17ae40ff7dd432852fda3335d13093175947a9 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 17:48:52 +0530 Subject: [PATCH 24/37] chore(vcs): ignore shape predictor and add readme for download info :see_no_evil: --- .gitignore | 3 +++ app/data/classifiers/readme.txt | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 app/data/classifiers/readme.txt diff --git a/.gitignore b/.gitignore index 340c1a4..cb9611b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Ignoring shape predictor +app/data/classifiers/shape_predictor_68_face_landmarks.dat + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/app/data/classifiers/readme.txt b/app/data/classifiers/readme.txt new file mode 100644 index 0000000..177460f --- /dev/null +++ b/app/data/classifiers/readme.txt @@ -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 \ No newline at end of file From 9801db11034115e7ccf60b573cd4f18c4c1426b0 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 18:35:38 +0530 Subject: [PATCH 25/37] chore(events): add sentiment score persistance --- app/events.py | 61 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/app/events.py b/app/events.py index f4f7b8e..635efd4 100644 --- a/app/events.py +++ b/app/events.py @@ -6,12 +6,25 @@ from io import BytesIO from PIL import Image from celery import chain + from app.models import Session from . import socketio, celery, cssi, db 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) + + @socketio.on("test/start") def on_test_start(head_frame, scene_frame, session_id, latency_interval=2): _head_frame = head_frame["head_frame"] @@ -28,9 +41,27 @@ def on_test_start(head_frame, scene_frame, session_id, latency_interval=2): _scene_frame_image_data = bytes(_scene_frame_image_data, encoding="ascii") _scene_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_scene_frame_image_data)))) - latency_workflow = chain(persist_prev_frames.s(_head_frame_decoded, _scene_frame_decoded, latency_interval), calculate_latency.s(_head_frame_decoded, _scene_frame_decoded, session_id["session_id"])).apply_async(expires=10) + latency_workflow = chain( + persist_prev_frames.s(_head_frame_decoded, _scene_frame_decoded, latency_interval), + calculate_latency.s(_head_frame_decoded, _scene_frame_decoded, session_id["session_id"]) + ).apply_async(expires=10) + + sentiment_workflow = 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) - sentiment_workflow = record_sentiment.apply_async(args=[_head_frame_decoded], expires=10) + # stop all celery workers + celery.control.purge() @socketio.on("disconnect") @@ -38,6 +69,9 @@ def on_disconnect(): """A Socket.IO client has disconnected.""" print("Socket.IO Client Disconnected") + # stop all celery workers + celery.control.purge() + @celery.task def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id): @@ -48,7 +82,9 @@ def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id) _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=pre_head_frame) _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=curr_head_frame) - _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, second_frame=curr_scene_frame, crop=True, crop_direction='horizontal') + _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, + second_frame=curr_scene_frame, crop=True, + crop_direction='horizontal') head_angles = [[phf_pitch, phf_yaw, phf_roll], [chf_pitch, chf_yaw, chf_roll]] camera_angles = [sf_pitch, sf_yaw, sf_roll] @@ -61,24 +97,31 @@ def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id) print('Celery calculate_latency Task - Scene Frame : {0}, {1}, {2}'.format(sf_pitch, sf_yaw, sf_roll)) print('Celery calculate_latency Task - Latency Score : {0}'.format(latency_score)) - new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'score': latency_score} - session = Session.query.filter_by(id=session_id).first() if session is not None: + new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'score': latency_score} session.latency_scores.append(new_score) print('New Scores: {}'.format(session.latency_scores)) db.session.commit() @celery.task -def record_sentiment(head_frame): +def record_sentiment(head_frame, session_id): """Sample celery task that posts a message.""" - sentiment = cssi.sentiment.detect_emotions(frame=head_frame) - print('From celery evaluate_sentiment: {0}'.format(sentiment)) + from .wsgi_aux import app + with app.app_context(): + sentiment = cssi.sentiment.detect_emotions(frame=head_frame) + + session = Session.query.filter_by(id=session_id).first() + if session is not None: + if sentiment is not None: + new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'sentiment': sentiment} + session.sentiment_scores.append(new_score) + print('New Scores: {}'.format(session.sentiment_scores)) + db.session.commit() @celery.task def persist_prev_frames(head_frame, scene_frame, interval): time.sleep(interval) return head_frame, scene_frame - From 1a21a621c0d2bc855ddad9dd504cffe5112cce1b Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 18:46:27 +0530 Subject: [PATCH 26/37] refactor(events): move celery tasks to a separate file :truck: --- app/events.py | 75 ++++--------------------------- app/tasks.py | 119 ++++++++++++++++---------------------------------- 2 files changed, 47 insertions(+), 147 deletions(-) diff --git a/app/events.py b/app/events.py index 635efd4..d36f7d4 100644 --- a/app/events.py +++ b/app/events.py @@ -1,14 +1,13 @@ -import time import logging import base64 import numpy as np -from datetime import datetime from io import BytesIO from PIL import Image from celery import chain from app.models import Session -from . import socketio, celery, cssi, db +from . import socketio, celery, db +from .tasks import calculate_latency, persist_prev_frames, record_sentiment logger = logging.getLogger('cssi.api') @@ -23,6 +22,7 @@ def on_test_init(session_id): 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") @@ -41,12 +41,12 @@ def on_test_start(head_frame, scene_frame, session_id, latency_interval=2): _scene_frame_image_data = bytes(_scene_frame_image_data, encoding="ascii") _scene_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_scene_frame_image_data)))) - latency_workflow = chain( + chain( persist_prev_frames.s(_head_frame_decoded, _scene_frame_decoded, latency_interval), calculate_latency.s(_head_frame_decoded, _scene_frame_decoded, session_id["session_id"]) ).apply_async(expires=10) - sentiment_workflow = record_sentiment.apply_async(args=[_head_frame_decoded, session_id["session_id"]], expires=10) + record_sentiment.apply_async(args=[_head_frame_decoded, session_id["session_id"]], expires=10) @socketio.on("test/stop") @@ -59,69 +59,12 @@ def on_test_stop(session_id): session.status = 'completed' db.session.commit() socketio.send({"status": "success", "message": "The test session completed successfully."}, json=True) - - # stop all celery workers - celery.control.purge() + 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.""" - print("Socket.IO Client Disconnected") - - # stop all celery workers - celery.control.purge() - - -@celery.task -def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id): - print('Session id: {}'.format(session_id)) - from .wsgi_aux import app - with app.app_context(): - pre_head_frame, prev_scene_frame = pre_frames - - _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=pre_head_frame) - _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=curr_head_frame) - _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, - second_frame=curr_scene_frame, crop=True, - crop_direction='horizontal') - - head_angles = [[phf_pitch, phf_yaw, phf_roll], [chf_pitch, chf_yaw, chf_roll]] - camera_angles = [sf_pitch, sf_yaw, sf_roll] - - latency_score = cssi.latency.generate_score(head_angles=head_angles, camera_angles=camera_angles) - - print( - 'Celery calculate_latency Task - Previous Head Frame : {0}, {1}, {2}'.format(phf_pitch, phf_yaw, phf_roll)) - print('Celery calculate_latency Task - Current Head Frame : {0}, {1}, {2}'.format(chf_pitch, chf_yaw, chf_roll)) - print('Celery calculate_latency Task - Scene Frame : {0}, {1}, {2}'.format(sf_pitch, sf_yaw, sf_roll)) - print('Celery calculate_latency Task - Latency Score : {0}'.format(latency_score)) - - session = Session.query.filter_by(id=session_id).first() - if session is not None: - new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'score': latency_score} - session.latency_scores.append(new_score) - print('New Scores: {}'.format(session.latency_scores)) - db.session.commit() - - -@celery.task -def record_sentiment(head_frame, session_id): - """Sample celery task that posts a message.""" - from .wsgi_aux import app - with app.app_context(): - sentiment = cssi.sentiment.detect_emotions(frame=head_frame) - - session = Session.query.filter_by(id=session_id).first() - if session is not None: - if sentiment is not None: - new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'sentiment': sentiment} - session.sentiment_scores.append(new_score) - print('New Scores: {}'.format(session.sentiment_scores)) - db.session.commit() - - -@celery.task -def persist_prev_frames(head_frame, scene_frame, interval): - time.sleep(interval) - return head_frame, scene_frame + celery.control.purge() # stop all celery workers + logger.info("The Socket.IO client disconnected.") diff --git a/app/tasks.py b/app/tasks.py index 9665aed..5c33aaf 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -1,95 +1,52 @@ -from functools import wraps -try: - from io import BytesIO -except ImportError: # pragma: no cover - from cStringIO import StringIO as BytesIO +import time +import logging +from datetime import datetime -from flask import Blueprint, abort, g, request -from werkzeug.exceptions import InternalServerError -from celery import states +from . import celery, cssi, db +from app.models import Session -from . import celery -from .utils import url_for - -text_types = (str, bytes) -try: - text_types += (unicode,) -except NameError: - # no unicode on Python 3 - pass - -tasks = Blueprint('tasks', __name__) +logger = logging.getLogger('cssi.api') @celery.task -def run_flask_request(environ): +def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id): from .wsgi_aux import app + with app.app_context(): + pre_head_frame, prev_scene_frame = pre_frames - if '_wsgi.input' in environ: - environ['wsgi.input'] = BytesIO(environ['_wsgi.input']) - - # Create a request context similar to that of the original request - # so that the task can have access to flask.g, flask.request, etc. - with app.request_context(environ): - # Record the fact that we are running in the Celery worker now - g.in_celery = True - - # Run the route function and record the response - try: - rv = app.full_dispatch_request() - except: - # If we are in debug mode we want to see the exception - # Else, return a 500 error - if app.debug: - raise - rv = app.make_response(InternalServerError()) - return (rv.get_data(), rv.status_code, rv.headers) + _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=pre_head_frame) + _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=curr_head_frame) + _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, + second_frame=curr_scene_frame, crop=True, + crop_direction='horizontal') + head_angles = [[phf_pitch, phf_yaw, phf_roll], [chf_pitch, chf_yaw, chf_roll]] + camera_angles = [sf_pitch, sf_yaw, sf_roll] -def async_task(f): - """ - This decorator transforms a sync route to asynchronous by running it - in a background thread. - """ - @wraps(f) - def wrapped(*args, **kwargs): - # If we are already running the request on the celery side, then we - # just call the wrapped function to allow the request to execute. - if getattr(g, 'in_celery', False): - return f(*args, **kwargs) + latency_score = cssi.latency.generate_score(head_angles=head_angles, camera_angles=camera_angles) - # If we are on the Flask side, we need to launch the Celery task, - # passing the request environment, which will be used to reconstruct - # the request object. The request body has to be handled as a special - # case, since WSGI requires it to be provided as a file-like object. - environ = {k: v for k, v in request.environ.items() - if isinstance(v, text_types)} - if 'wsgi.input' in request.environ: - environ['_wsgi.input'] = request.get_data() - t = run_flask_request.apply_async(args=(environ,)) + session = Session.query.filter_by(id=session_id).first() + if session is not None: + new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'score': latency_score} + session.latency_scores.append(new_score) + db.session.commit() - # Return a 202 response, with a link that the client can use to - # obtain task status that is based on the Celery task id. - if t.state == states.PENDING or t.state == states.RECEIVED or \ - t.state == states.STARTED: - return '', 202, {'Location': url_for('tasks.get_status', id=t.id)} - # If the task already finished, return its return value as response. - # This would be the case when CELERY_ALWAYS_EAGER is set to True. - return t.info - return wrapped +@celery.task +def record_sentiment(head_frame, session_id): + """Sample celery task that posts a message.""" + from .wsgi_aux import app + with app.app_context(): + sentiment = cssi.sentiment.detect_emotions(frame=head_frame) + session = Session.query.filter_by(id=session_id).first() + if session is not None: + if sentiment is not None: + new_score = {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'sentiment': sentiment} + session.sentiment_scores.append(new_score) + db.session.commit() -@tasks.route('/status/', methods=['GET']) -def get_status(id): - """ - Return status about an asynchronous task. If this request returns a 202 - status code, it means that task hasn't finished yet. Else, the response - from the task is returned. - """ - task = run_flask_request.AsyncResult(id) - if task.state == states.PENDING: - abort(404) - if task.state == states.RECEIVED or task.state == states.STARTED: - return '', 202, {'Location': url_for('tasks.get_status', id=id)} - return task.info +@celery.task +def persist_prev_frames(head_frame, scene_frame, interval): + time.sleep(interval) + return head_frame, scene_frame From 9df36763cf735455979f9f1100bf7d440c4d0bab Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Wed, 24 Apr 2019 18:54:20 +0530 Subject: [PATCH 27/37] chore(core): import tasks to the base file --- app/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 973f4f4..a9c2e85 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -44,7 +44,7 @@ from app.models import * # noqa # Import celery task to register them with Celery workers -from .tasks import run_flask_request # noqa +from . import tasks # noqa # Import Socket.IO events to register them with Flask-SocketIO from . import events # noqa @@ -92,8 +92,4 @@ def create_app(config_name=None, main=True): from app.routes.v1 import questionnaire as questionnaire_blueprint app.register_blueprint(questionnaire_blueprint, url_prefix=root + '/questionnaires') - # Register async tasks support - from .tasks import tasks as tasks_blueprint - app.register_blueprint(tasks_blueprint, url_prefix='/tasks') - return app From 45b351ba1aef2085bb39a1f56aa1d316a286ce8e Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 3 May 2019 23:53:43 +0530 Subject: [PATCH 28/37] chore(questionnaire): add PATCH request to update questionnaire --- app/routes/v1/questionnaire.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/routes/v1/questionnaire.py b/app/routes/v1/questionnaire.py index b4f9e0d..be21216 100644 --- a/app/routes/v1/questionnaire.py +++ b/app/routes/v1/questionnaire.py @@ -44,6 +44,17 @@ def get_questionnaire(id): return jsonify({'status': 'success', 'message': None, 'data': result}), 200 +@questionnaire.route('//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(): From a8e47c7d2d140cbe44182be1aa8bef2ef56bd3d8 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 3 May 2019 23:54:13 +0530 Subject: [PATCH 29/37] chore(utils): add function to decode base63 strings --- app/utils.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/utils.py b/app/utils.py index d757746..b670dfd 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,4 +1,7 @@ import time +import base64 +from io import BytesIO +from PIL import Image from flask import url_for as _url_for, current_app, _request_ctx_stack @@ -22,3 +25,14 @@ def url_for(*args, **kwargs): with current_app.test_request_context(): return _url_for(*args, **kwargs) return _url_for(*args, **kwargs) + + +def decode_base64(base64_str): + """decodes a base64 image string""" + starter = base64_str.find(',') + image_data = base64_str[starter + 1:] + image_data = bytes(image_data, encoding="ascii") + image = Image.open(BytesIO(base64.b64decode(image_data))) + if image.mode != "RGB": + image = image.convert("RGB") + return image From 4d33cde24717ee05f87450eb1990030c09d7afbf Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 3 May 2019 23:54:49 +0530 Subject: [PATCH 30/37] chore(tasks): add logic to persist stream of frames on Redis --- app/tasks.py | 98 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/app/tasks.py b/app/tasks.py index 5c33aaf..052724d 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -1,29 +1,53 @@ -import time +import json import logging +import redis from datetime import datetime from . import celery, cssi, db from app.models import Session +from .utils import decode_base64 logger = logging.getLogger('cssi.api') @celery.task -def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id): +def calculate_latency(session_id, limit): + """Celery task which handles latency score generation and persistence""" from .wsgi_aux import app with app.app_context(): - pre_head_frame, prev_scene_frame = pre_frames + head_key = "head-frames" + scene_key = "scene-frames" - _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=pre_head_frame) - _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=curr_head_frame) - _, _, sf_pitch, sf_yaw, sf_roll = cssi.latency.calculate_camera_pose(first_frame=prev_scene_frame, - second_frame=curr_scene_frame, crop=True, - crop_direction='horizontal') + r = redis.StrictRedis(host='localhost', port=6379, db=0) + head_frames_raw = get_frames_from_redis(r=r, key=head_key, limit=limit) + scene_frames_raw = get_frames_from_redis(r=r, key=scene_key, limit=limit) + + head_stream = [] + scene_stream = [] + + for data in head_frames_raw: + head_stream.append(decode_base64(data)) + + for data in scene_frames_raw: + scene_stream.append(decode_base64(data)) + + _, phf_pitch, phf_yaw, phf_roll = cssi.latency.calculate_head_pose(frame=head_stream[0]) + _, chf_pitch, chf_yaw, chf_roll = cssi.latency.calculate_head_pose(frame=head_stream[1]) + _, _, ff_angles, sf_angles = cssi.latency.calculate_camera_pose(first_frame=scene_stream[0], + second_frame=scene_stream[1], crop=True, + crop_direction='horizontal') head_angles = [[phf_pitch, phf_yaw, phf_roll], [chf_pitch, chf_yaw, chf_roll]] - camera_angles = [sf_pitch, sf_yaw, sf_roll] + camera_angles = [ff_angles, sf_angles] + + latency_score = cssi.latency.generate_rotation_latency_score(head_angles=head_angles, + camera_angles=camera_angles) - latency_score = cssi.latency.generate_score(head_angles=head_angles, camera_angles=camera_angles) + head_movement = cssi.latency.check_for_head_movement(head_stream) + logger.debug("Head movement detected: {0}".format(head_movement)) + + pst = cssi.latency.calculate_pst(scene_stream, 10) + logger.debug("Pixel switching time: {0}".format(pst)) session = Session.query.filter_by(id=session_id).first() if session is not None: @@ -34,10 +58,10 @@ def calculate_latency(pre_frames, curr_head_frame, curr_scene_frame, session_id) @celery.task def record_sentiment(head_frame, session_id): - """Sample celery task that posts a message.""" + """Celery task which handles sentiment score generation and persistence""" from .wsgi_aux import app with app.app_context(): - sentiment = cssi.sentiment.detect_emotions(frame=head_frame) + sentiment = cssi.sentiment.generate_sentiment_score(frame=head_frame) session = Session.query.filter_by(id=session_id).first() if session is not None: if sentiment is not None: @@ -47,6 +71,50 @@ def record_sentiment(head_frame, session_id): @celery.task -def persist_prev_frames(head_frame, scene_frame, interval): - time.sleep(interval) - return head_frame, scene_frame +def persist_frames(head_frame, scene_frame, limit): + r = redis.StrictRedis(host='localhost', port=6379, db=0) + is_complete = save_frames_on_redis(r=r, head_frame=head_frame, scene_frame=scene_frame, limit=limit) + return is_complete + + +def save_frames_on_redis(r, head_frame, scene_frame, limit): + """Store dictionary on redis""" + head_key = "head-frames" + scene_key = "scene-frames" + head_frame_count = 0 + scene_frame_count = 0 + if r.exists(head_key) and r.exists(scene_key): + head_values = json.loads(r.get(head_key)) + head_values.append(head_frame) + head_frame_count = len(head_values) + + scene_values = json.loads(r.get(scene_key)) + scene_values.append(scene_frame) + scene_frame_count = len(scene_values) + else: + head_values = [head_frame] + scene_values = [scene_frame] + + r = redis.StrictRedis(host='localhost') + + r.set(head_key, json.dumps(head_values)) + r.set(scene_key, json.dumps(scene_values)) + + if head_frame_count and scene_frame_count >= limit: + return True + + return False + + +def get_frames_from_redis(r, key, limit): + count = 0 + frames = [] + if r.exists(key): + frames = json.loads(r.get(key)) + count = len(frames) + + if count >= limit: + r.delete(key) + logger.debug("Cleaning frame stream - key: {0}".format(key)) + + return frames From 614658abdfdd5e8dc325f4bdd8a6b810b968e65e Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 3 May 2019 23:55:19 +0530 Subject: [PATCH 31/37] refactor(events): modify celery task init logic --- app/events.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/app/events.py b/app/events.py index d36f7d4..c34dc4b 100644 --- a/app/events.py +++ b/app/events.py @@ -1,13 +1,9 @@ import logging -import base64 -import numpy as np -from io import BytesIO -from PIL import Image -from celery import chain from app.models import Session from . import socketio, celery, db -from .tasks import calculate_latency, persist_prev_frames, record_sentiment +from .tasks import calculate_latency, persist_frames, record_sentiment +from .utils import decode_base64 logger = logging.getLogger('cssi.api') @@ -29,22 +25,20 @@ def on_test_init(session_id): 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 base64 string to opencv compatible format - _head_frame_starter = _head_frame.find(',') - _head_frame_image_data = _head_frame[_head_frame_starter + 1:] - _head_frame_image_data = bytes(_head_frame_image_data, encoding="ascii") - _head_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_head_frame_image_data)))) + # decoding head-frame image(base64) string to OpenCV compatible format + _head_frame_decoded = decode_base64(_head_frame) - _scene_frame_starter = _scene_frame.find(',') - _scene_frame_image_data = _scene_frame[_scene_frame_starter + 1:] - _scene_frame_image_data = bytes(_scene_frame_image_data, encoding="ascii") - _scene_frame_decoded = np.array(Image.open(BytesIO(base64.b64decode(_scene_frame_image_data)))) + # decoding scene-frame image(base64) string to OpenCV compatible format + _scene_frame_decoded = decode_base64(_scene_frame) - chain( - persist_prev_frames.s(_head_frame_decoded, _scene_frame_decoded, latency_interval), - calculate_latency.s(_head_frame_decoded, _scene_frame_decoded, session_id["session_id"]) - ).apply_async(expires=10) + # 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) From 2a4c30b5047cdbfaac97aefa2de970bbfeb0f506 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Fri, 3 May 2019 23:55:51 +0530 Subject: [PATCH 32/37] chore(deps): add numpy dependency :heavy_plus_sign: --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 824d08f..83b48ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ flask-cors flask-socketio==3.3.2 PyMySQL==0.9.3 redis==3.2.1 -Pillow \ No newline at end of file +Pillow +numpy>=1.16.2 \ No newline at end of file From fe32273c02e79390d41bee780a45ea7e61c3d775 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sat, 4 May 2019 00:28:59 +0530 Subject: [PATCH 33/37] chore(config): add cssi config :wrench: --- config.cssi | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 config.cssi diff --git a/config.cssi b/config.cssi new file mode 100644 index 0000000..d0226b4 --- /dev/null +++ b/config.cssi @@ -0,0 +1,20 @@ +[run] +plugins = + heartrate.plugin + ecg.plugin + +[latency] +latency_weight = 50 +latency_boundary = 3 + +[sentiment] +sentiment_weight = 30 + +[questionnaire] +questionnaire_weight = 20 + +[heartrate.plugin] +weight = 0 + +[ecg.plugin] +weight = 0 \ No newline at end of file From a80df96b46c76df5931003ef9d0e1175537a71dc Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sat, 4 May 2019 00:29:57 +0530 Subject: [PATCH 34/37] refactcor: rename cssi config file param --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index a9c2e85..adf9907 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,7 +31,7 @@ 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="cssi.rc") +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() From a20ef01b09732739ce66b7266f568d29162f267e Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sat, 4 May 2019 00:30:44 +0530 Subject: [PATCH 35/37] chore(sessions): add PUT method for session complete handling --- app/routes/v1/session.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py index 69b3735..cf70f21 100644 --- a/app/routes/v1/session.py +++ b/app/routes/v1/session.py @@ -20,6 +20,8 @@ from app.models import Session, SessionSchema, Application, Questionnaire from app import db +from app import cssi + session = Blueprint('session', __name__) session_schema = SessionSchema(strict=True) @@ -40,10 +42,34 @@ def get_sessions_list(): def get_session(id): """Get info on a session when an id is passed in""" session = Session.query.get(id) - result = sessions_schema.dump(session).data + result = session_schema.dump(session).data return jsonify({'status': 'success', 'message': None, 'data': result}), 200 +@session.route('/', methods=['PUT']) +@cross_origin(supports_credentials=True) +def update_session(id): + """Update information when the session comes to an end.""" + session = Session.query.get(id) + + latency_score = cssi.latency.generate_final_score(scores=session.latency_scores) + sentiment_score = cssi.sentiment.generate_final_score(all_emotions=session.sentiment_scores, expected_emotions=session.expected_emotions) + questionnaire_score = cssi.questionnaire.generate_final_score(pre=session.questionnaire.pre, post=session.questionnaire.post) + cssi_score = cssi.generate_cssi_score(tl=latency_score, ts=sentiment_score, tq=questionnaire_score) + + session.total_latency_score = latency_score + session.total_sentiment_score = sentiment_score + session.total_questionnaire_score = questionnaire_score + session.cssi_score = cssi_score + + session.status = "completed" + db.session.commit() + + result = session_schema.dump(session).data + + return jsonify({'status': 'success', 'message': 'Successfully updated the session data', 'data': result}), 200 + + @session.route('/', methods=['POST']) @cross_origin(supports_credentials=True) def create_session(): From 941e74c6facaa29f9900a5b6bf22492cc7fbcbfb Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sat, 4 May 2019 00:55:59 +0530 Subject: [PATCH 36/37] chore(sessions): update session PUT to include questionnaire breakdown --- app/routes/v1/session.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py index cf70f21..d05ed8e 100644 --- a/app/routes/v1/session.py +++ b/app/routes/v1/session.py @@ -52,16 +52,36 @@ def update_session(id): """Update information when the session comes to an end.""" session = Session.query.get(id) + # get all the final scores latency_score = cssi.latency.generate_final_score(scores=session.latency_scores) sentiment_score = cssi.sentiment.generate_final_score(all_emotions=session.sentiment_scores, expected_emotions=session.expected_emotions) questionnaire_score = cssi.questionnaire.generate_final_score(pre=session.questionnaire.pre, post=session.questionnaire.post) cssi_score = cssi.generate_cssi_score(tl=latency_score, ts=sentiment_score, tq=questionnaire_score) + # set the scores in the session session.total_latency_score = latency_score session.total_sentiment_score = sentiment_score session.total_questionnaire_score = questionnaire_score session.cssi_score = cssi_score + # get a breakdown of the questionnaire scores and set it in the session + [pre_n, pre_o, pre_d, pre_ts], [post_n, post_o, post_d, post_ts] = cssi.questionnaire.generate_score_breakdown(pre=session.questionnaire.pre, post=session.questionnaire.post) + q_score_breakdown = { + "pre": { + "N": pre_n, + "O": pre_o, + "D": pre_d, + "TS": pre_ts + }, + "post": { + "N": post_n, + "O": post_o, + "D": post_d, + "TS": post_ts + } + } + session.questionnaire_scores = q_score_breakdown + session.status = "completed" db.session.commit() From 331624bc05d3325b473c548117677c5ebd96b5d0 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Sat, 4 May 2019 01:00:13 +0530 Subject: [PATCH 37/37] chore(sessions): add PATCH request to update session status --- app/routes/v1/session.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py index d05ed8e..bed592f 100644 --- a/app/routes/v1/session.py +++ b/app/routes/v1/session.py @@ -90,6 +90,17 @@ def update_session(id): return jsonify({'status': 'success', 'message': 'Successfully updated the session data', 'data': result}), 200 +@session.route('//status', methods=['PATCH']) +@cross_origin(supports_credentials=True) +def update_session_status(id): + """Update session status""" + session = Session.query.get(id) + session.status = request.json['status'] + db.session.commit() + result = session_schema.dump(session).data + return jsonify({'status': 'success', 'message': 'Successfully update the session status', 'data': result}), 200 + + @session.route('/', methods=['POST']) @cross_origin(supports_credentials=True) def create_session():