Source code for kripodb.webservice.server

# Copyright 2016 Netherlands eScience Center
#
# Licensed under the Apache License, Version 2.0 (the 'License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Kripo datafiles wrapped in a webservice"""
from __future__ import absolute_import

import logging

import connexion
import flask
from flask import current_app
from flask.json import JSONEncoder
from pkg_resources import resource_filename
from rdkit.Chem.AllChem import Mol
from rdkit.Chem.AllChem import MolToMolBlock
from rdkit.Chem.Draw import rdMolDraw2D
from six.moves.urllib_parse import urlparse

from kripodb.pharmacophores import as_phar, PharmacophoresDb
from ..db import FragmentsDb
from ..pairs import open_similarity_matrix
from ..version import __version__

LOGGER = logging.getLogger(__name__)


[docs]class KripodbJSONEncoder(JSONEncoder): """JSON encoder for KripoDB object types Copied from http://flask.pocoo.org/snippets/119/"""
[docs] def default(self, obj): try: if isinstance(obj, Mol): return MolToMolBlock(obj) iterable = iter(obj) except TypeError: pass else: return list(iterable) return JSONEncoder.default(self, obj)
[docs]def get_similar_fragments(fragment_id, cutoff, limit): """Find similar fragments to query. Args: fragment_id (str): Query fragment identifier cutoff (float): Cutoff, similarity scores below cutoff are discarded. limit (int): Maximum number of hits. Default is None for no limit. Returns: list[dict]: List of dict with query fragment identifier, hit fragment identifier and similarity score Raises: werkzeug.exceptions.NotFound: When the fragments_id could not be found """ similarity_matrix = current_app.config['similarities'] query_id = fragment_id hits = [] try: raw_hits = similarity_matrix.find(query_id, cutoff, limit) # add query column for hit_id, score in raw_hits: hits.append({'query_frag_id': query_id, 'hit_frag_id': hit_id, 'score': score}) except LookupError: return fragment_not_found(fragment_id) return hits
def fragment_not_found(fragment_id): title = 'Not Found' description = 'Fragment with identifier \'{0}\' not found'.format(fragment_id) ext = {'identifier': fragment_id} return connexion.problem(404, title, description, ext=ext)
[docs]def get_fragments(fragment_ids=None, pdb_codes=None): """Retrieve fragments based on their identifier or PDB code. Args: fragment_ids (List[str]): List of fragment identifiers pdb_codes (List[str]): List of PDB codes Returns: list[dict]: List of fragment information Raises: werkzeug.exceptions.NotFound: When one of the fragments_ids or pdb_code could not be found """ fragments_db_filename = current_app.config['fragments'] with FragmentsDb(fragments_db_filename) as fragmentsdb: fragments = [] missing_ids = [] if fragment_ids: for frag_id in fragment_ids: try: fragments.append(fragmentsdb[frag_id]) except LookupError: missing_ids.append(frag_id) if pdb_codes: for pdb_code in pdb_codes: try: for fragment in fragmentsdb.by_pdb_code(pdb_code.lower()): fragments.append(fragment) except LookupError: missing_ids.append(pdb_code) # TODO if fragment_ids and pdb_codes are both None then return paged list of all fragments if missing_ids: title = 'Not found' label = 'identifiers' if pdb_codes: label = 'PDB codes' description = 'Fragments with {1} \'{0}\' not found'.format(','.join(missing_ids), label) # connexion.problem is using json.dumps instead of flask custom json encoder, so performing convert myself # TODO remove mol2string conversion when https://github.com/zalando/connexion/issues/266 is fixed for fragment in fragments: if fragment['mol']: fragment['mol'] = MolToMolBlock(fragment['mol']) ext = {'absent_identifiers': missing_ids, 'fragments': fragments} return connexion.problem(404, title, description, ext=ext) return fragments
def mol2svg(mol, width, height): drawer = rdMolDraw2D.MolDraw2DSVG(width, height) drawer.DrawMolecule(mol) drawer.FinishDrawing() svg = drawer.GetDrawingText() return svg
[docs]def get_fragment_svg(fragment_id, width, height): """2D drawing of fragment in SVG format Args: fragment_id (str): Fragment identifier width (int): Width of SVG in pixels height (int): Height of SVG in pixels Returns: flask.Response|connexion.lifecycle.ConnexionResponse: SVG document|problem """ fragments_db_filename = current_app.config['fragments'] with FragmentsDb(fragments_db_filename) as fragmentsdb: try: fragment = fragmentsdb[fragment_id] if not fragment['mol']: title = 'Not Found' description = 'Fragment with identifier \'{0}\' has no molblock'.format(fragment_id) ext = {'identifier': fragment_id} return connexion.problem(404, title, description, ext=ext) mol = fragment['mol'] svg = mol2svg(mol, width, height) return flask.Response(svg, mimetype='image/svg+xml') except LookupError: return fragment_not_found(fragment_id)
[docs]def get_fragment_phar(fragment_id): """Pharmacophore in phar format of fragment Args: fragment_id (str): Fragment identifier Returns: flask.Response|connexion.lifecycle.ConnexionResponse: Pharmacophore|problem """ pharmacophores_db = current_app.config['pharmacophores'] try: points = pharmacophores_db[fragment_id] phar = as_phar(fragment_id, points) return flask.Response(phar, mimetype='text/plain') except LookupError: return fragment_not_found(fragment_id)
[docs]def get_version(): """ Returns: dict[version]: Version of web service """ # TODO check if matrix is usable return {'version': __version__}
[docs]def wsgi_app(similarities, fragments, pharmacophores, external_url='http://localhost:8084/kripo'): """Create wsgi app Args: similarities (SimilarityMatrix): Similarity matrix to use in webservice fragments (FragmentsDb): Fragment database filename pharmacophores: Filename of pharmacophores hdf5 file external_url (str): URL which should be used in Swagger spec Returns: connexion.App """ app = connexion.App(__name__) url = urlparse(external_url) swagger_file = resource_filename(__name__, 'swagger.yaml') app.app.json_encoder = KripodbJSONEncoder app.app.config['similarities'] = similarities app.app.config['fragments'] = fragments app.app.config['pharmacophores'] = pharmacophores arguments = {'basepath': url.path, 'version': __version__} # Keep validate_responses turned off, because of conflict with connexion.problem # see https://github.com/zalando/connexion/issues/266 app.add_api(swagger_file, base_path=url.path, arguments=arguments) return app
[docs]def serve_app(similarities, fragments, pharmacophores, internal_port=8084, external_url='http://localhost:8084/kripo'): """Serve webservice forever Args: similarities: Filename of similarity matrix hdf5 file fragments: Filename of fragments database file pharmacophores: Filename of pharmacophores hdf5 file internal_port: TCP port on which to listen external_url (str): URL which should be used in Swagger spec """ sim_matrix = open_similarity_matrix(similarities) pharmacophores_db = PharmacophoresDb(pharmacophores) app = wsgi_app(sim_matrix, fragments, pharmacophores_db, external_url) LOGGER.setLevel(logging.INFO) LOGGER.addHandler(logging.StreamHandler()) LOGGER.info(' * Swagger spec at {}/swagger.json'.format(external_url)) LOGGER.info(' * Swagger ui at {}/ui'.format(external_url)) try: app.run(port=internal_port) finally: sim_matrix.close()