KarrLab/bcforms

View on GitHub
bcforms/rest.py

Summary

Maintainability
F
4 days
Test Coverage
A
97%
""" REST JSON API

:Author: Mike Zheng <xzheng20@colby.edu>
:Author: Jonathan Karr <karr@mssm.edu>
:Date: 2019-07-03
:Copyright: 2019, Karr Lab
:License: MIT
"""

import bcforms
import bcforms.core
import bpforms
from wc_utils.util.chem import EmpiricalFormula
import flask
import flask_restplus
import flask_restplus.errors
import flask_restplus.fields

# the max total length of bpforms-encoded subunits must be less than 50
max_len_get_structure = 50

# setup app
app = flask.Flask(__name__)


class PrefixMiddleware(object):
    def __init__(self, app, prefix=''):
        self.app = app
        self.prefix = prefix

    def __call__(self, environ, start_response):
        if environ['PATH_INFO'].startswith(self.prefix):
            environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):]
            environ['SCRIPT_NAME'] = self.prefix
            return self.app(environ, start_response)
        else:
            start_response('404', [('Content-Type', 'text/plain')])
            return ["This url does not belong to the app.".encode()]


app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix='/api')

api = flask_restplus.Api(app,
                         title='bcforms JSON REST API',
                         description='JSON REST API for calculating properties of biocomplex forms',
                         contact='info@karrlab.org',
                         version=bcforms.__version__,
                         license='MIT',
                         license_url='https://github.com/KarrLab/bcforms/blob/master/LICENSE',
                         doc='/')

bcform_ns = flask_restplus.Namespace('bcform', description='Calculate properties of biocomplex forms')
api.add_namespace(bcform_ns)

# define model

# if encoding, structure defined -> ignore formula, mol_wt, charge, and define them based on structure
# if neither encoding, structure set and formula is defined -> ignore mol_wt, and define mol_wt based on formula
subunit_fields = {}
subunit_fields['name'] = flask_restplus.fields.String(required=True, title='Subunit name', example='abc_a')
# encoding can be smiles, bpforms.ProteinForm, bpforms.DnaForm, bpforms.RnaForm
subunit_fields['encoding'] = flask_restplus.fields.String(required=False, title='Structure encoding', example='bpforms.ProteinForm')
subunit_fields['structure'] = flask_restplus.fields.String(required=False, title='Structure string', example='AAA')
subunit_fields['formula'] = flask_restplus.fields.String(required=False, title='Empirical formula', example='C5H10O')
subunit_fields['mol_wt'] = flask_restplus.fields.Float(required=False, title='Molecular weight', example=86.0)
subunit_fields['charge'] = flask_restplus.fields.Integer(required=False, title='Total charge', example=0)

bcform_fields = {}
bcform_fields['form'] = flask_restplus.fields.String(required=True, title='BcForm', description='input biocomplex form', example='2 * abc_a + 3 * abc_b')
bcform_fields['subunits'] = flask_restplus.fields.List(flask_restplus.fields.Nested(bcform_ns.model('Subunit',subunit_fields)), example=[
    {
      "name": "abc_a",
      "encoding": "bpforms.ProteinForm",
      "structure": "AAA"
    },
    {
      "name": "abc_b",
      "encoding": "bpforms.ProteinForm",
      "structure": "MM"
    }
  ])

bcforms_model = bcform_ns.model('BcForm', bcform_fields)


@bcform_ns.route("/")
class Bcform(flask_restplus.Resource):

    @bcform_ns.expect(bcforms_model, validate=True)
    def post(self):
        ret = {}
        warnings = []

        args = bcform_ns.payload

        # print(args)

        # get arguments
        form = args['form']
        arg_subunits = args.get('subunits', None)

        # validate form
        try:
            bc_form = bcforms.core.BcForm().from_str(form)
        except Exception as error:
            flask_restplus.abort(400, 'Form is invalid', errors={'form': str(error)})

        errors = bc_form.validate()
        if errors:
            flask_restplus.abort(400, 'Form is invalid', errors={'form': '. '.join(errors)})

        # validate input subunit properties
        sum_length = 0
        if arg_subunits is not None:
            for subunit in arg_subunits:

                # check if name is in the form
                subunit_id = subunit['name']
                if subunit_id in [subunit.id for subunit in bc_form.subunits]:

                    # check if encoding and structure are present at the same time
                    if ('encoding' in subunit) and ('structure' in subunit):
                        # if encoding and structure both present, check if encoding is known
                        encoding = subunit['encoding'].strip()
                        if encoding == 'bpforms.ProteinForm':
                            try:
                                subunit_structure = bpforms.ProteinForm().from_str(subunit['structure'])
                                sum_length += len(subunit_structure) * bc_form.get_subunit_attribute(subunit_id, 'stoichiometry')
                                bc_form.set_subunit_attribute(subunit_id, 'structure', subunit_structure)
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse bpforms.ProteinForm', errors={'structure': str(error)})
                        elif encoding == 'bpforms.DnaForm':
                            try:
                                subunit_structure = bpforms.DnaForm().from_str(subunit['structure'])
                                sum_length += len(subunit_structure) * bc_form.get_subunit_attribute(subunit_id, 'stoichiometry')
                                bc_form.set_subunit_attribute(subunit_id, 'structure', subunit_structure)
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse bpforms.DnaForm', errors={'structure': str(error)})
                        elif encoding == 'bpforms.RnaForm':
                            try:
                                subunit_structure = bpforms.RnaForm().from_str(subunit['structure'])
                                sum_length += len(subunit_structure) * bc_form.get_subunit_attribute(subunit_id, 'stoichiometry')
                                bc_form.set_subunit_attribute(subunit_id, 'structure', subunit_structure)
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse bpforms.RnaForm', errors={'structure': str(error)})
                        elif encoding == 'smiles' or encoding == 'SMILES' or encoding == 'smi' or encoding == 'SMI':
                            try:
                                bc_form.set_subunit_attribute(subunit_id, 'structure', subunit['structure'])
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse SMILES string', errors={'structure': str(error)})

                    # else if one is present but not the other, report error
                    elif ('encoding' in subunit) ^ ('structure' in subunit):
                        flask_restplus.abort(400, 'One of encoding and structure is present but not both')

                    # when neither encoding nor structure is present
                    else:
                        # check formula
                        if 'formula' in subunit:
                            try:
                                bc_form.set_subunit_attribute(subunit_id, 'formula', subunit['formula'])
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse formula', errors={'formula': str(error)})
                        elif 'mol_wt' in subunit:
                            try:
                                bc_form.set_subunit_attribute(subunit_id, 'mol_wt', subunit['mol_wt'])
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse mol_wt', errors={'mol_wt': str(error)})

                        # check charge
                        if 'charge' in subunit:
                            try:
                                bc_form.set_subunit_attribute(subunit_id, 'charge', subunit['charge'])
                            except Exception as error:
                                flask_restplus.abort(400, 'Unable to parse charge', errors={'charge': str(error)})

                else:
                    flask_restplus.abort(400, 'Subunit name not in BcForm', errors={'subunit':subunit_id})


        ret['form'] = str(bc_form)

        if sum_length <= max_len_get_structure:
            try:
                ret['structure'] = bc_form.export()
            except Exception:
                pass
        else:
            warnings.append('The sum of length of bpforms-encoded subunits is {}, which exceeds the max length limit {}.'.format(sum_length, max_len_get_structure))
            ret['structure'] = None

        try:
            ret['formula'] = str(bc_form.get_formula())
        except Exception:
            pass

        try:
            ret['mol_wt'] = bc_form.get_mol_wt()
        except Exception:
            pass

        try:
            ret['charge'] = bc_form.get_charge()
        except Exception:
            pass

        if len(warnings) > 0:
            ret['warnings'] = ' '.join(warnings)

        return ret

xlink_ns = flask_restplus.Namespace('crosslink', description='List crosslinks and get information about crosslinks')
api.add_namespace(xlink_ns)

@xlink_ns.route("/")
@xlink_ns.doc(params={})

class CrosslinkResource(flask_restplus.Resource):
    """ Get crosslinks """

    def get(self):
        """ Get crosslinks

        Returns:
            :obj:`dict`: dictionary representation of all crosslinks
        """
        return get_crosslinks()


@bcforms.core.cache.memoize(typed=False, expire=30 * 24 * 60 * 60)
def get_crosslinks():
    """ Get an alphabet

    Returns:
        :obj:`dict`: dictionary representation of crosslinks
    """

    crosslink_dict = dict(bcforms.core.parse_yaml(bcforms.core._xlink_filename))

    return crosslink_dict

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')