Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement API for EvaluationResult protocol #55

Merged
merged 14 commits into from
Jan 22, 2019
2 changes: 2 additions & 0 deletions drucker_dashboard/apis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def init_app(self, app, **kwargs):
from .api_model import mdl_info_namespace
from .api_misc import misc_info_namespace
from .api_admin import admin_info_namespace
from .api_evaluation import eval_info_namespace


@api.errorhandler(ApiException)
Expand All @@ -79,4 +80,5 @@ def default_error_handler(error):
api.add_namespace(srv_info_namespace, path='/api/applications')
api.add_namespace(mdl_info_namespace, path='/api/applications')
api.add_namespace(admin_info_namespace, path='/api/applications')
api.add_namespace(eval_info_namespace, path='/api/applications')
api.add_namespace(misc_info_namespace, path='/api')
60 changes: 1 addition & 59 deletions drucker_dashboard/apis/api_application.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import uuid
import datetime

from flask_jwt_simple import get_jwt_identity
from flask_restplus import Namespace, fields, Resource, reqparse
from werkzeug.datastructures import FileStorage

from . import api
from drucker_dashboard import DruckerDashboardClient
from drucker_dashboard.models import db, Application, Service, Evaluation, EvaluationResult, ApplicationUserRole, Role, User
from drucker_dashboard.models import db, Application, Service, ApplicationUserRole, Role, User
from drucker_dashboard.apis import DatetimeToTimestamp
from drucker_dashboard.utils import HashUtil


app_info_namespace = Namespace('applications', description='Application Endpoint.')
Expand Down Expand Up @@ -143,58 +140,3 @@ def patch(self, application_id:int):
db.session.commit()
db.session.close()
return response_body

@app_info_namespace.route('/<int:application_id>/evaluation')
class ApiEvaluation(Resource):
upload_parser = reqparse.RequestParser()
upload_parser.add_argument('file', location='files', type=FileStorage, required=True)

@app_info_namespace.expect(upload_parser)
def post(self, application_id:int):
"""update data to be evaluated"""
args = self.upload_parser.parse_args()
file = args['file']
checksum = HashUtil.checksum(file)

eobj = db.session.query(Evaluation).filter(
Evaluation.application_id == application_id,
Evaluation.checksum == checksum).one_or_none()
if eobj is not None:
return {"status": True, "evaluation_id": eobj.evaluation_id}

eval_data_path = "eval-{0:%Y%m%d%H%M%S}.txt".format(datetime.datetime.utcnow())

sobj = Service.query.filter_by(application_id=application_id).first_or_404()

drucker_dashboard_application = DruckerDashboardClient(logger=api.logger, host=sobj.host)
response_body = drucker_dashboard_application.run_upload_evaluation_data(file, eval_data_path)

if not response_body['status']:
raise Exception('Failed to upload')
eobj = Evaluation(checksum=checksum, application_id=application_id, data_path=eval_data_path)
db.session.add(eobj)
db.session.flush()
evaluation_id = eobj.evaluation_id
db.session.commit()
db.session.close()

return {"status": True, "evaluation_id": evaluation_id}


@app_info_namespace.route('/<int:application_id>/evaluation/<int:evaluation_id>')
class ApiEvaluation(Resource):
def delete(self, application_id:int, evaluation_id:int):
"""delete data to be evaluated"""
eval_query = db.session.query(Evaluation)\
.filter(Evaluation.application_id == application_id,
Evaluation.evaluation_id == evaluation_id)
if eval_query.one_or_none() is None:
return {"status": False}, 404

eval_query.delete()
db.session.query(EvaluationResult)\
.filter(EvaluationResult.evaluation_id == evaluation_id).delete()
db.session.commit()
db.session.close()

return {"status": True, "message": "Success."}
191 changes: 191 additions & 0 deletions drucker_dashboard/apis/api_evaluation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import datetime
import json
from itertools import chain

from flask_restplus import Namespace, fields, Resource, reqparse
from werkzeug.datastructures import FileStorage

from . import api
from drucker_dashboard.models import db, Service, Evaluation, EvaluationResult
from drucker_dashboard.drucker_dashboard_client import DruckerDashboardClient
from drucker_dashboard.utils.hash_util import HashUtil


eval_info_namespace = Namespace('evaluation', description='Evaluation Endpoint.')
success_or_not = eval_info_namespace.model('Success', {
'status': fields.Boolean(required=True),
'message': fields.String(required=True)
})
eval_metrics = eval_info_namespace.model('Evaluation result', {
'num': fields.Integer(required=True, description='number of evaluated data'),
'accuracy': fields.Float(required=True, description='accuracy of evaluation'),
'fvalue': fields.List(fields.Float, required=True, description='F-value of evaluation'),
'precision': fields.List(fields.Float, required=True, description='precision of evaluation'),
'recall': fields.List(fields.Float, required=True, description='recall of evaluation'),
'option': fields.Raw(),
'status': fields.Boolean(required=True),
'result_id': fields.Integer(required=True, description='ID of evaluation result')
})
eval_data_upload = eval_info_namespace.model('Result of uploading evaluation data', {
'status': fields.Boolean(required=True),
'evaluation_id': fields.Integer(required=True, description='ID of uploaded data')
})


@eval_info_namespace.route('/<int:application_id>/evaluation')
class ApiEvaluation(Resource):
upload_parser = reqparse.RequestParser()
upload_parser.add_argument('file', location='files', type=FileStorage, required=True)

@eval_info_namespace.expect(upload_parser)
@eval_info_namespace.marshal_with(eval_data_upload)
def post(self, application_id:int):
"""update data to be evaluated"""
args = self.upload_parser.parse_args()
file = args['file']
checksum = HashUtil.checksum(file)

eobj = db.session.query(Evaluation).filter(
Evaluation.application_id == application_id,
Evaluation.checksum == checksum).one_or_none()
if eobj is not None:
return {"status": True, "evaluation_id": eobj.evaluation_id}

eval_data_path = "eval-{0:%Y%m%d%H%M%S}.txt".format(datetime.datetime.utcnow())

sobj = Service.query.filter_by(application_id=application_id).first_or_404()

drucker_dashboard_application = DruckerDashboardClient(logger=api.logger, host=sobj.host)
response_body = drucker_dashboard_application.run_upload_evaluation_data(file, eval_data_path)

if not response_body['status']:
raise Exception('Failed to upload')
eobj = Evaluation(checksum=checksum, application_id=application_id, data_path=eval_data_path)
db.session.add(eobj)
db.session.flush()
evaluation_id = eobj.evaluation_id
db.session.commit()
db.session.close()

return {"status": True, "evaluation_id": evaluation_id}


@eval_info_namespace.route('/<int:application_id>/evaluation/<int:evaluation_id>')
class ApiEvaluation(Resource):

@eval_info_namespace.marshal_with(success_or_not)
def delete(self, application_id:int, evaluation_id:int):
"""delete data to be evaluated"""
eval_query = db.session.query(Evaluation)\
.filter(Evaluation.application_id == application_id,
Evaluation.evaluation_id == evaluation_id)
if eval_query.one_or_none() is None:
return {"status": False, "message": "Not Found."}, 404

eval_query.delete()
db.session.query(EvaluationResult)\
.filter(EvaluationResult.evaluation_id == evaluation_id).delete()
db.session.commit()
db.session.close()

return {"status": True, "message": "Success."}
keigohtr marked this conversation as resolved.
Show resolved Hide resolved


@eval_info_namespace.route('/<int:application_id>/evaluate')
class ApiEvaluate(Resource):
eval_parser = reqparse.RequestParser()
eval_parser.add_argument('service_id', location='form', type=int, required=True)
eval_parser.add_argument('evaluation_id', location='form', type=int, required=False)
eval_parser.add_argument('overwrite', location='form', type=bool, required=False)

@eval_info_namespace.expect(eval_parser)
@eval_info_namespace.marshal_with(eval_metrics)
def post(self, application_id:int):
"""evaluate"""
args = self.eval_parser.parse_args()
eval_id = args.get('evaluation_id', None)
service_id = args.get('service_id')
keigohtr marked this conversation as resolved.
Show resolved Hide resolved
if eval_id:
eobj = Evaluation.query.filter_by(
application_id=application_id,
evaluation_id=eval_id).first_or_404()
else:
# if evaluation_id is not given, use the lastest one.
eobj = Evaluation.query\
.filter_by(application_id=application_id)\
.order_by(Evaluation.register_date.desc()).first_or_404()
sobj = Service.query.filter_by(
application_id=application_id,
service_id=service_id).first_or_404()

robj = db.session.query(EvaluationResult)\
.filter(EvaluationResult.model_id == sobj.model_id,
EvaluationResult.evaluation_id == eobj.evaluation_id).one_or_none()
if robj is not None and args.get('overwrite', False):
return robj.result

eval_result_path = "eval-result-{0:%Y%m%d%H%M%S}.txt".format(datetime.datetime.utcnow())
drucker_dashboard_application = DruckerDashboardClient(logger=api.logger, host=sobj.host)
response_body = drucker_dashboard_application.run_evaluate_model(eobj.data_path, eval_result_path)

if response_body['status']:
result = json.dumps(response_body)
if robj is None:
robj = EvaluationResult(model_id=sobj.model_id,
data_path=eval_result_path,
evaluation_id=eobj.evaluation_id,
result=result)
db.session.add(robj)
else:
robj.data_path = eval_result_path
robj.result = result
db.session.flush()
response_body = robj.result
db.session.commit()
db.session.close()

return response_body


@eval_info_namespace.route('/<int:application_id>/evaluation_result/<int:eval_result_id>')
keigohtr marked this conversation as resolved.
Show resolved Hide resolved
class ApiEvaluationResult(Resource):

def get(self, application_id:int, eval_result_id:int):
"""get detailed evaluation result"""
eval_with_result = db.session.query(Evaluation, EvaluationResult)\
.filter(Evaluation.application_id == application_id,
EvaluationResult.evaluation_id == Evaluation.evaluation_id,
EvaluationResult.evaluation_result_id == eval_result_id).one_or_none()
if eval_with_result is None:
return {"status": False, "message": "Not Found."}, 404
sobj = Service.query.filter_by(application_id=application_id).first_or_404()
drucker_dashboard_application = DruckerDashboardClient(logger=api.logger, host=sobj.host)
eobj = eval_with_result.Evaluation
robj = eval_with_result.EvaluationResult

response_body = list(drucker_dashboard_application.run_evaluation_data(eobj.data_path, robj.data_path))
if len(response_body) == 0:
return {"status": False, "message": "Result Not Found."}, 404

return {
'status': all(r['status'] for r in response_body),
'metrics': response_body[0]['metrics'],
'details': list(chain.from_iterable(r['detail'] for r in response_body))
}

@eval_info_namespace.marshal_with(success_or_not)
def delete(self, application_id:int, eval_result_id:int):
"""get detailed evaluation result"""
eval_with_result = db.session.query(Evaluation, EvaluationResult)\
.filter(Evaluation.application_id == application_id,
EvaluationResult.evaluation_id == Evaluation.evaluation_id,
EvaluationResult.evaluation_result_id == eval_result_id).one_or_none()
if eval_with_result is None:
return {"status": False, "message": "Not Found."}, 404

db.session.query(EvaluationResult)\
.filter(EvaluationResult.evaluation_result_id == eval_result_id).delete()
db.session.commit()
db.session.close()
keigohtr marked this conversation as resolved.
Show resolved Hide resolved

return {"status": True, "message": "Success."}
57 changes: 2 additions & 55 deletions drucker_dashboard/apis/api_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import datetime
import json

from flask_restplus import Namespace, fields, Resource, reqparse

from . import api, DatetimeToTimestamp
from . import DatetimeToTimestamp
from .api_kubernetes import update_dbs_kubernetes, switch_drucker_service_model_assignment
from drucker_dashboard import DruckerDashboardClient
from drucker_dashboard.models import db, Kubernetes, Application, Service, EvaluationResult, Evaluation
from drucker_dashboard.models import db, Kubernetes, Application, Service


srv_info_namespace = Namespace('services', description='Service Endpoint.')
Expand Down Expand Up @@ -184,54 +182,3 @@ def delete(self, application_id:int, service_id:int):
db.session.commit()
db.session.close()
return response_body

@srv_info_namespace.route('/<int:application_id>/services/<int:service_id>/evaluate')
class ApiEvaluate(Resource):
eval_parser = reqparse.RequestParser()
eval_parser.add_argument('evaluation_id', location='form', type=int, required=False)
eval_parser.add_argument('overwrite', location='form', type=bool, required=False)

@srv_info_namespace.expect(eval_parser)
def post(self, application_id:int, service_id:int):
"""evaluate"""
args = self.eval_parser.parse_args()
eval_id = args.get('evaluation_id', None)
if eval_id:
eobj = Evaluation.query.filter_by(
application_id=application_id,
evaluation_id=eval_id).first_or_404()
else:
# if evaluation_id is not given, use the lastest one.
eobj = Evaluation.query\
.filter_by(application_id=application_id)\
.order_by(Evaluation.register_date.desc()).first_or_404()

sobj = Service.query.filter_by(
application_id=application_id,
service_id=service_id).first_or_404()

robj = db.session.query(EvaluationResult)\
.filter(EvaluationResult.model_id == sobj.model_id,
EvaluationResult.evaluation_id == eobj.evaluation_id).one_or_none()
if robj is not None and args.get('overwrite', False):
return json.loads(robj.result)

eval_result_path = "eval-result-{0:%Y%m%d%H%M%S}.txt".format(datetime.datetime.utcnow())
drucker_dashboard_application = DruckerDashboardClient(logger=api.logger, host=sobj.host)
response_body = drucker_dashboard_application.run_evaluate_model(eobj.data_path, eval_result_path)

if response_body['status']:
result = json.dumps(response_body)
if robj is None:
robj = EvaluationResult(model_id=sobj.model_id,
data_path=eval_result_path,
evaluation_id=eobj.evaluation_id,
result=result)
db.session.add(robj)
else:
robj.data_path = eval_result_path
robj.result = result
db.session.commit()
db.session.close()

return response_body
30 changes: 30 additions & 0 deletions drucker_dashboard/drucker_dashboard_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,33 @@ def run_upload_evaluation_data(self, f:FileStorage, data_path:str):
response = protobuf_to_dict(self.stub.UploadEvaluationData(request_iterator),
including_default_value_fields=True)
return response

def __get_value_from_io(self, io:drucker_pb2.IO):
if io.WhichOneof('io_oneof') == 'str':
val = io.str.val
else:
val = io.tensor.val

if len(val) == 1:
return val[0]
else:
return list(val)

@error_handling({"status": False})
def run_evaluation_data(self, data_path:str, result_path:str):
request = drucker_pb2.EvaluationResultRequest(data_path=data_path, result_path=result_path)
for raw_response in self.stub.EvaluationResult(request):
details = []
for detail in raw_response.detail:
details.append(dict(
protobuf_to_dict(detail, including_default_value_fields=True),
input=self.__get_value_from_io(detail.input),
label=self.__get_value_from_io(detail.label),
output=self.__get_value_from_io(detail.output),
score=detail.score[0] if len(detail.score) == 1 else list(detail.score)
))
response = protobuf_to_dict(raw_response,
including_default_value_fields=True)
response['detail'] = details
response['status'] = True
yield response
Loading