Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat 314 smartspim imaging #339

Merged
merged 10 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
exclude =
.git,
__pycache__,
build
build.
env
max-complexity = 10
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ server = [
'fastapi==0.115.0',
'uvicorn[standard]==0.31.0',
'python-dateutil',
'aind-slims-api==0.1.19',
'azure-identity==1.15.0'
'aind-slims-api==0.1.21',
'azure-identity==1.15.0',
'networkx'
]

client = [
Expand Down Expand Up @@ -97,5 +98,5 @@ line_length = 79
profile = "black"

[tool.interrogate]
exclude = ["setup.py", "docs", "build"]
exclude = ["setup.py", "docs", "build", "env"]
fail-under = 100
1 change: 1 addition & 0 deletions src/aind_metadata_service/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class StatusCodes(Enum):
NO_DATA_FOUND = 404
MULTI_STATUS = 207
UNPROCESSIBLE_ENTITY = 422
BAD_REQUEST = 400


class AindMetadataServiceClient:
Expand Down
33 changes: 33 additions & 0 deletions src/aind_metadata_service/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Additional response models not defined in aind-data-schema"""

from datetime import datetime
from typing import Optional

from aind_data_schema.core.data_description import Funding
Expand Down Expand Up @@ -36,6 +37,38 @@ class FundingInformation(Funding):
investigators: Optional[str] = Field(default=None)


class SpimImagingInformation(BaseModel):
"""SmartSPIM Imaging information that will be returned to the user that
requests information from the SmartSPIM Imaging SLIMS Workflow"""

specimen_id: str = Field(..., description="Specimen ID")
subject_id: str = Field(..., description="Subject ID")
protocol_name: Optional[str] = Field(None, description="Protocol Name")
protocol_id: Optional[str] = Field(None, description="Protocol ID")
date_performed: Optional[datetime] = Field(
None, description="Date Performed"
)
chamber_immersion_medium: Optional[str] = Field(
None, description="Chamber Immersion Medium"
)
sample_immersion_medium: Optional[str] = Field(
None, description="Sample Immersion Medium"
)
chamber_refractive_index: Optional[float] = Field(
None, description="Chamber Refractive Index"
)
sample_refractive_index: Optional[float] = Field(
None, description="Sample Refractive Index"
)
instrument_id: Optional[str] = Field(None, description="Instrument ID")
experimenter_name: Optional[str] = Field(
None, description="Experimenter Name"
)
z_direction: Optional[str] = Field(None, description="Z Direction")
y_direction: Optional[str] = Field(None, description="Y Direction")
x_direction: Optional[str] = Field(None, description="X Direction")


class ViralMaterialInformation(ViralMaterial):
"""Viral Material with Stock Titer from SLIMS"""

Expand Down
14 changes: 14 additions & 0 deletions src/aind_metadata_service/response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ def no_data_found_error_response(cls):
message="No Data Found.",
)

@classmethod
def bad_request_error_response(cls, message="Bad Request."):
"""Bad Request Error"""
return cls(
status_code=StatusCodes.BAD_REQUEST,
aind_models=[],
message=message,
)

@staticmethod
def _validate_model(model) -> Optional[str]:
"""Helper method to validate a model and return validation errors."""
Expand Down Expand Up @@ -188,6 +197,11 @@ def map_to_json_response(self, validate: bool = True) -> JSONResponse:
status_code=StatusCodes.INTERNAL_SERVER_ERROR.value,
content=({"message": self.message, "data": None}),
)
elif self.status_code == StatusCodes.BAD_REQUEST:
response = JSONResponse(
status_code=StatusCodes.BAD_REQUEST.value,
content=({"message": self.message, "data": None}),
)
else:
response = self._map_data_response(validate=validate)
return response
Expand Down
29 changes: 28 additions & 1 deletion src/aind_metadata_service/server.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Starts and runs a Flask Service"""

import os
from typing import Optional

from aind_metadata_mapper.bergamo.session import BergamoEtl
from aind_metadata_mapper.bergamo.session import (
JobSettings as BergamoJobSettings,
)
from fastapi import FastAPI, Request
from fastapi import FastAPI, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
Expand Down Expand Up @@ -218,6 +219,32 @@ async def retrieve_project_names():
return json_response


@app.get("/slims/smartspim_imaging")
async def retrieve_smartspim_imaging(
subject_id: Optional[str] = Query(None, alias="subject_id"),
start_date_gte: Optional[str] = Query(
None,
alias="start_date_gte",
description="Experiment run created on or after. (ISO format)",
),
end_date_lte: Optional[str] = Query(
None,
alias="end_date_lte",
description="Experiment run created on or before. (ISO format)",
),
):
"""
Retrieves SPIM Imaging data from SLIMS server
"""
response = await run_in_threadpool(
slims_client.get_slims_imaging_response,
subject_id=subject_id,
start_date=start_date_gte,
end_date=end_date_lte,
)
return response


@app.get("/subject/{subject_id}")
async def retrieve_subject(subject_id):
"""
Expand Down
83 changes: 83 additions & 0 deletions src/aind_metadata_service/slims/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Module for slims client"""

import json
import logging
from datetime import datetime
from typing import Optional, Union

from aind_data_schema.core.instrument import Instrument
from aind_data_schema.core.procedures import Procedures
Expand All @@ -12,6 +15,7 @@
fetch_ecephys_sessions,
fetch_histology_procedures,
)
from fastapi.responses import JSONResponse
from pydantic import Extra, Field, SecretStr
from pydantic_settings import BaseSettings
from requests.models import Response
Expand All @@ -20,6 +24,8 @@

from aind_metadata_service.client import StatusCodes
from aind_metadata_service.response_handler import ModelResponse
from aind_metadata_service.slims.imaging.handler import SlimsImagingHandler
from aind_metadata_service.slims.imaging.mapping import SlimsSpimMapper
from aind_metadata_service.slims.procedures.mapping import SlimsHistologyMapper
from aind_metadata_service.slims.sessions.mapping import SlimsSessionMapper

Expand Down Expand Up @@ -200,3 +206,80 @@ def get_specimen_procedures_model_response(
except Exception as e:
logging.error(repr(e))
return ModelResponse.internal_server_error_response()

@staticmethod
def _parse_date(
date_str: Optional[str],
) -> Union[Optional[datetime], ModelResponse]:
"""Parse a date_str to datetime object or return a Bad Request
response"""
if date_str is None:
return None
else:
try:
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
return dt
except ValueError:
return ModelResponse.bad_request_error_response(
message=f"{date_str} is not valid ISOFormat!"
)

def get_slims_imaging_response(
self,
subject_id: Optional[str],
start_date: Optional[str],
end_date: Optional[str],
) -> JSONResponse:
"""

Parameters
----------
subject_id : str | None
start_date : str | None
Optional ISO Format datetime string
end_date : str | None
Optional ISO Format datetime string
Returns
-------
JSONResponse

"""
if subject_id is not None and subject_id == "":
return ModelResponse.bad_request_error_response(
message="subject_id cannot be an empty string!"
).map_to_json_response()
parsed_start_date = self._parse_date(start_date)
if isinstance(parsed_start_date, ModelResponse):
return parsed_start_date.map_to_json_response()
parsed_end_date = self._parse_date(end_date)
if isinstance(parsed_end_date, ModelResponse):
return parsed_end_date.map_to_json_response()
try:
slims_imaging_handler = SlimsImagingHandler(client=self.client.db)
slims_spim_data = slims_imaging_handler.get_spim_data_from_slims(
subject_id=subject_id,
start_date_greater_than_or_equal=parsed_start_date,
end_date_less_than_or_equal=parsed_end_date,
)
spim_data = SlimsSpimMapper(
slims_spim_data=slims_spim_data
).map_info_from_slims()
if len(spim_data) == 0:
m = ModelResponse.no_data_found_error_response()
return m.map_to_json_response()
response = JSONResponse(
status_code=StatusCodes.VALID_DATA.value,
content=(
{
"message": "Data from SLIMS",
"data": [
json.loads(m.model_dump_json()) for m in spim_data
],
}
),
)
return response
except Exception as e:
logging.exception(e)
m = ModelResponse.internal_server_error_response()
return m.map_to_json_response()
1 change: 1 addition & 0 deletions src/aind_metadata_service/slims/imaging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package for handling imaging info from slims."""
Loading