From 29722ca2d2675181804816bdc0e3be7d18bfa7c4 Mon Sep 17 00:00:00 2001 From: Alina Tikhova Date: Fri, 31 Mar 2023 01:11:24 +0300 Subject: [PATCH 1/3] #1083: indigo-service: added indigo options support --- api/http/indigo_service/indigo_http.py | 31 ++-- api/http/indigo_service/indigo_tools.py | 9 +- api/http/indigo_service/jsonapi.py | 11 +- api/http/indigo_service/service.py | 11 +- api/http/tests/test_indigo_http.py | 191 +++++++++++++++++++++++- 5 files changed, 238 insertions(+), 15 deletions(-) diff --git a/api/http/indigo_service/indigo_http.py b/api/http/indigo_service/indigo_http.py index a45b533472..b21c5e1ce2 100644 --- a/api/http/indigo_service/indigo_http.py +++ b/api/http/indigo_service/indigo_http.py @@ -99,6 +99,8 @@ def similarities( beta = request.data.attributes.beta metric = request.data.attributes.metric + service.set_indigo_options(request.data.attributes.options) + compound, *_ = service.extract_compounds(source(request)) target_pairs = targets(request) target_compounds = service.extract_compounds(target_pairs) @@ -123,6 +125,8 @@ def similarities( response_model_exclude_unset=True, ) def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse: + service.set_indigo_options(request.data.attributes.options) + compound, *_ = service.extract_compounds(source(request)) target_pairs = targets(request) target_compounds = service.extract_compounds(target_pairs) @@ -152,6 +156,7 @@ def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse: def convert( request: jsonapi.CompoundConvertRequest, ) -> jsonapi.CompoundResponse: + service.set_indigo_options(request.data.attributes.options) compound, *_ = service.extract_compounds( compounds(request), request.data.attributes.compound.modifiers ) @@ -166,6 +171,7 @@ def convert( response_model_exclude_unset=True, ) def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse: + service.set_indigo_options(request.data.attributes.options) compound, *_ = service.extract_compounds(compounds(request)) validations = request.data.attributes.validations results = {} @@ -182,6 +188,7 @@ def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse: def descriptors( request: jsonapi.DescriptorRequest, ) -> jsonapi.DescriptorResponse: + service.set_indigo_options(request.data.attributes.options) compound, *_ = service.extract_compounds(compounds(request)) properties = request.data.attributes.descriptors results = {} @@ -198,6 +205,7 @@ def descriptors( def common_bits( request: jsonapi.CommonBitsRequest, ) -> jsonapi.CommonBitsResponse: + service.set_indigo_options(request.data.attributes.options) compound, *_ = service.extract_compounds(source(request)) target_compounds = service.extract_compounds(targets(request)) source_fp = compound.fingerprint("sim") @@ -212,20 +220,23 @@ def common_bits( def render( request: jsonapi.RenderRequest, ) -> jsonapi.RenderResponse: - compound, *_ = service.extract_compounds(compounds(request)) - output_format = request.data.attributes.outputFormat indigo_renderer = IndigoRenderer(indigo()) + + output_format = request.data.attributes.outputFormat + options = request.data.attributes.options + if options: + output_format_option = options.get("render-output-format") + if output_format_option and output_format_option not in output_format: + raise HTTPException( + status_code=400, detail="Choose only one output format" + ) + indigo().setOption( "render-output-format", jsonapi.rendering_formats.get(output_format) ) - options = request.data.attributes.options - if options: - for option, value in options.items(): - if option == "render-output-format": - raise HTTPException( - status_code=400, detail="Choose only one output format" - ) - indigo().setOption(option, value) + service.set_indigo_options(options) + + compound, *_ = service.extract_compounds(compounds(request)) raw_image = indigo_renderer.renderToBuffer(compound) return jsonapi.make_render_response(raw_image, output_format) diff --git a/api/http/indigo_service/indigo_tools.py b/api/http/indigo_service/indigo_tools.py index 0231ba4ddb..de911ccf53 100644 --- a/api/http/indigo_service/indigo_tools.py +++ b/api/http/indigo_service/indigo_tools.py @@ -26,15 +26,22 @@ __cur_thread: ContextVar[int] = ContextVar( "thread_id", default=threading.get_ident() ) +print("BEGINNING", threading.get_ident(), __cur_thread.get()) def indigo(thread_local: bool = False) -> Indigo: + # print("__cur_thread", __cur_thread.get()) + # print("__indigo_1", __indigo.get().getSessionId()) + # print("thread_local ----> ", thread_local) if thread_local: + # print("THREAD LOCAL !!!") ident = threading.get_ident() + # print("def indigo ----> ", ident) if __cur_thread.get() != ident: __indigo.set(Indigo()) __cur_thread.set(ident) - + # print("def indigo ----> ", threading.get_ident()) + # print("__indigo_2", __indigo.get().getSessionId()) return __indigo.get() diff --git a/api/http/indigo_service/jsonapi.py b/api/http/indigo_service/jsonapi.py index 2a78df4f56..8c9069fa52 100644 --- a/api/http/indigo_service/jsonapi.py +++ b/api/http/indigo_service/jsonapi.py @@ -19,7 +19,7 @@ import base64 import functools from enum import Enum -from typing import Dict, Generic, List, Optional, TypeVar, Union +from typing import Any, Dict, Generic, List, Optional, TypeVar, Union from fastapi import HTTPException from pydantic import BaseModel, conlist, validator @@ -147,6 +147,7 @@ class CompoundObjectWithModifiers(CompoundObject): class CompoundModel(GenericModel, Generic[OutputFormatT]): compound: CompoundObjectWithModifiers outputFormat: OutputFormatT + options: Optional[Dict[str, Any]] = None class CompoundArrayModel(GenericModel, Generic[OutputFormatT]): @@ -164,6 +165,7 @@ class CompoundPair(GenericModel, Generic[OutputFormatT]): compounds: conlist(CompoundObject, min_items=2, max_items=2) # type: ignore # pylint: disable=line-too-long # fmt: on outputFormat: Optional[OutputFormatT] + options: Optional[Dict[str, Any]] = None CompoundRequest = Request[CompoundModelType, CompoundModel[CompoundFormat]] @@ -265,6 +267,7 @@ class MatchModel(BaseModel): targets: List[CompoundObject] outputFormat: MatchOutputFormat flag: Optional[str] = "ALL" + options: Optional[Dict[str, Any]] = None MapAtomResponse = Response[MapAtomModelType, MapModel[MapAtomModel]] @@ -328,6 +331,7 @@ class CommonBitsModelType(BaseModel): class CommonBitsModel(BaseModel): source: CompoundObject targets: List[CompoundObject] + options: Optional[Dict[str, Any]] = None class CommonBitsCountModel(BaseModel): @@ -397,6 +401,7 @@ class SimilaritiesModel(BaseModel): metric: SimilarityMetric alpha: Optional[float] = 0.5 beta: Optional[float] = 0.5 + options: Optional[Dict[str, Any]] = None @validator("alpha", "beta") def tversky_factor( @@ -455,6 +460,7 @@ class ValidationResultsModelType(BaseModel): class ValidationModel(BaseModel): compound: CompoundObject validations: List[Validations] + options: Optional[Dict[str, Any]] = None class ValidationResultsModel(BaseModel): @@ -527,6 +533,7 @@ class DescriptorModelType(BaseModel): class DescriptorModel(BaseModel): compound: CompoundObject descriptors: List[Descriptors] + options: Optional[Dict[str, Any]] = None class DescriptorResultModelType(BaseModel): @@ -598,7 +605,7 @@ def make_descriptor_response( class RenderModel(BaseModel): compound: CompoundObject outputFormat: str - options: Optional[Dict[str, Union[int, float, bool, str]]] = None + options: Optional[Dict[str, Any]] = None class RenderModelType(BaseModel): diff --git a/api/http/indigo_service/service.py b/api/http/indigo_service/service.py index bac29cf029..d6f89b32ee 100644 --- a/api/http/indigo_service/service.py +++ b/api/http/indigo_service/service.py @@ -16,7 +16,7 @@ # limitations under the License. # -from typing import List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union from indigo import IndigoObject from indigo.inchi import IndigoInchi @@ -25,6 +25,15 @@ from indigo_service.indigo_tools import indigo +def set_indigo_options( + options: Optional[Dict[str, Union[str, int, float, bool]]] +) -> None: + if options: + for option, value in options.items(): + indigo().setOption(option, value) + return + + def extract_compounds( pairs: List[Tuple[str, jsonapi.CompoundFormat]], modifiers: Optional[List[jsonapi.CompoundModifiers]] = None, diff --git a/api/http/tests/test_indigo_http.py b/api/http/tests/test_indigo_http.py index 8f7c13caeb..effdca4c64 100644 --- a/api/http/tests/test_indigo_http.py +++ b/api/http/tests/test_indigo_http.py @@ -4,7 +4,7 @@ import os import pathlib import xml.etree.ElementTree as elTree -from typing import Any, BinaryIO, Dict, List, Optional +from typing import Any, BinaryIO, Dict, Iterable, List, Optional import PyPDF2 import pytest @@ -28,6 +28,18 @@ }, ] +test_stereo_err_structure = { + "structure": "C1[C@@H]=CC=[C@H]C=1", + "format": "auto", +} + +test_bad_valence_structure = {"structure": "C(=O)(=O)(=O)=O", "format": "auto"} + +test_isotopes_structure = { + "structure": "InChI=1S/C2H6OS/c1-4(2)3/h1-2H3/i1D3,2D3", + "format": "auto", +} + # Convert @@ -344,6 +356,183 @@ def test_render_incorrect_options() -> None: ) +def get_request_attributes( + compound: Optional[dict[str, str]] = None, + modifiers: Optional[Iterable[tuple[str, Any]]] = None, + source: Optional[dict[str, str]] = None, + targets: Optional[list[dict[str, str]]] = None, + fp: Optional[str] = None, + metric: Optional[str] = None, + descriptors: Optional[list[str]] = None, + validations: Optional[list[str]] = None, + output_format: Optional[str] = None, +) -> dict[str, Any]: + if compound and modifiers: + compound.update(modifiers) + request = { + "compound": compound, + "source": source, + "targets": targets, + "fingerprint": fp, + "metric": metric, + "descriptors": descriptors, + "validations": validations, + "outputFormat": output_format, + "options": None, + } + return dict((k, v) for k, v in request.items() if v is not None) + + +@pytest.mark.parametrize( + "url, type_, attributes, fail_response, fixing_option, ok_response", + [ + ( + [ + "similarities", + "similarities", + get_request_attributes( + source=test_stereo_err_structure, + targets=[test_stereo_err_structure], + fp="sim", + metric="tanimoto", + ), + {400: "chirality not possible on atom #1"}, + [("options", {"ignore-stereochemistry-errors": 1})], + {200: None}, + ] + ), + ( + [ + "exactMatch", + "match", + get_request_attributes( + source=test_stereo_err_structure, + targets=[test_stereo_err_structure], + output_format="highlightedTargetSmiles", + ), + {400: "chirality not possible on atom #1"}, + [("options", {"ignore-stereochemistry-errors": 1})], + {200: None}, + ] + ), + ( + [ + "convert", + "convert", + get_request_attributes( + compound=test_stereo_err_structure, + modifiers=[("modifiers", ["clean2d"])], + output_format="smiles", + ), + {400: "chirality not possible on atom #1"}, + [("options", {"ignore-stereochemistry-errors": 1})], + {200: None}, + ] + ), + ( + [ + "validate", + "validation", + get_request_attributes( + compound=test_bad_valence_structure, + validations=["badValence"], + ), + { + 200: { + "badValence": ( + "element: bad valence on C having 8 drawn bonds, " + "charge 0, and 0 radical electrons" + ) + } + }, + [("options", {"ignore-bad-valence": 1})], + {200: {"badValence": ""}}, + ] + ), + ( + [ + "descriptors", + "descriptor", + get_request_attributes( + compound=test_isotopes_structure, + descriptors=["grossFormula"], + ), + {200: {"grossFormula": "C2 H6 O S"}}, + [("options", {"gross-formula-add-isotopes": 1})], + {200: {"grossFormula": "C2 D6 O S"}}, + ] + ), + ( + [ + "commonBits", + "commonBits", + get_request_attributes( + source=test_stereo_err_structure, + targets=[test_stereo_err_structure], + ), + {400: "chirality not possible on atom #1"}, + [("options", {"ignore-stereochemistry-errors": 1})], + {200: None}, + ] + ), + ( + [ + "render", + "render", + get_request_attributes( + compound=test_stereo_err_structure, + output_format="image/png", + ), + {400: "chirality not possible on atom #1"}, + [("options", {"ignore-stereochemistry-errors": 1})], + {200: None}, + ] + ), + ], +) +def test_indigo_options( + url: str, + type_: str, + attributes: dict[str, Any], + fail_response: dict[int, str], + fixing_option: Iterable[tuple[str, Any]], + ok_response: dict[int, Optional[dict[str, str]]], +) -> None: + response = client.post( + f"/indigo/{url}", + json={ + "data": { + "type": type_, + "attributes": attributes, + } + }, + ) + + for status_code, message in fail_response.items(): + assert response.status_code == status_code + if status_code == 200: + assert response.json()["data"]["attributes"] == message + elif status_code == 400: + assert message in response.json()["errors"][0]["detail"] + + attributes.update(fixing_option) + + options_response = client.post( + f"/indigo/{url}", + json={ + "data": { + "type": type_, + "attributes": attributes, + } + }, + ) + + for status_code, message in ok_response.items(): # type: ignore + assert options_response.status_code == status_code + if message: + assert options_response.json()["data"]["attributes"] == message + + # TODO: /indigo/render with alternative responses types # def render_request( # structure: str, output_format: str, options: Dict = None From 62f91fd15700509edb72ac4d43371aafaa10b911 Mon Sep 17 00:00:00 2001 From: Alina Tikhova Date: Fri, 31 Mar 2023 01:51:35 +0300 Subject: [PATCH 2/3] #1083: indigo-service: linters fixes --- api/http/indigo_service/service.py | 1 - api/http/tests/test_indigo_http.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/api/http/indigo_service/service.py b/api/http/indigo_service/service.py index d6f89b32ee..aba2acef51 100644 --- a/api/http/indigo_service/service.py +++ b/api/http/indigo_service/service.py @@ -31,7 +31,6 @@ def set_indigo_options( if options: for option, value in options.items(): indigo().setOption(option, value) - return def extract_compounds( diff --git a/api/http/tests/test_indigo_http.py b/api/http/tests/test_indigo_http.py index effdca4c64..caee1a1adb 100644 --- a/api/http/tests/test_indigo_http.py +++ b/api/http/tests/test_indigo_http.py @@ -356,12 +356,12 @@ def test_render_incorrect_options() -> None: ) -def get_request_attributes( +def get_request_attributes( # pylint: disable=too-many-arguments compound: Optional[dict[str, str]] = None, modifiers: Optional[Iterable[tuple[str, Any]]] = None, source: Optional[dict[str, str]] = None, targets: Optional[list[dict[str, str]]] = None, - fp: Optional[str] = None, + fingerprint: Optional[str] = None, metric: Optional[str] = None, descriptors: Optional[list[str]] = None, validations: Optional[list[str]] = None, @@ -373,7 +373,7 @@ def get_request_attributes( "compound": compound, "source": source, "targets": targets, - "fingerprint": fp, + "fingerprint": fingerprint, "metric": metric, "descriptors": descriptors, "validations": validations, @@ -393,7 +393,7 @@ def get_request_attributes( get_request_attributes( source=test_stereo_err_structure, targets=[test_stereo_err_structure], - fp="sim", + fingerprint="sim", metric="tanimoto", ), {400: "chirality not possible on atom #1"}, @@ -490,7 +490,7 @@ def get_request_attributes( ), ], ) -def test_indigo_options( +def test_indigo_options( # pylint: disable=too-many-arguments url: str, type_: str, attributes: dict[str, Any], From 273510d6a9c5162a622dba20b01be16a7a0b8b2d Mon Sep 17 00:00:00 2001 From: Alina Tikhova Date: Fri, 31 Mar 2023 12:12:54 +0300 Subject: [PATCH 3/3] #1083: indigo-service: removed prints --- api/http/indigo_service/indigo_tools.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/api/http/indigo_service/indigo_tools.py b/api/http/indigo_service/indigo_tools.py index de911ccf53..07fbe3fb35 100644 --- a/api/http/indigo_service/indigo_tools.py +++ b/api/http/indigo_service/indigo_tools.py @@ -26,22 +26,14 @@ __cur_thread: ContextVar[int] = ContextVar( "thread_id", default=threading.get_ident() ) -print("BEGINNING", threading.get_ident(), __cur_thread.get()) def indigo(thread_local: bool = False) -> Indigo: - # print("__cur_thread", __cur_thread.get()) - # print("__indigo_1", __indigo.get().getSessionId()) - # print("thread_local ----> ", thread_local) if thread_local: - # print("THREAD LOCAL !!!") ident = threading.get_ident() - # print("def indigo ----> ", ident) if __cur_thread.get() != ident: __indigo.set(Indigo()) __cur_thread.set(ident) - # print("def indigo ----> ", threading.get_ident()) - # print("__indigo_2", __indigo.get().getSessionId()) return __indigo.get()