Skip to content

Commit

Permalink
Merge pull request #310 from AllenNeuralDynamics/release-v0.17.0
Browse files Browse the repository at this point in the history
Release v0.17.0
  • Loading branch information
jtyoung84 authored Dec 23, 2024
2 parents da0d5c2 + 4d8dd0c commit 11026a8
Show file tree
Hide file tree
Showing 17 changed files with 545 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/aind_metadata_service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""REST service to retrieve metadata from databases."""

__version__ = "0.16.0"
__version__ = "0.17.0"
1 change: 1 addition & 0 deletions src/aind_metadata_service/mgi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package to handle mgi requests"""
88 changes: 88 additions & 0 deletions src/aind_metadata_service/mgi/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Module to handle retrieving information from MGI"""

import logging
from typing import Any, List, Optional, Union

import requests
from pydantic import BaseModel, Extra, Field, HttpUrl
from pydantic_settings import BaseSettings

from aind_metadata_service.response_handler import ModelResponse


class MgiSettings(BaseSettings):
"""Settings required for endpoint"""

url: HttpUrl = Field(
default="https://www.informatics.jax.org//quicksearch/alleleBucket"
)

class Config:
"""Set env prefix and forbid extra fields."""

env_prefix = "MGI_"
extra = Extra.forbid


class MgiSummaryRow(BaseModel):
"""Model of Summary Row dictionary returned"""

detailUri: Optional[str] = Field(default=None)
featureType: Optional[str] = Field(default=None)
strand: Optional[str] = Field(default=None)
chromosome: Optional[str] = Field(default=None)
stars: Optional[str] = Field(default=None)
bestMatchText: Optional[str] = Field(default=None)
bestMatchType: Optional[str] = Field(default=None)
name: Optional[str] = Field(default=None)
location: Optional[str] = Field(default=None)
symbol: Optional[str] = Field(default=None)


class MgiResponse(BaseModel):
"""Model for response from MGI"""

summaryRows: List[MgiSummaryRow]
totalCount: Optional[int] = Field(default=None)
meta: Optional[Any] = Field(default=None)


class MgiClient:
"""Client to connect to Mgi"""

def __init__(self, settings: MgiSettings):
"""Class constructor"""

self.settings = settings

def get_allele_info(
self, allele_name: str
) -> Union[MgiResponse, ModelResponse]:
"""
Get allele info from mgi endpoint
Parameters
----------
allele_name : str
Returns
-------
MgiResponse
"""
try:
params = {
"queryType": "exactPhrase",
"query": allele_name,
"submit": "Quick+Search",
"startIndex": "0",
"results": "1",
}
response = requests.get(
url=self.settings.url.unicode_string(), params=params
)
response.raise_for_status()
response_model = MgiResponse(**response.json())
return response_model
except Exception as e:
logging.exception(e)
return ModelResponse.internal_server_error_response()
66 changes: 66 additions & 0 deletions src/aind_metadata_service/mgi/mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Module to handle mapping data from mgi to aind-data-schema models"""

import logging
import re
from typing import Union

from aind_data_schema_models.pid_names import PIDName
from aind_data_schema_models.registries import Registry

from aind_metadata_service.client import StatusCodes
from aind_metadata_service.mgi.client import MgiResponse
from aind_metadata_service.response_handler import ModelResponse


class MgiMapper:
"""Class that handles mapping mgi info"""

DETAIL_URI_PATTERN = re.compile(r"/allele/MGI:(\d+)")

def __init__(self, mgi_info: Union[MgiResponse, ModelResponse]):
"""Class constructor"""

self.mgi_info = mgi_info

def get_model_response(self) -> ModelResponse:
"""Get a model response from mgi ingo"""

try:
if isinstance(self.mgi_info, ModelResponse):
return self.mgi_info

if len(self.mgi_info.summaryRows) == 0:
return ModelResponse.no_data_found_error_response()
first_summary_row = self.mgi_info.summaryRows[0]

# 4 stars represent an exact match
if (
first_summary_row.stars != "****"
or first_summary_row.bestMatchType != "Synonym"
):
return ModelResponse.no_data_found_error_response()

if (
re.match(self.DETAIL_URI_PATTERN, first_summary_row.detailUri)
is not None
):
registry_identifier = re.match(
self.DETAIL_URI_PATTERN, first_summary_row.detailUri
).group(1)
else:
registry_identifier = None

pid_name = PIDName(
name=first_summary_row.symbol,
abbreviation=None,
registry=Registry.MGI,
registry_identifier=registry_identifier,
)

model_response = ModelResponse(
aind_models=[pid_name], status_code=StatusCodes.DB_RESPONDED
)
return model_response
except Exception as e:
logging.exception(e)
return ModelResponse.internal_server_error_response()
2 changes: 2 additions & 0 deletions src/aind_metadata_service/response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from aind_data_schema.core.rig import Rig
from aind_data_schema.core.session import Session
from aind_data_schema.core.subject import Subject
from aind_data_schema_models.pid_names import PIDName
from aind_metadata_mapper.core import JobResponse
from fastapi import Response
from fastapi.encoders import jsonable_encoder
Expand All @@ -37,6 +38,7 @@
Instrument,
Rig,
Session,
PIDName,
)


Expand Down
20 changes: 20 additions & 0 deletions src/aind_metadata_service/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
LabTracksClient,
LabTracksSettings,
)
from aind_metadata_service.mgi.client import MgiClient, MgiSettings
from aind_metadata_service.mgi.mapping import MgiMapper
from aind_metadata_service.response_handler import EtlResponse
from aind_metadata_service.sharepoint.client import (
SharePointClient,
Expand Down Expand Up @@ -65,6 +67,8 @@
access_token=SMARTSHEET_PROTOCOLS_TOKEN, sheet_id=SMARTSHEET_PROTOCOLS_ID
)

mgi_settings = MgiSettings()

app = FastAPI()

app.add_middleware(
Expand All @@ -90,6 +94,7 @@
)
slims_client = SlimsHandler(settings=slims_settings)
tars_client = TarsClient(azure_settings=tars_settings)
mgi_client = MgiClient(settings=mgi_settings)

template_directory = os.path.abspath(
os.path.join(os.path.dirname(__file__), "templates")
Expand Down Expand Up @@ -148,6 +153,21 @@ async def retrieve_protocols(
return model_response.map_to_json_response()


@app.get("/mgi_allele/{allele_name}")
async def retrieve_mgi_allele(
allele_name,
):
"""Retrieves allele information from mgi"""

# TODO: We can probably cache the response if it's 200
mgi_response = await run_in_threadpool(
mgi_client.get_allele_info, allele_name=allele_name
)
mapper = MgiMapper(mgi_info=mgi_response)
model_response = mapper.get_model_response()
return model_response.map_to_json_response()


@app.get("/perfusions/{subject_id}")
async def retrieve_perfusions(subject_id):
"""Retrieves perfusion information from smartsheet"""
Expand Down
Loading

0 comments on commit 11026a8

Please sign in to comment.