utils/indigo-service/backend/service/v2/indigo_api.py (1,069 lines of code) (raw):

#!/usr/bin/env python # -*- coding: utf-8 -* import base64 import collections import json import logging import traceback from functools import wraps from threading import local from flask import Blueprint, jsonify, request # type: ignore from indigo import Indigo, IndigoException # type: ignore from indigo.inchi import IndigoInchi # type: ignore from indigo.renderer import IndigoRenderer # type: ignore from marshmallow.exceptions import ValidationError # type: ignore from .common import config from .common.util import highlight from .validation import ( IndigoAutomapSchema, IndigoCalculateSchema, IndigoCheckSchema, IndigoConvertExplicitHydrogensSchema, IndigoRendererSchema, IndigoRequestSchema, ) tls = local() indigo_api = Blueprint("indigo_api", __name__) indigo_api.indigo_defaults = { # type: ignore "ignore-stereochemistry-errors": "true", "smart-layout": "true", "gross-formula-add-rsites": "true", "mass-skip-error-on-pseudoatoms": "false", } indigo_api_logger = logging.getLogger("indigo") LOG_MAX_SYMBOLS = 50 def get_shorten_text(intext): if len(intext) < LOG_MAX_SYMBOLS: return intext return "{} ...".format( intext.strip().ljust(LOG_MAX_SYMBOLS)[:LOG_MAX_SYMBOLS] ) def LOG_DATA(*args): format_shorten = [] format_str = [] for a in args: if config.__dict__["LOG_LEVEL"] == "DEBUG": format_str.append(str(a)) else: format_shorten.append(get_shorten_text(str(a))) if config.__dict__["LOG_LEVEL"] == "DEBUG": indigo_api_logger.debug('"{}"'.format('" "'.join(format_str))) else: indigo_api_logger.info('"{}"'.format('" "'.join(format_shorten))) def indigo_init(options={}): try: tls.indigo = Indigo() tls.indigo.inchi = IndigoInchi(tls.indigo) tls.indigo.renderer = IndigoRenderer(tls.indigo) for option, value in indigo_api.indigo_defaults.items(): tls.indigo.setOption(option, value) for option, value in options.items(): # TODO: Remove this when Indigo API supports smiles type option if option in ( "smiles", "smarts", "input-format", "output-content-type", "monomerLibrary", ): continue tls.indigo.setOption(option, value) return tls.indigo except Exception as e: indigo_api_logger.error("indigo-init: {0}".format(e)) return None class MolData: is_query = False is_rxn = False struct = None substruct = None def is_rxn(molstr): return ( ">>" in molstr or molstr.startswith("$RXN") or "<reactantList>" in molstr ) def qmol_to_mol(m, selected, indigo): for atom in m.iterateAtoms(): if not atom.index() in selected: atom.resetAtom("C") for bond in m.iterateBonds(): if not ( bond.source().index() in selected or bond.destination().index() in selected ): m.removeBonds( [ bond.index(), ] ) return indigo.loadMolecule(m.clone().molfile()) class ImplicitHCalcExpection(IndigoException): pass def remove_implicit_h_in_selected_components(m, selected): if m.countRSites(): for index in selected: if m.getAtom(index).isRSite(): raise ImplicitHCalcExpection( b"Cannot calculate properties for RGroups" ) if m.countAttachmentPoints(): count = m.countAttachmentPoints() for order in range(1, count + 1): for ap in m.iterateAttachmentPoints(order): if ap.index() in selected: raise ImplicitHCalcExpection( b"Cannot calculate properties for RGroups" ) removed_atoms = set() implicit_h_decrease = collections.defaultdict(int) for c in m.iterateComponents(): for a in c.iterateAtoms(): a_index = a.index() if a_index not in selected: continue for n in a.iterateNeighbors(): n_index = n.index() if n_index not in selected: bond_order = None for bond in m.iterateBonds(): if all( i in [a_index, n_index] for i in ( bond.source().index(), bond.destination().index(), ) ): bond_order = bond.bondOrder() break if bond_order not in (1, 2, 3): raise ImplicitHCalcExpection( b"Cannot calculate implicit hydrogens on single atom with query or aromatic bonds" ) implicit_h_decrease[a_index] += bond_order removed_atoms.add(n_index) m.removeAtoms(list(removed_atoms)) for index, value in implicit_h_decrease.items(): a = m.getAtom(index) implicit_h_count = a.countImplicitHydrogens() - value if implicit_h_count < 0: raise ImplicitHCalcExpection( b"Cannot calculate implicit hydrogenes on atom with bad valence" ) a.setImplicitHCount(implicit_h_count) return m def iterate_selected_submolecules(r, selected, indigo): atomCounter = 0 for m in r.iterateMolecules(): moleculeAtoms = [] for atom in selected: if atomCounter <= atom < atomCounter + m.countAtoms(): moleculeAtoms.append(atom - atomCounter) atomCounter += m.countAtoms() if moleculeAtoms: if r.dbgInternalType() == "#05: <query reaction>": m = qmol_to_mol(m, moleculeAtoms, indigo) m = remove_implicit_h_in_selected_components(m, moleculeAtoms) yield m.getSubmolecule(moleculeAtoms).clone() def iterate_selected_components(m, selected): for c in m.iterateComponents(): for atom in c.iterateAtoms(): if atom.index() in selected: yield c break def do_calc(m, func_name, precision): try: value = getattr(m, func_name)() except IndigoException as e: value = "calculation error: {0}".format(e.value.split(": ")[-1]) if isinstance(value, float): value = round(value, precision) return str(value) def molecule_calc(m, func_name, precision=None): if m.dbgInternalType() == "#03: <query molecule>": return "Cannot calculate properties for structures with query features" if m.countRGroups() or m.countAttachmentPoints(): return "Cannot calculate properties for RGroups" results = [] for c in m.iterateComponents(): results.append(do_calc(c.clone(), func_name, precision)) return "; ".join(results) def reaction_calc(rxn, func_name, precision=None): if rxn.dbgInternalType() == "#05: <query reaction>": return "Cannot calculate properties for structures with query features" for m in rxn.iterateMolecules(): if m.countRGroups() or m.countAttachmentPoints(): return "Cannot calculate properties for RGroups" reactants_results = [] for r in rxn.iterateReactants(): reactants_results.append( "[{0}]".format(molecule_calc(r, func_name, precision)) ) product_results = [] for p in rxn.iterateProducts(): product_results.append( "[{0}]".format(molecule_calc(p, func_name, precision)) ) return "{0} > {1}".format( " + ".join(reactants_results), " + ".join(product_results) ) def selected_molecule_calc( m, selected, func_name, precision=None, indigo=None ): if m.dbgInternalType() == "#03: <query molecule>": try: m = qmol_to_mol(m, selected, indigo) except IndigoException: return "Cannot calculate properties for structures with query features" if m.countRGroups() and max(selected) >= m.countAtoms(): return "Cannot calculate properties for RGroups" try: m = remove_implicit_h_in_selected_components(m, selected) except ImplicitHCalcExpection as e: return str(e.value) results = [] for c in iterate_selected_components(m, selected): cc = c.clone() if cc.countRSites() or cc.countAttachmentPoints(): return "Cannot calculate properties for RGroups" results.append(do_calc(cc, func_name, precision)) return "; ".join(results) def selected_reaction_calc( r, selected, func_name, precision=None, indigo=None ): results = [] total_atoms_count = sum([m.countAtoms() for m in r.iterateMolecules()]) total_rgroups_count = sum([m.countRGroups() for m in r.iterateMolecules()]) if total_rgroups_count and max(selected) >= total_atoms_count: return "Cannot calculate properties for RGroups" try: for csm in iterate_selected_submolecules(r, selected, indigo): if csm.countRSites() or csm.countAttachmentPoints(): return "Cannot calculate properties for RGroups" results.append(do_calc(csm, func_name, precision)) except ImplicitHCalcExpection as e: return str(e.value) except IndigoException: return "Cannot calculate properties for structures with query features" return "; ".join(results) def remove_unselected_repeating_units_m(m, selected): for ru in m.iterateRepeatingUnits(): for atom in ru.iterateAtoms(): if atom.index() not in selected: ru.remove() break def remove_unselected_repeating_units_r(r, selected): atomCounter = 0 for m in r.iterateMolecules(): moleculeAtoms = [] for atom in selected: if atomCounter <= atom < atomCounter + m.countAtoms(): moleculeAtoms.append(atom - atomCounter) atomCounter += m.countAtoms() if moleculeAtoms: remove_unselected_repeating_units_m(m, moleculeAtoms) def load_moldata( molstr, indigo=None, options={}, query=False, mime_type=None, selected=None, library=None, try_document=False, ): if not indigo: try: indigo = indigo_init(options) except Exception as e: raise HttpException( "Problem with Indigo initialization: {0}".format(e), 501 ) md = MolData() input_format = mime_type if "input-format" in options: input_format = options["input-format"] if input_format in ("smarts", "chemical/x-daylight-smarts"): md.struct = indigo.loadSmarts(molstr) md.is_query = True elif input_format == "chemical/x-peptide-sequence": md.struct = indigo.loadSequence(molstr, "PEPTIDE", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-peptide-sequence-3-letter": md.struct = indigo.loadSequence(molstr, "PEPTIDE-3-LETTER", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-rna-sequence": md.struct = indigo.loadSequence(molstr, "RNA", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-dna-sequence": md.struct = indigo.loadSequence(molstr, "DNA", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-peptide-fasta": md.struct = indigo.loadFasta(molstr, "PEPTIDE", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-rna-fasta": md.struct = indigo.loadFasta(molstr, "RNA", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-dna-fasta": md.struct = indigo.loadFasta(molstr, "DNA", library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-idt": md.struct = indigo.loadIdt(molstr, library) md.is_rxn = False md.is_query = False elif input_format == "chemical/x-helm": md.struct = indigo.loadHelm(molstr, library) md.is_rxn = False md.is_query = False elif molstr.startswith("InChI"): md.struct = indigo.inchi.loadMolecule(molstr) md.is_rxn = False md.is_query = False else: if try_document: try: md.struct = indigo.loadKetDocument(molstr) return md except IndigoException: pass try: if not query: md.struct = indigo.loadMolecule(molstr) md.is_query = False else: md.struct = indigo.loadQueryMolecule(molstr) md.is_query = True except IndigoException: try: md.struct = indigo.loadQueryMolecule(molstr) md.is_query = True except IndigoException: md.is_rxn = True try: if not query: md.struct = indigo.loadReaction(molstr) md.is_query = False else: md.struct = indigo.loadQueryReaction(molstr) md.is_query = True except IndigoException: try: md.struct = indigo.loadQueryReaction(molstr) md.is_query = True except IndigoException: if library is None: raise HttpException( "struct data not recognized as molecule, query, reaction or reaction query", 400, ) else: # has library try to load IDT and HELM try: md.struct = indigo.loadIdt(molstr, library) md.is_rxn = False except IndigoException: try: md.struct = indigo.loadHelm( molstr, library ) except IndigoException: raise HttpException( "struct data not recognized as molecule, query, reaction or reaction query", 400, ) return md def save_moldata( md, output_format=None, options={}, indigo=None, library=None ): if output_format in ("chemical/x-mdl-molfile", "chemical/x-mdl-rxnfile"): return md.struct.rxnfile() if md.is_rxn else md.struct.molfile() elif output_format == "chemical/x-indigo-ket": return md.struct.json() elif output_format == "chemical/x-sequence": return md.struct.sequence(library) elif output_format == "chemical/x-peptide-sequence-3-letter": return md.struct.sequence3Letter(library) elif output_format == "chemical/x-fasta": return md.struct.fasta(library) elif output_format == "chemical/x-idt": return md.struct.idt(library) elif output_format == "chemical/x-helm": return md.struct.helm(library) elif output_format == "chemical/x-daylight-smiles": if options.get("smiles") == "canonical": return md.struct.canonicalSmiles() else: indigo.setOption("smiles-saving-format", "daylight") return md.struct.smiles() elif output_format == "chemical/x-chemaxon-cxsmiles": if options.get("smiles") == "canonical": return md.struct.canonicalSmiles() else: indigo.setOption("smiles-saving-format", "chemaxon") return md.struct.smiles() elif output_format == "chemical/x-daylight-smarts": return md.struct.smarts() elif output_format == "chemical/x-cml": return md.struct.cml() elif output_format == "chemical/x-cdxml": return md.struct.cdxml() elif output_format == "chemical/x-cdx": return md.struct.b64cdx() elif output_format == "chemical/x-inchi": return indigo.inchi.getInchi(md.struct) elif output_format == "chemical/x-inchi-key": return indigo.inchi.getInchiKey(indigo.inchi.getInchi(md.struct)) elif output_format == "chemical/x-inchi-aux": res = indigo.inchi.getInchi(md.struct) aux = indigo.inchi.getAuxInfo() return "{}\n{}".format(res, aux) elif output_format == "chemical/x-sdf": buffer = indigo.writeBuffer() sdfSaver = indigo.createSaver(buffer, "sdf") for frag in md.struct.iterateComponents(): sdfSaver.append(frag.clone()) sdfSaver.close() return buffer.toString() elif output_format == "chemical/x-rdf": buffer = indigo.writeBuffer() rdfSaver = indigo.createSaver(buffer, "rdf") for reac in md.struct.iterateReactions(): rdfSaver.append(reac.clone()) rdfSaver.close() return buffer.toString() raise HttpException("Format %s is not supported" % output_format, 400) class HttpException(Exception): def __init__(self, value, code, headers={"Content-Type": "text/plain"}): self.value = value self.code = code self.headers = headers def get_request_data(request): request_data = {} if request.content_type == "application/json": request_data = json.loads(request.data.decode("utf-8")) request_data["json_output"] = True else: request_data["struct"] = request.data.decode("utf-8") request_data["input_format"] = ( request.headers["Content-Type"] if "Content-Type" in request.headers else None ) request_data["output_format"] = "chemical/x-mdl-molfile" if "Accept" in request.headers and request.headers["Accept"] != "*/*": request_data["output_format"] = request.headers["Accept"] request_data["json_output"] = False return request_data def get_response( md, output_struct_format, json_output, options, indigo, library=None ): output_mol = save_moldata( md, output_struct_format, options, indigo, library ) LOG_DATA( "[RESPONSE]", output_struct_format, options, output_mol.encode("utf-8") ) if json_output or options.get("output-content-type") == "application/json": return ( jsonify( { "struct": output_mol, "format": output_struct_format, "original_format": md.struct.getOriginalFormat(), } ), 200, {"Content-Type": "application/json"}, ) else: return output_mol, 200, {"Content-Type": output_struct_format} def get_error_response(value, error_code, json_output=False): if json_output: return ( jsonify({"error": value}), error_code, {"Content-Type": "application/json"}, ) else: return value, error_code, {"Content-Type": "text/plain"} def check_exceptions(f): @wraps(f) def wrapper(*args, **kwargs): json_output = ( "Accept" in request.headers and request.headers["Accept"] == "application/json" ) or ( "Content-Type" in request.headers and request.headers["Content-Type"] == "application/json" ) try: return f(*args, **kwargs) except ValidationError as e: indigo_api_logger.error( "[RESPONSE-400] validation error: {0}".format(e.messages) ) indigo_api_logger.debug(traceback.format_exc()) if json_output: return ( jsonify({"error": e.messages}), 400, {"Content-Type": "application/json; charset=utf-8"}, ) else: return ( "ValidationError: {0}".format(str(e.messages)), 400, {"Content-Type": "text/plain"}, ) except HttpException as e: indigo_api_logger.error("HttpException: {0}".format(e.value)) indigo_api_logger.debug(traceback.format_exc()) if json_output: return ( jsonify({"error": e.value}), e.code, e.headers.update({"Content-Type": "application/json"}), ) else: return ( e.value, e.code, e.headers.update({"Content-Type": "text/plain"}), ) except IndigoException as e: indigo_api_logger.error("IndigoException: {0}".format(e.value)) indigo_api_logger.error(traceback.format_exc()) if json_output: return ( jsonify({"error": "IndigoException: {0}".format(e.value)}), 400, {"Content-Type": "application/json"}, ) else: return ( "IndigoException: {0}".format(e.value), 400, {"Content-Type": "text/plain"}, ) except Exception as e: indigo_api_logger.error("Exception: {0}".format(e), exc_info=e) indigo_api_logger.debug(traceback.format_exc()) if json_output: return ( jsonify({"error": "{0}".format(e)}), 500, {"Content-Type": "application/json"}, ) else: return ( "Exception: {0}".format(e), 500, {"Content-Type": "text/plain"}, ) return wrapper @indigo_api.route("/info") @check_exceptions def info(): """ Get information about Indigo version --- tags: - indigo responses: 200: description: JSON with Indigo version """ indigo_api_logger.info("[REQUEST] /info") indigo = indigo_init() return ( jsonify({"Indigo": {"version": indigo.version()}}), 200, {"Content-Type": "application/json"}, ) def versionInfo(): """ Get information about Indigo version info --- tags: - indigo responses: 200: description: JSON with Indigo version """ indigo_api_logger.info("[REQUEST] /info") indigo = indigo_init() return ( jsonify({"Indigo": {"version_info": indigo.versionInfo()}}), 200, {"Content-Type": "application/json"}, ) @indigo_api.route("/aromatize", methods=["POST"]) @check_exceptions def aromatize(): """ Aromatize structure --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoRequest required: - struct properties: struct: type: string required: true examples: C1=CC=CC=C1 output_format: type: string default: chemical/x-mdl-molfile examples: chemical/x-daylight-smiles enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-daylight-smiles responses: 200: description: Aromatized chemical structure schema: id: IndigoResponse required: - struct - format properties: struct: type: string format: type: string default: chemical/x-mdl-molfile 400: description: 'A problem with supplied client data' schema: id: ClientError required: - error properties: error: type: string 500: description: 'A problem on server side' schema: id: ServerError required: - error properties: error: type: string """ request_data = get_request_data(request) indigo_api_logger.info("[RAW REQUEST] {}".format(request_data)) data = IndigoRequestSchema().load(request_data) LOG_DATA( "[REQUEST] /aromatize", data["input_format"], data["output_format"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) md.struct.aromatize() return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/dearomatize", methods=["POST"]) @check_exceptions def dearomatize(): """ Dearomatize structure --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoDearomatizeRequest properties: struct: type: string required: true examples: c1ccccc1 output_format: type: string default: chemical/x-mdl-molfile examples: chemical/x-daylight-smiles enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: c1ccccc1 output_format: chemical/x-daylight-smiles responses: 200: description: Dearomatized chemical structure schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoRequestSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /dearomatize", data["input_format"], data["output_format"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) md.struct.dearomatize() return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/convert", methods=["POST"]) @check_exceptions def convert(): """ Convert structure to Molfile/Rxnfile, SMILES, CML or InChI --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoConvertRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 output_format: type: string default: chemical/x-mdl-molfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-inchi-key - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-mdl-molfile responses: 200: description: Chemical structure in requested format schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ if request.method == "POST": data = IndigoRequestSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /convert", data["input_format"], data["output_format"], data["struct"].encode("utf-8"), data["options"], ) indigo = indigo_init(data["options"]) monomer_library = data["options"].get("monomerLibrary") library = None if monomer_library is not None: library = indigo.loadMonomerLibrary(monomer_library) else: library = indigo.loadMonomerLibrary('{"root":{}}') query = False if "smarts" in data["output_format"]: query = True try_document = False if data["output_format"] in ( "chemical/x-sequence", "chemical/x-fasta", "chemical/x-idt", "chemical/x-helm", "chemical/x-peptide-sequence-3-letter", ): try_document = True md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, query=query, library=library, try_document=try_document, ) return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, library=library, ) elif request.method == "GET": input_dict = { "struct": request.args["struct"], "output_format": ( request.args["output_format"] if "output_format" in request.args else "chemical/x-mdl-molfile" ), } data = IndigoRequestSchema().load(input_dict) LOG_DATA( "[REQUEST] /convert", data["input_format"], data["output_format"], data["struct"].encode("utf-8"), data["options"], ) indigo = indigo_init(data["options"]) monomer_library = data["options"].get("monomerLibrary") library = None if monomer_library is not None: library = indigo.loadMonomerLibrary(monomer_library) else: library = indigo.loadMonomerLibrary('{"root":{}}') md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, library=library, try_document=True, ) if "json_output" in request.args: data["json_output"] = True else: data["json_output"] = False return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, library=library, ) @indigo_api.route("/convert_explicit_hydrogens", methods=["POST"]) @check_exceptions def convert_explicit_hydrogens(): """ Convert hydrogens from implicit to explicit and vice versa --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoConvertExplicitHydrogensRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 mode: type: string default: auto enum: auto fold unfold output_format: type: string default: chemical/x-mdl-molfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-mdl-molfile responses: 200: description: Chemical structure with converted explicit hydrogens schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoConvertExplicitHydrogensSchema().load( get_request_data(request) ) LOG_DATA( "[REQUEST] /convert_explicit_hydrogens", data["input_format"], data["output_format"], data["struct"].encode("utf-8"), data.get("mode", "mode undefined"), ) indigo = indigo_init(data["options"]) query = False if "smarts" in data["output_format"]: query = True md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, query=query, ) mode = data.get("mode", "auto") if mode == "fold": md.struct.foldHydrogens() elif mode == "unfold": md.struct.unfoldHydrogens() else: md.struct.foldUnfoldHydrogens() return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/layout", methods=["POST"]) @check_exceptions def layout(): """ Layout structure --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoLayoutRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 output_format: type: string default: chemical/x-mdl-molfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-mdl-molfile responses: 200: description: Chemical structure with calculated correct coordinates schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoRequestSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /layout", data["input_format"], data["output_format"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) query = False if "smarts" in data["output_format"]: query = True md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, query=query, ) md.struct.layout() return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/clean", methods=["POST"]) @check_exceptions def clean(): """ Clean up structure or selected substructure coordinates --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoClean2dRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 output_format: type: string default: chemical/x-mdl-molfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-mdl-molfile responses: 200: description: Chemical structure with approximately corrected coordinates schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoRequestSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /clean", data["input_format"], data["output_format"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], selected=data["selected"], indigo=indigo, ) if md.is_rxn and data["selected"]: for sm in iterate_selected_submolecules(md.struct, data["selected"]): sm.clean2d() else: md.substruct = ( md.struct.getSubmolecule(data["selected"]) if data["selected"] else md.struct ) md.substruct.clean2d() return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/automap", methods=["POST"]) @check_exceptions def automap(): """ Automatically calculate reaction atoms mapping --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoAutomapRequest properties: struct: type: string required: true examples: C1=CC=CC=C1.N>>C1=CC=CC=N1.C output_format: type: string default: chemical/x-mdl-rxnfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1.N>>C1=CC=CC=N1.C output_format: chemical/x-mdl-rxnfile responses: 200: description: Reaction with calculated atom-to-atom mappings schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoAutomapSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /automap", data["input_format"], data["output_format"], data["mode"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) md.struct.automap(data["mode"]) return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route("/calculate_cip", methods=["POST"]) @check_exceptions def calculate_cip(): """ Calculate CIP --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoCalculateCipRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 output_format: type: string default: chemical/x-mdl-molfile enum: - chemical/x-mdl-rxnfile - chemical/x-mdl-molfile - chemical/x-indigo-ket - chemical/x-daylight-smiles - chemical/x-chemaxon-cxsmiles - chemical/x-cml - chemical/x-inchi - chemical/x-iupac - chemical/x-daylight-smarts - chemical/x-inchi-aux example: struct: C1=CC=CC=C1 output_format: chemical/x-mdl-molfile responses: 200: description: Chemical structure with calculaated CIP schema: $ref: "#/definitions/IndigoResponse" 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoRequestSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /calculate_cip", data["input_format"], data["output_format"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) indigo.setOption("json-saving-add-stereo-desc", True) indigo.setOption("molfile-saving-add-stereo-desc", True) return get_response( md, data["output_format"], data["json_output"], data["options"], indigo=indigo, ) @indigo_api.route( "/check", methods=[ "POST", ], ) @check_exceptions def check(): """ Check chemical structure --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoCheckRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 types: type: array default: ["valence", "ambiguous_h", "query", "pseudoatoms", "radicals", "stereo", "overlapping_atoms", "overlapping_bonds", "3d", "sgroups", "v3000", "rgroups"] enum: - valence - ambiguous_h - query - pseudoatoms - radicals - stereo - overlapping_atoms - overlapping_bonds - 3d - sgroups - v3000 - rgroups example: struct: "[C+5]" types: ["valence", "ambiguous_h"] responses: 200: description: JSON with errors for given types if errors present schema: id: IndigoCheckResponse 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoCheckSchema().load(get_request_data(request)) try: indigo = indigo_init(data["options"]) except Exception as e: raise HttpException( "Problem with Indigo initialization: {0}".format(e), 501 ) LOG_DATA( "[REQUEST] /check", data["types"], data["struct"], data["options"] ) result = indigo.check(data["struct"], json.dumps(data["types"])) return result, 200, {"Content-Type": "application/json"} @indigo_api.route( "/calculate", methods=[ "POST", ], ) @check_exceptions def calculate(): """ Calculate properites for input structure --- tags: - indigo parameters: - name: json_request in: body required: true schema: id: IndigoCalculateRequest properties: struct: type: string required: true examples: C1=CC=CC=C1 properties: type: array default: ["molecular-weight"] examples: ["molecular-weight"] enum: - molecular-weight - most-abundant-mass - monoisotopic-mass - gross - mass-composition example: struct: C1=CC=CC=C1 properties: ["molecular-weight"] responses: 200: description: Calculated properties 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ data = IndigoCalculateSchema().load(get_request_data(request)) LOG_DATA( "[REQUEST] /calculate", data["properties"], data["selected"], data["struct"], data["options"], ) indigo = indigo_init(data["options"]) md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], selected=data["selected"], indigo=indigo, ) if data["selected"]: if md.is_rxn: remove_unselected_repeating_units_r(md.struct, data["selected"]) else: remove_unselected_repeating_units_m(md.struct, data["selected"]) calculate_properties = data["properties"] result = {} precision = data["precision"] func_name_dict = { "molecular-weight": "molecularWeight", "most-abundant-mass": "mostAbundantMass", "monoisotopic-mass": "monoisotopicMass", "mass-composition": "massComposition", "gross": "grossFormula", } for p in calculate_properties: if md.is_rxn: if data["selected"]: result[p] = selected_reaction_calc( md.struct, data["selected"], func_name_dict[p], precision=precision, indigo=indigo, ) else: result[p] = reaction_calc( md.struct, func_name_dict[p], precision=precision ) else: if data["selected"]: result[p] = selected_molecule_calc( md.struct, data["selected"], func_name_dict[p], precision=precision, indigo=indigo, ) else: result[p] = molecule_calc( md.struct, func_name_dict[p], precision=precision ) if data["json_output"]: return jsonify(result), 200, {"Content-Type": "application/json"} else: return ( "\n".join( [ "{0}: {1}".format(key, value) for key, value in { k: v for k, v in result.items() if v }.items() ] ), 200, {"Content-Type": "text/plain"}, ) @indigo_api.route("/render", methods=["POST"]) @check_exceptions def render(): """ Render molecule --- tags: - indigo description: 'Returns a molecule image' parameters: - name: json_request in: body required: true schema: id: IndigoRenderRequest properties: struct: type: string examples: C1=CC=CC=C1 query: type: string examples: C output_format: type: string default: image/svg+xml options: type: array example: struct: C1=CC=CC=C1 query: C output_format: image/svg+xml responses: 200: description: 'A rendered molecule image' schema: type: file 400: description: 'A problem with supplied client data' schema: $ref: "#/definitions/ClientError" 500: description: 'A problem on server side' schema: $ref: "#/definitions/ServerError" """ render_format_dict = { "image/svg+xml": "svg", "image/png": "png", "application/pdf": "pdf", "image/png;base64": "png", "image/svg;base64": "svg", } render_format_dict_r = { "png": "image/png;base64", "svg": "image/svg;base64", "pdf": "application/pdf;base64", } # if request.method == "POST": LOG_DATA( "[REQUEST] /render", request.headers["Content-Type"], request.headers["Accept"], request.data, ) try: if "application/json" in request.headers["Content-Type"]: input_dict = json.loads(request.data.decode()) else: input_dict = { "struct": request.data, "output_format": request.headers["Accept"], } except ValueError: return get_error_response( "Invalid input JSON: {0}".format(request.data), 400 ) data = IndigoRendererSchema().load(input_dict) indigo = indigo_init(data["options"]) if data["struct"] and not data["query"]: md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) elif data["query"] and not data["struct"]: md = load_moldata( data["query"], options=data["options"], indigo=indigo ) else: md = load_moldata( data["struct"], mime_type=data["input_format"], options=data["options"], indigo=indigo, ) mdq = load_moldata( data["query"], mime_type=data["input_format"], query=True, indigo=indigo, ) try: md.struct = highlight(indigo, md.struct, mdq.struct) except RuntimeError: pass # elif request.method == "GET": # LOG_DATA("[REQUEST] /render GET", request.args) # try: # input_dict = { # "struct": request.args["struct"] # if "struct" in request.args # else None, # "output_format": request.args["output_format"] # if "output_format" in request.args # else "image/svg+xml", # "query": request.args["query"] # if "query" in request.args # else None, # } # if input_dict["struct"] and not input_dict["query"]: # md = load_moldata(input_dict["struct"]) # elif input_dict["query"] and not input_dict["struct"]: # mdq = load_moldata(input_dict["query"]) # else: # md = load_moldata(input_dict["struct"]) # mdq = load_moldata( # input_dict["query"], # indigo=md.struct._session, # query=True, # ) # md.struct = highlight( # md.struct._session, md.struct, mdq.struct # ) # data = IndigoRendererSchema().load(input_dict) # except Exception as e: # return get_error_response( # "Invalid GET query {}".format(str(e)), 400 # ) # indigo = md.struct._session # indigo = indigo_init(data["options"]) content_type = data["output_format"] if "render-output-format" in data["options"]: rof = data["options"]["render-output-format"] content_type = render_format_dict_r[rof] else: indigo.setOption( "render-output-format", render_format_dict[content_type] ) result = indigo.renderer.renderToBuffer(md.struct) if "base64" in content_type: result = base64.b64encode(result) indigo_api_logger.info( "[RESPONSE] Content-Type: {0}, Content-Size: {1}".format( content_type, len(result) ) ) return result, 200, {"Content-Type": content_type}