Skip to content

Commit

Permalink
Add resource for basic and acid PKA calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
khyurri committed Jul 20, 2022
1 parent 869db9c commit ac2f9bd
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 273 deletions.
17 changes: 11 additions & 6 deletions api/http/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ repos:
args:
[
"-sn",
"--rcfile=api/http/pylintrc",
"indigo_service",
"tests"
"--rcfile=api/http/pylintrc"
]
language: system
types: [ python ]


- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.910"
- repo: local
hooks:
- id: mypy
name: mypy
language: system
entry: mypy
args:
[
"--config-file",
"api/http/mypy.ini"
]
types: [ python ]
2 changes: 1 addition & 1 deletion api/http/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# limitations under the License.

# For test purposes
FROM python:3.9-slim-buster as indigo_service_dev
FROM python:3.10-slim-buster as indigo_service_dev

RUN mkdir -p /opt/indigo
WORKDIR /opt/indigo
Expand Down
118 changes: 53 additions & 65 deletions api/http/indigo_service/indigo_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def compounds(
jsonapi.ValidationRequest,
jsonapi.CompoundConvertRequest,
jsonapi.RenderRequest,
jsonapi.PKARequest,
],
) -> List[Tuple[str, jsonapi.CompoundFormat]]:
return service.extract_pairs(request.data.attributes.compound)
Expand All @@ -81,7 +82,7 @@ def targets(


@app.get(f"{BASE_URL_INDIGO}/version", response_model=jsonapi.VersionResponse)
def indigo_version() -> jsonapi.VersionResponse:
async def indigo_version() -> jsonapi.VersionResponse:
return jsonapi.make_version_response(indigo().version())


Expand All @@ -90,7 +91,7 @@ def indigo_version() -> jsonapi.VersionResponse:
response_model=jsonapi.SimilaritiesResponse,
response_model_exclude_unset=True,
)
def similarities(
async def similarities(
request: jsonapi.SimilaritiesRequest,
) -> jsonapi.SimilaritiesResponse:

Expand Down Expand Up @@ -122,7 +123,7 @@ def similarities(
response_model=jsonapi.MatchResponse, # type: ignore
response_model_exclude_unset=True,
)
def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
async def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
compound, *_ = service.extract_compounds(source(request))
target_pairs = targets(request)
target_compounds = service.extract_compounds(target_pairs)
Expand All @@ -149,7 +150,7 @@ def exact_match(request: jsonapi.MatchRequest) -> jsonapi.MatchResponse:
response_model=jsonapi.CompoundResponse,
response_model_exclude_unset=True,
)
def convert(
async def convert(
request: jsonapi.CompoundConvertRequest,
) -> jsonapi.CompoundResponse:
compound, *_ = service.extract_compounds(
Expand All @@ -165,7 +166,9 @@ def convert(
response_model=jsonapi.ValidationResponse,
response_model_exclude_unset=True,
)
def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse:
async def validate(
request: jsonapi.ValidationRequest
) -> jsonapi.ValidationResponse:
compound, *_ = service.extract_compounds(compounds(request))
validations = request.data.attributes.validations
results = {}
Expand All @@ -179,7 +182,7 @@ def validate(request: jsonapi.ValidationRequest) -> jsonapi.ValidationResponse:
response_model=jsonapi.DescriptorResponse,
response_model_exclude_unset=True,
)
def descriptors(
async def descriptors(
request: jsonapi.DescriptorRequest,
) -> jsonapi.DescriptorResponse:
compound, *_ = service.extract_compounds(compounds(request))
Expand All @@ -190,12 +193,54 @@ def descriptors(
return jsonapi.make_descriptor_response(results)


@app.post(f"{BASE_URL_INDIGO}/pka", response_model=jsonapi.PKAResponse)
async def pka(request: jsonapi.PKARequest) -> jsonapi.PKAResponse:
compound, *_ = service.extract_compounds(compounds(request))
if request.data.attributes.pka_model == jsonapi.PKAModel.ADVANCED:
indigo().setOption("pKa-model", jsonapi.PKAModel.ADVANCED.value)
pka_model_level = request.data.attributes.pka_model_level
pka_model_min_level = request.data.attributes.pka_model_min_level
pka_values = jsonapi.AtomToValueContainer()
pka_type = request.data.attributes.pka_type
pka_model_build = request.data.attributes.pka_model_build

if pka_model_build is not None:
service.build_pka_model(
pka_model_build.sdf,
pka_model_build.max_level,
pka_model_build.threshold,
)

for atom in compound.iterateAtoms():
if pka_type == jsonapi.PKAType.BASIC:
pka_values.mappings.append(
jsonapi.AtomToValueMapping(
index=atom.index(),
symbol=atom.symbol(),
value=compound.getBasicPkaValue(
atom, pka_model_level, pka_model_min_level
),
)
)
elif pka_type == jsonapi.PKAType.ACID:
pka_values.mappings.append(
jsonapi.AtomToValueMapping(
index=atom.index(),
symbol=atom.symbol(),
value=compound.getAcidPkaValue(
atom, pka_model_level, pka_model_min_level
),
)
)
return jsonapi.make_pka_response(pka_values)


@app.post(
f"{BASE_URL_INDIGO}/commonBits",
response_model=jsonapi.CommonBitsResponse,
response_model_exclude_unset=True,
)
def common_bits(
async def common_bits(
request: jsonapi.CommonBitsRequest,
) -> jsonapi.CommonBitsResponse:
compound, *_ = service.extract_compounds(source(request))
Expand All @@ -209,9 +254,7 @@ def common_bits(


@app.post(f"{BASE_URL_INDIGO}/render", response_model=jsonapi.RenderResponse)
def render(
request: jsonapi.RenderRequest,
) -> jsonapi.RenderResponse:
async def render(request: jsonapi.RenderRequest,) -> jsonapi.RenderResponse:
compound, *_ = service.extract_compounds(compounds(request))
output_format = request.data.attributes.outputFormat
indigo_renderer = IndigoRenderer(indigo())
Expand Down Expand Up @@ -245,58 +288,3 @@ def run_debug() -> None:

if __name__ == "__main__":
run_debug()


# TODO: /indigo/render with alternative responses types
# @app.post(f"{BASE_URL_INDIGO}/render")
# def render(
# request: jsonapi.RenderRequest,
# ) -> Union[Response, FileResponse]:
# compound, *_ = service.extract_compounds(compounds(request))
# output_format = request.data.attributes.outputFormat
# indigo_renderer = IndigoRenderer(indigo())
# 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)
# if output_format == "image/png":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# response = Response(
# result,
# headers={"Content-Type": "image/png"}
# )
# elif output_format == "image/png;base64":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# decoded_image = base64.b64encode(result).decode("utf-8")
# image_base64 = f"data:image/png;base64,{decoded_image}"
# response = Response(
# image_base64,
# headers={"Content-Type": "image/png"}
# )
# elif output_format == "image/svg+xml":
# result = indigo_renderer.renderToString(compound)
# response = Response(
# result,
# headers={"Content-Type": "image/svg+xml"}
# )
# elif output_format == "application/pdf":
# result = indigo_renderer.renderToBuffer(compound).tobytes()
# response = Response(
# result, headers={
# "Content-Type": "application/pdf",
# "Content-Disposition": "attachment; filename=mol.pdf"
# }
# )
# else:
# raise HTTPException(
# status_code=400,
# detail=f"Incorrect output format {output_format}"
# )
# return response
63 changes: 57 additions & 6 deletions api/http/indigo_service/jsonapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,6 @@ class Descriptors(str, Enum):
MONOISOTOPIC_MASS = "monoisotopicMass"
MOST_ABUNDANT_MASS = "mostAbundantMass"
NAME = "name"
GET_ACID_PKA_VALUE = "acidPkaValue"
GET_BASIC_PKA_VALUE = "basicPkaValue"
GROSS_FORMULA = "grossFormula"


Expand Down Expand Up @@ -563,8 +561,6 @@ class DescriptorResultModel(BaseModel):
monoisotopicMass: Optional[str]
mostAbundantMass: Optional[str]
name: Optional[str]
getAcidPkaValue: Optional[str]
getBasicPkaValue: Optional[str]
grossFormula: Optional[str]


Expand All @@ -585,6 +581,62 @@ def make_descriptor_response(
]


# PKA models


class PKAType(str, Enum):
ACID = "acid"
BASIC = "basic"


class PKAModel(str, Enum):
SIMPLE = "simple"
ADVANCED = "advanced"


class PKAModelBuild(BaseModel):
sdf: str
max_level: int
threshold: float


class PKARequestModel(BaseModel):
compound: CompoundObject
pka_model_build: Optional[PKAModelBuild]
pka_model: PKAModel
pka_type: PKAType = PKAType.ACID
pka_model_level: int = 0
pka_model_min_level: int = 0


class PKARequestModelType(BaseModel):
__root__ = "pka"


class AtomToValueMapping(BaseModel):
index: int
symbol: str
value: float


class AtomToValueContainer(BaseModel):
mappings: list[AtomToValueMapping] = []


class PKAResultModelType(BaseModel):
__root__ = "pkaResult"


PKARequest = Request[PKARequestModelType, PKARequestModel]
PKAResponse = Response[PKAResultModelType, AtomToValueContainer]


def make_pka_response(mappings: AtomToValueContainer) -> PKAResponse:
return PKAResponse(
**{"data": {"type": "pkaResult", "attributes": mappings}}
)


# Render


Expand Down Expand Up @@ -618,8 +670,7 @@ class RenderResultModel(BaseModel):


def make_render_response(
raw_image: bytes,
output_format: str,
raw_image: bytes, output_format: str
) -> RenderResponse:
if output_format == "image/svg+xml":
str_image = raw_image.decode("utf-8")
Expand Down
19 changes: 18 additions & 1 deletion api/http/indigo_service/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
# limitations under the License.
#

import logging
import tempfile
from pathlib import Path
from typing import List, Optional, Tuple, Union

from indigo import IndigoObject
from indigo import IndigoException, IndigoObject
from indigo.inchi import IndigoInchi

from indigo_service import jsonapi
from indigo_service.indigo_tools import indigo

logger = logging.getLogger(__name__)


def extract_compounds(
pairs: List[Tuple[str, jsonapi.CompoundFormat]],
Expand Down Expand Up @@ -142,3 +147,15 @@ def get_descriptor(
compound: IndigoObject, descriptor: jsonapi.Descriptors
) -> str:
return str(getattr(compound, descriptor.value)())


def build_pka_model(sdf: str, max_level: int, threshold: float) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
model_file = Path(tmp_dir) / "model.sdf"
with open(model_file, mode="w", encoding="utf-8") as sdf_file:
sdf_file.write(sdf)
try:
indigo().buildPkaModel(max_level, threshold, str(model_file))
except IndigoException as err:
logger.exception(err)
raise IndigoException("Unable to build pka model") from err
1 change: 1 addition & 0 deletions api/http/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
python_version = 3.9
strict = True
plugins = pydantic.mypy
exclude = dist

[mypy-indigo.*]
ignore_missing_imports = True
Expand Down
5 changes: 4 additions & 1 deletion api/http/pylintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[FORMAT]
max-line-length = 79

[REPORTS]
output-format=parseable

[MESSAGE CONTROL]
disable = missing-module-docstring,
missing-class-docstring,
Expand All @@ -9,4 +12,4 @@ disable = missing-module-docstring,
wrong-import-order,
too-few-public-methods,
no-name-in-module,
fixme
fixmem
1 change: 1 addition & 0 deletions api/http/requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ types-requests==2.26.3
urllib3==1.26.7
isort==5.9.3
pylint==2.11.1
mypy==0.910
Loading

0 comments on commit ac2f9bd

Please sign in to comment.