diff --git a/pyproject.toml b/pyproject.toml index 830550e4..711e2fe7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,9 @@ dynamic = ["version"] dependencies = [ 'pydantic-settings>=2.0', - 'pydantic>=2.7,<2.9', - 'aind-metadata-mapper[bergamo]==0.18.0', - 'aind-data-schema==1.1.1', + 'pydantic>=2.10', + 'aind-metadata-mapper[bergamo]==0.21.0', + 'aind-data-schema==1.2.0', 'typing-extensions==4.12.2' ] @@ -43,7 +43,7 @@ server = [ 'fastapi==0.115.0', 'uvicorn[standard]==0.31.0', 'python-dateutil', - 'aind-slims-api==0.1.15', + 'aind-slims-api==0.1.17', 'azure-identity==1.15.0' ] diff --git a/src/aind_metadata_service/__init__.py b/src/aind_metadata_service/__init__.py index 53d48819..51f6d348 100644 --- a/src/aind_metadata_service/__init__.py +++ b/src/aind_metadata_service/__init__.py @@ -1,3 +1,3 @@ """REST service to retrieve metadata from databases.""" -__version__ = "0.15.1" +__version__ = "0.16.0" diff --git a/src/aind_metadata_service/response_handler.py b/src/aind_metadata_service/response_handler.py index 5c78595f..d8a2fbfb 100644 --- a/src/aind_metadata_service/response_handler.py +++ b/src/aind_metadata_service/response_handler.py @@ -13,6 +13,7 @@ ViralMaterial, ) 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_metadata_mapper.core import JobResponse from fastapi import Response @@ -35,6 +36,7 @@ ProtocolInformation, Instrument, Rig, + Session, ) @@ -89,37 +91,42 @@ def no_data_found_error_response(cls): message="No Data Found.", ) + @staticmethod + def _validate_model(model) -> Optional[str]: + """Helper method to validate a model and return validation errors.""" + validation_error = None + try: + model.__class__.model_validate(model.model_dump()) + except ValidationError as e: + validation_error = repr(e) + except (AttributeError, ValueError, KeyError) as oe: + validation_error = repr(oe) + return validation_error + def _map_data_response( # noqa: C901 self, validate: bool = True ) -> Union[Response, JSONResponse]: """Map ModelResponse with StatusCodes.DB_RESPONDED to a JSONResponse. Perform validations, bypasses validation if flag is set to False.""" + if len(self.aind_models) == 0: status_code = StatusCodes.NO_DATA_FOUND.value content_data = None message = "No Data Found." + elif len(self.aind_models) == 1: aind_model = self.aind_models[0] content_data = jsonable_encoder( json.loads(aind_model.model_dump_json()) ) if validate: - validation_error = None - try: - aind_model.__class__.model_validate( - aind_model.model_dump() - ) - except ValidationError as e: - validation_error = repr(e) - except (AttributeError, ValueError, KeyError) as oe: - validation_error = repr(oe) + validation_error = self._validate_model(aind_model) if validation_error: status_code = StatusCodes.INVALID_DATA.value message = f"Validation Errors: {validation_error}" else: status_code = StatusCodes.VALID_DATA.value message = "Valid Model." - # if validate flag is False else: status_code = StatusCodes.UNPROCESSIBLE_ENTITY.value message = ( @@ -132,6 +139,7 @@ def _map_data_response( # noqa: C901 "There was an error retrieving records from one or more of" " the databases." ) + else: status_code = StatusCodes.MULTIPLE_RESPONSES.value message = "Multiple Items Found." @@ -139,6 +147,25 @@ def _map_data_response( # noqa: C901 jsonable_encoder(json.loads(model.model_dump_json())) for model in self.aind_models ] + + if validate: + # Validate each model and accumulate errors + validation_errors = [] + for model in self.aind_models: + error = self._validate_model(model) + print(error) + if error: + validation_errors.append(error) + + if validation_errors: + message += ( + f" Validation Errors: {', '.join(validation_errors)}" + ) + else: + message += " All Models Valid." + else: + message += " Models have not been validated." + return JSONResponse( status_code=status_code, content=({"message": message, "data": content_data}), diff --git a/src/aind_metadata_service/server.py b/src/aind_metadata_service/server.py index cae05cf3..3f529956 100644 --- a/src/aind_metadata_service/server.py +++ b/src/aind_metadata_service/server.py @@ -122,6 +122,15 @@ async def retrieve_rig(rig_id): return model_response.map_to_json_response(validate=False) +@app.get("/ecephys_sessions_by_subject/{subject_id}") +async def retrieve_sessions(subject_id): + """Retrieves sessions from slims""" + model_response = await run_in_threadpool( + slims_client.get_sessions_model_response, subject_id=subject_id + ) + return model_response.map_to_json_response() + + @app.get("/protocols/{protocol_name}") async def retrieve_protocols( protocol_name, diff --git a/src/aind_metadata_service/sharepoint/las2020/mapping.py b/src/aind_metadata_service/sharepoint/las2020/mapping.py index db9f8825..7a778243 100644 --- a/src/aind_metadata_service/sharepoint/las2020/mapping.py +++ b/src/aind_metadata_service/sharepoint/las2020/mapping.py @@ -2094,7 +2094,7 @@ def map_viral_materials(injectable_materials: List[InjectableMaterial]): ) return viral_materials - def get_procedure(self, subject_id) -> Surgery: + def get_procedure(self, subject_id) -> Optional[Surgery]: """Return Surgery as best as possible from a record.""" subject_procedures = [] if self.has_ip_injection(): @@ -2118,10 +2118,13 @@ def get_procedure(self, subject_id) -> Surgery: injection_materials=injection_materials, ) subject_procedures.append(ro_injection) - return Surgery.model_construct( - experimenter_full_name=self.aind_author_id, - iacuc_protocol=self.aind_protocol, - start_date=self.aind_n_start_date, - end_date=self.aind_n_end_date, - procedures=subject_procedures, - ) + if subject_procedures: + return Surgery.model_construct( + experimenter_full_name=self.aind_author_id, + iacuc_protocol=self.aind_protocol, + start_date=self.aind_n_start_date, + end_date=self.aind_n_end_date, + procedures=subject_procedures, + ) + else: + return None diff --git a/src/aind_metadata_service/sharepoint/las2020/procedures.py b/src/aind_metadata_service/sharepoint/las2020/procedures.py index 6094b160..79342677 100644 --- a/src/aind_metadata_service/sharepoint/las2020/procedures.py +++ b/src/aind_metadata_service/sharepoint/las2020/procedures.py @@ -49,5 +49,6 @@ def get_procedures_from_sharepoint( las_model = LASList.model_validate(list_item.to_json()) mapped_model = MappedLASList(las=las_model) procedure = mapped_model.get_procedure(subject_id=subject_id) - list_of_procedures.append(procedure) + if procedure: + list_of_procedures.append(procedure) return list_of_procedures diff --git a/src/aind_metadata_service/sharepoint/nsb2023/mapping.py b/src/aind_metadata_service/sharepoint/nsb2023/mapping.py index d570f63a..88a31516 100644 --- a/src/aind_metadata_service/sharepoint/nsb2023/mapping.py +++ b/src/aind_metadata_service/sharepoint/nsb2023/mapping.py @@ -7,7 +7,7 @@ from enum import Enum from typing import Any, List, Optional, Union -from aind_data_schema.components.devices import FiberProbe +from aind_data_schema.components.devices import FerruleMaterial, FiberProbe from aind_data_schema.core.procedures import ( Anaesthetic, BrainInjection, @@ -20,11 +20,13 @@ NanojectInjection, NonViralMaterial, OphysProbe, + Procedures, Side, Surgery, ViralMaterial, ) from aind_data_schema.core.subject import Sex +from aind_data_schema_models.organizations import Organization from aind_metadata_service.sharepoint.nsb2023.models import NSBList from aind_metadata_service.sharepoint.nsb2023.models import ( @@ -86,6 +88,13 @@ class InjectionType(Enum): IONTOPHORESIS = "Iontophoresis" +class FiberType(Enum): + """Enum class for Fiber Types""" + + STANDARD = "Standard" + CUSTOM = "Custom (specs in comment)." + + @dataclass class SurgeryDuringInfo: """Container for information related to surgeries during initial or @@ -126,8 +135,10 @@ class BurrHoleInfo: alternating_current: Optional[str] = None inj_duration: Optional[Decimal] = None inj_volume: Optional[List[Decimal]] = None - fiber_implant_depth: Optional[Decimal] = None inj_materials: Optional[List[InjectableMaterial]] = None + fiber_implant_depth: Optional[Decimal] = None + fiber_type: Optional[FiberType] = None + fiber_implant_length: Optional[Decimal] = None @dataclass @@ -200,6 +211,7 @@ class MappedNSBList: r"^[-+]?\d*\.?\d+[eE][-+]?\d+(?![\d.])" ) CONCENTRATION_REGEX = re.compile(r"^\d+(\.\d+)?\s*mg[/]m[lL]$") + LENGTH_MM_REGEX = re.compile(r"^([1-9]\.\d) mm$") def __init__(self, nsb: NSBList): """Class constructor""" @@ -252,6 +264,17 @@ def _parse_length_of_time_str( else: return None + def _parse_fiber_length_mm_str(self, fiber_length_str: Optional[str]): + """Parses length of fiber length strings""" + if fiber_length_str is not None: + parsed_string = re.search(self.LENGTH_MM_REGEX, fiber_length_str) + if parsed_string: + return self._parse_basic_decimal_str(parsed_string.group(1)) + else: + return None + else: + return None + @staticmethod def _parse_datetime_to_date(dt: Optional[datetime]) -> Optional[date]: """Parse date from datetime""" @@ -819,14 +842,14 @@ def aind_burr_1_dv_2(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.burr_1_dv_2) @property - def aind_burr_1_fiber_t(self) -> Optional[Any]: + def aind_burr_1_fiber_t(self) -> Optional[FiberType]: """Maps burr_1_fiber_t to aind model""" return ( None if self._nsb.burr_1_fiber_t is None else { - self._nsb.burr_1_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_1_fiber_t.CUSTOM: None, + self._nsb.burr_1_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_1_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_1_fiber_t, None) ) @@ -883,14 +906,14 @@ def aind_burr_2_d_v_x000(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.burr_2_d_v_x000) @property - def aind_burr_2_fiber_t(self) -> Optional[Any]: + def aind_burr_2_fiber_t(self) -> Optional[FiberType]: """Maps burr_2_fiber_t to aind model""" return ( None if self._nsb.burr_2_fiber_t is None else { - self._nsb.burr_2_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_2_fiber_t.CUSTOM: None, + self._nsb.burr_2_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_2_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_2_fiber_t, None) ) @@ -950,14 +973,14 @@ def aind_burr_3_d_v_x000(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.burr_3_d_v_x000) @property - def aind_burr_3_fiber_t(self) -> Optional[Any]: + def aind_burr_3_fiber_t(self) -> Optional[FiberType]: """Maps burr_3_fiber_t to aind model""" return ( None if self._nsb.burr_3_fiber_t is None else { - self._nsb.burr_3_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_3_fiber_t.CUSTOM: None, + self._nsb.burr_3_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_3_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_3_fiber_t, None) ) @@ -1030,14 +1053,14 @@ def aind_burr_4_d_v_x000(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.burr_4_d_v_x000) @property - def aind_burr_4_fiber_t(self) -> Optional[Any]: + def aind_burr_4_fiber_t(self) -> Optional[FiberType]: """Maps burr_4_fiber_t to aind model""" return ( None if self._nsb.burr_4_fiber_t is None else { - self._nsb.burr_4_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_4_fiber_t.CUSTOM: None, + self._nsb.burr_4_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_4_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_4_fiber_t, None) ) @@ -1126,8 +1149,8 @@ def aind_burr_5_fiber_t(self) -> Optional[Any]: None if self._nsb.burr_5_fiber_t is None else { - self._nsb.burr_5_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_5_fiber_t.CUSTOM: None, + self._nsb.burr_5_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_5_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_5_fiber_t, None) ) @@ -1215,14 +1238,14 @@ def aind_burr_6_d_v_x001(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.burr_6_d_v_x001) @property - def aind_burr_6_fiber_t(self) -> Optional[Any]: + def aind_burr_6_fiber_t(self) -> Optional[FiberType]: """Maps burr_6_fiber_t to aind model""" return ( None if self._nsb.burr_6_fiber_t is None else { - self._nsb.burr_6_fiber_t.STANDARD_PROVIDED_BY_NSB: None, - self._nsb.burr_6_fiber_t.CUSTOM: None, + self._nsb.burr_6_fiber_t.STANDARD_PROVIDED_BY_NSB: FiberType.STANDARD, + self._nsb.burr_6_fiber_t.CUSTOM: FiberType.CUSTOM, }.get(self._nsb.burr_6_fiber_t, None) ) @@ -1596,23 +1619,9 @@ def aind_fiber_implant1_dv(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant1_dv) @property - def aind_fiber_implant1_lengt(self) -> Optional[Any]: + def aind_fiber_implant1_lengt(self) -> Optional[Decimal]: """Maps fiber_implant1_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant1_lengt is None - else { - self._nsb.fiber_implant1_lengt.SELECT: None, - self._nsb.fiber_implant1_lengt.N_15_MM: None, - self._nsb.fiber_implant1_lengt.N_20_MM: None, - self._nsb.fiber_implant1_lengt.N_25_MM: None, - self._nsb.fiber_implant1_lengt.N_30_MM: None, - self._nsb.fiber_implant1_lengt.N_35_MM: None, - self._nsb.fiber_implant1_lengt.N_40_MM: None, - self._nsb.fiber_implant1_lengt.N_45_MM: None, - self._nsb.fiber_implant1_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant1_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant1_lengt) @property def aind_fiber_implant2_dv(self) -> Optional[Decimal]: @@ -1620,23 +1629,9 @@ def aind_fiber_implant2_dv(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant2_dv) @property - def aind_fiber_implant2_lengt(self) -> Optional[Any]: + def aind_fiber_implant2_lengt(self) -> Optional[Decimal]: """Maps fiber_implant2_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant2_lengt is None - else { - self._nsb.fiber_implant2_lengt.SELECT: None, - self._nsb.fiber_implant2_lengt.N_15_MM: None, - self._nsb.fiber_implant2_lengt.N_20_MM: None, - self._nsb.fiber_implant2_lengt.N_25_MM: None, - self._nsb.fiber_implant2_lengt.N_30_MM: None, - self._nsb.fiber_implant2_lengt.N_35_MM: None, - self._nsb.fiber_implant2_lengt.N_40_MM: None, - self._nsb.fiber_implant2_lengt.N_45_MM: None, - self._nsb.fiber_implant2_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant2_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant2_lengt) @property def aind_fiber_implant3_d_x00(self) -> Optional[Decimal]: @@ -1644,23 +1639,9 @@ def aind_fiber_implant3_d_x00(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant3_d_x00) @property - def aind_fiber_implant3_lengt(self) -> Optional[Any]: + def aind_fiber_implant3_lengt(self) -> Optional[Decimal]: """Maps fiber_implant3_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant3_lengt is None - else { - self._nsb.fiber_implant3_lengt.SELECT: None, - self._nsb.fiber_implant3_lengt.N_15_MM: None, - self._nsb.fiber_implant3_lengt.N_20_MM: None, - self._nsb.fiber_implant3_lengt.N_25_MM: None, - self._nsb.fiber_implant3_lengt.N_30_MM: None, - self._nsb.fiber_implant3_lengt.N_35_MM: None, - self._nsb.fiber_implant3_lengt.N_40_MM: None, - self._nsb.fiber_implant3_lengt.N_45_MM: None, - self._nsb.fiber_implant3_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant3_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant3_lengt) @property def aind_fiber_implant4_d_x00(self) -> Optional[Decimal]: @@ -1668,23 +1649,9 @@ def aind_fiber_implant4_d_x00(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant4_d_x00) @property - def aind_fiber_implant4_lengt(self) -> Optional[Any]: + def aind_fiber_implant4_lengt(self) -> Optional[Decimal]: """Maps fiber_implant4_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant4_lengt is None - else { - self._nsb.fiber_implant4_lengt.SELECT: None, - self._nsb.fiber_implant4_lengt.N_15_MM: None, - self._nsb.fiber_implant4_lengt.N_20_MM: None, - self._nsb.fiber_implant4_lengt.N_25_MM: None, - self._nsb.fiber_implant4_lengt.N_30_MM: None, - self._nsb.fiber_implant4_lengt.N_35_MM: None, - self._nsb.fiber_implant4_lengt.N_40_MM: None, - self._nsb.fiber_implant4_lengt.N_45_MM: None, - self._nsb.fiber_implant4_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant4_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant4_lengt) @property def aind_fiber_implant5_d_x00(self) -> Optional[Decimal]: @@ -1692,23 +1659,9 @@ def aind_fiber_implant5_d_x00(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant5_d_x00) @property - def aind_fiber_implant5_lengt(self) -> Optional[Any]: + def aind_fiber_implant5_lengt(self) -> Optional[Decimal]: """Maps fiber_implant5_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant5_lengt is None - else { - self._nsb.fiber_implant5_lengt.SELECT: None, - self._nsb.fiber_implant5_lengt.N_15_MM: None, - self._nsb.fiber_implant5_lengt.N_20_MM: None, - self._nsb.fiber_implant5_lengt.N_25_MM: None, - self._nsb.fiber_implant5_lengt.N_30_MM: None, - self._nsb.fiber_implant5_lengt.N_35_MM: None, - self._nsb.fiber_implant5_lengt.N_40_MM: None, - self._nsb.fiber_implant5_lengt.N_45_MM: None, - self._nsb.fiber_implant5_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant5_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant5_lengt) @property def aind_fiber_implant6_d_x00(self) -> Optional[Decimal]: @@ -1716,23 +1669,9 @@ def aind_fiber_implant6_d_x00(self) -> Optional[Decimal]: return self._map_float_to_decimal(self._nsb.fiber_implant6_d_x00) @property - def aind_fiber_implant6_lengt(self) -> Optional[Any]: + def aind_fiber_implant6_lengt(self) -> Optional[Decimal]: """Maps fiber_implant6_lengt to aind model""" - return ( - None - if self._nsb.fiber_implant6_lengt is None - else { - self._nsb.fiber_implant6_lengt.SELECT: None, - self._nsb.fiber_implant6_lengt.N_15_MM: None, - self._nsb.fiber_implant6_lengt.N_20_MM: None, - self._nsb.fiber_implant6_lengt.N_25_MM: None, - self._nsb.fiber_implant6_lengt.N_30_MM: None, - self._nsb.fiber_implant6_lengt.N_35_MM: None, - self._nsb.fiber_implant6_lengt.N_40_MM: None, - self._nsb.fiber_implant6_lengt.N_45_MM: None, - self._nsb.fiber_implant6_lengt.N_50_MM: None, - }.get(self._nsb.fiber_implant6_lengt, None) - ) + return self._parse_fiber_length_mm_str(self._nsb.fiber_implant6_lengt) @property def aind_first_inj_recovery(self) -> Optional[Decimal]: @@ -3254,8 +3193,10 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj1volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant1_dv, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant1_dv, + fiber_type=self.aind_burr_1_fiber_t, + fiber_implant_length=self.aind_fiber_implant1_lengt, ) elif burr_hole_num == 2: coordinate_depth = self._map_burr_hole_dv( @@ -3292,8 +3233,10 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj2volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant2_dv, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant2_dv, + fiber_type=self.aind_burr_2_fiber_t, + fiber_implant_length=self.aind_fiber_implant2_lengt, ) elif burr_hole_num == 3: coordinate_depth = self._map_burr_hole_dv( @@ -3330,8 +3273,10 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj3volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant3_d_x00, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant3_d_x00, + fiber_type=self.aind_burr_3_fiber_t, + fiber_implant_length=self.aind_fiber_implant3_lengt, ) elif burr_hole_num == 4: coordinate_depth = self._map_burr_hole_dv( @@ -3368,8 +3313,10 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj4volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant4_d_x00, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant4_d_x00, + fiber_type=self.aind_burr_4_fiber_t, + fiber_implant_length=self.aind_fiber_implant4_lengt, ) elif burr_hole_num == 5: coordinate_depth = self._map_burr_hole_dv( @@ -3406,8 +3353,10 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj5volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant5_d_x00, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant5_d_x00, + fiber_type=self.aind_burr_5_fiber_t, + fiber_implant_length=self.aind_fiber_implant5_lengt, ) elif burr_hole_num == 6: coordinate_depth = self._map_burr_hole_dv( @@ -3444,29 +3393,14 @@ def burr_hole_info(self, burr_hole_num: int) -> BurrHoleInfo: inj_volume=self._map_burr_hole_volume( vol=self.aind_inj6volperdepth, dv=coordinate_depth ), - fiber_implant_depth=self.aind_fiber_implant6_d_x00, inj_materials=injectable_materials, + fiber_implant_depth=self.aind_fiber_implant6_d_x00, + fiber_type=self.aind_burr_6_fiber_t, + fiber_implant_length=self.aind_fiber_implant6_lengt, ) else: return BurrHoleInfo() - @staticmethod - def _map_burr_hole_number_to_probe( - burr_hole_num: int, - ) -> Optional[str]: - """Maps NSB Burr hole number into AIND ProbeName""" - # TODO: add probes for burr_hole_nums 5 and 6 - if burr_hole_num == 1: - return "Probe A" - elif burr_hole_num == 2: - return "Probe B" - elif burr_hole_num == 3: - return "Probe C" - elif burr_hole_num == 4: - return "Probe D" - else: - return None - @staticmethod def _map_burr_hole_dv(dv1, dv2, dv3): """Maps dvs for a burr hole to one coordinate depth list""" @@ -3536,6 +3470,48 @@ def _pair_burr_hole_inj_materials( injectable_materials.append(injectable_material) return injectable_materials + def _map_burr_fiber_probe(self, burr_info: BurrHoleInfo) -> FiberProbe: + """Constructs a fiber probe""" + if burr_info.fiber_type == FiberType.STANDARD: + return FiberProbe.model_construct( + manufacturer=Organization.NEUROPHOTOMETRICS, + core_diameter=200, + numerical_aperture=0.37, + ferrule_material=FerruleMaterial.CERAMIC, + total_length=burr_info.fiber_implant_length, + ) + elif burr_info.fiber_type == FiberType.CUSTOM: + # if custom, specs are stored in requestor comments + return FiberProbe.model_construct( + total_length=burr_info.fiber_implant_length, + notes=self.aind_long_requestor_comments, + ) + else: + return FiberProbe.model_construct( + total_length=burr_info.fiber_implant_length, + ) + + @staticmethod + def assign_fiber_probe_names(procedures: List) -> None: + """Assigns ordered names to FiberProbe objects within each fiber implant""" + all_probes = [] + for proc in procedures: + if isinstance(proc, FiberImplant): + all_probes.extend(proc.probes) + + # Sort all probes based on ap (descending) and ml (ascending) + sorted_probes = sorted( + all_probes, + key=lambda probe: ( + -float(probe.stereotactic_coordinate_ap), + float(probe.stereotactic_coordinate_ml), + ), + ) + for probe_index, probe in enumerate(sorted_probes): + probe.ophys_probe.name = f"Fiber_{probe_index}" + + return None + def get_procedure(self) -> List[Surgery]: """Get a List of Surgeries""" # Surgery info @@ -3760,7 +3736,6 @@ def get_procedure(self) -> List[Surgery]: BurrHoleProcedure.FIBER_IMPLANT, BurrHoleProcedure.INJECTION_FIBER_IMPLANT, }: - probe_name = self._map_burr_hole_number_to_probe(burr_hole_num) burr_hole_info = self.burr_hole_info( burr_hole_num=burr_hole_num ) @@ -3769,16 +3744,9 @@ def get_procedure(self) -> List[Surgery]: inj_type=burr_hole_info.inj_type, ) bregma_to_lambda_distance = self.aind_breg2_lamb - # Need to figure out core_diameter, numerical_aperture, - # total_length, and targeted_structure - fiber_probe = FiberProbe.model_construct( - name=probe_name, - core_diameter=None, - numerical_aperture=None, - total_length=None, - ) + fiber_probe = self._map_burr_fiber_probe(burr_hole_info) ophys_probe = OphysProbe.model_construct( - fiber_probe=fiber_probe, + ophys_probe=fiber_probe, targeted_structure=None, stereotactic_coordinate_ml=burr_hole_info.coordinate_ml, stereotactic_coordinate_ap=burr_hole_info.coordinate_ap, @@ -3815,6 +3783,7 @@ def get_procedure(self) -> List[Surgery]: surgeries = [] if initial_procedures: + self.assign_fiber_probe_names(initial_procedures) initial_surgery = Surgery.model_construct( start_date=initial_start_date, experimenter_full_name=experimenter_full_name, @@ -3828,6 +3797,7 @@ def get_procedure(self) -> List[Surgery]: ) surgeries.append(initial_surgery) if followup_procedures: + self.assign_fiber_probe_names(followup_procedures) followup_surgery = Surgery.model_construct( start_date=followup_start_date, experimenter_full_name=experimenter_full_name, @@ -3843,6 +3813,7 @@ def get_procedure(self) -> List[Surgery]: # any other mapped procedures without During info will be put into one surgery object if other_procedures: + self.assign_fiber_probe_names(other_procedures) other_surgery = Surgery.model_construct( start_date=None, experimenter_full_name=experimenter_full_name, diff --git a/src/aind_metadata_service/slims/client.py b/src/aind_metadata_service/slims/client.py index 6b19f0ad..7e04d6eb 100644 --- a/src/aind_metadata_service/slims/client.py +++ b/src/aind_metadata_service/slims/client.py @@ -7,6 +7,7 @@ from aind_slims_api import SlimsClient from aind_slims_api.exceptions import SlimsRecordNotFound from aind_slims_api.models.instrument import SlimsInstrumentRdrc +from aind_slims_api.operations.ecephys_session import fetch_ecephys_sessions from pydantic import Extra, Field, SecretStr from pydantic_settings import BaseSettings from requests.models import Response @@ -14,6 +15,7 @@ from aind_metadata_service.client import StatusCodes from aind_metadata_service.response_handler import ModelResponse +from aind_metadata_service.slims.mapping import SlimsSessionMapper class SlimsSettings(BaseSettings): @@ -114,3 +116,26 @@ def get_rig_model_response(self, input_id) -> ModelResponse: except Exception as e: logging.error(repr(e)) return ModelResponse.internal_server_error_response() + + def get_sessions_model_response(self, subject_id: str) -> ModelResponse: + """ + Fetches sessions for a given subject ID from SLIMS. + """ + try: + sessions = fetch_ecephys_sessions( + subject_id=subject_id, client=self.client + ) + if sessions: + mapper = SlimsSessionMapper() + mapped_sessions = mapper.map_sessions(sessions, subject_id) + return ModelResponse( + aind_models=mapped_sessions, + status_code=StatusCodes.DB_RESPONDED, + ) + else: + return ModelResponse.no_data_found_error_response() + except SlimsRecordNotFound: + return ModelResponse.no_data_found_error_response() + except Exception as e: + logging.error(repr(e)) + return ModelResponse.internal_server_error_response() diff --git a/src/aind_metadata_service/slims/mapping.py b/src/aind_metadata_service/slims/mapping.py new file mode 100644 index 00000000..53ae6a67 --- /dev/null +++ b/src/aind_metadata_service/slims/mapping.py @@ -0,0 +1,429 @@ +""" +Module to map data from SLIMS to the Session model. +""" + +from enum import Enum +from typing import List, Optional, Tuple + +from aind_data_schema.components.coordinates import CcfCoords, Coordinates3d +from aind_data_schema.components.devices import SpoutSide +from aind_data_schema.core.procedures import CoordinateReferenceLocation +from aind_data_schema.core.session import ( + DomeModule, + LaserConfig, + LightEmittingDiodeConfig, + ManipulatorModule, + RewardDeliveryConfig, + RewardSolution, + RewardSpoutConfig, + Session, + SpeakerConfig, + StimulusEpoch, + StimulusModality, + Stream, +) +from aind_data_schema_models.modalities import Modality +from aind_slims_api.models.ecephys_session import ( + SlimsBrainStructureRdrc, + SlimsRewardDeliveryRdrc, + SlimsRewardSpoutsRdrc, + SlimsStimulusEpochsResult, +) +from aind_slims_api.operations.ecephys_session import ( + EcephysSession as SlimsEcephysSession, +) +from aind_slims_api.operations.ecephys_session import ( + SlimsRewardDeliveryInfo, + SlimsStream, + SlimsStreamModule, +) + + +class SlimsStreamModalities(Enum): + """Enum class for stream modalities in SLIMS.""" + + ECEPHYS = "Ecephys" + BEHAVIOR = "Behavior" + BEHAVIOR_VIDEOS = "Behavior videos" + CONFOCAL = "Confocal" + ELECTROMYOGRAPHY = "Electromyography" + FMOST = "Fmost" + ICEPHYS = "Icephys" + FIB = "Fib" + ISI = "Isi" + MERFISH = "Merfish" + MRI = "Mri" + POPHYS = "POphys" + SLAP = "Slap" + SPIM = "Spim" + + +class SlimsRewardSolution(str, Enum): + """Enum class for reward solution in SLIMS.""" + + WATER = "Water" + OTHER = "Other, (if Other, specify below)" + + +class SlimsSessionMapper: + """Client for interacting with SLIMS and mapping session data.""" + + def map_sessions( + self, sessions: List[SlimsEcephysSession], subject_id: str + ) -> List[Session]: + """Maps SLIMS sessions to AIND session models.""" + return [ + self._map_session(session, subject_id=subject_id) + for session in sessions + ] + + def _map_session( + self, session: SlimsEcephysSession, subject_id: str + ) -> Session: + """Map a single SLIMS session to the AIND session model.""" + session_type = getattr(session.session_group, "session_type", None) + mouse_platform_name = getattr( + session.session_group, "mouse_platform_name", None + ) + active_mouse_platform = getattr( + session.session_group, "active_mouse_platform", False + ) + + rig_id = getattr(session.session_instrument, "name", None) + + session_result = ( + session.session_result + if hasattr(session, "session_result") + else None + ) + + animal_weight_prior = getattr(session_result, "weight_prior_g", None) + animal_weight_post = getattr(session_result, "weight_post_g", None) + reward_consumed_total = getattr( + session_result, "reward_consumed_vol", None + ) + + streams = [ + self._map_stream(stream) + for stream in getattr(session, "streams", []) + ] + stimulus_epochs = [ + self._map_stimulus_epoch(epoch) + for epoch in getattr(session, "stimulus_epochs", []) + ] + reward_delivery_info = ( + self._map_reward_delivery(getattr(session, "reward_delivery")) + if getattr(session, "reward_delivery") + else None + ) + + # model_construct because start and end times are not stored in SLIMS + return Session.model_construct( + rig_id=rig_id, + subject_id=subject_id, + session_type=session_type, + mouse_platform_name=mouse_platform_name, + active_mouse_platform=active_mouse_platform, + animal_weight_prior=animal_weight_prior, + animal_weight_post=animal_weight_post, + data_streams=streams, + stimulus_epochs=stimulus_epochs, + reward_delivery=reward_delivery_info, + reward_consumed_total=reward_consumed_total, + ) + + def _map_reward_delivery( + self, reward_info: SlimsRewardDeliveryInfo + ) -> RewardDeliveryConfig: + """Map reward info from SLIMS to RewardDeliveryConfig model.""" + + slims_reward_delivery = getattr(reward_info, "reward_delivery", None) + slims_reward_spouts = getattr(reward_info, "reward_spouts", None) + + reward_solution, notes = ( + self._map_reward_solution(slims_reward_delivery) + if slims_reward_delivery + else (None, None) + ) + + reward_spouts = ( + [self._map_reward_spouts(slims_reward_spouts)] + if slims_reward_spouts + else [] + ) + + return RewardDeliveryConfig( + reward_solution=reward_solution, + reward_spouts=reward_spouts, + notes=notes, + ) + + @staticmethod + def _map_reward_solution( + reward_delivery: SlimsRewardDeliveryRdrc, + ) -> tuple[Optional[RewardSolution], Optional[str]]: + """Map reward solution and notes.""" + + slims_reward_solution = getattr( + reward_delivery, "reward_solution", None + ) + + if slims_reward_solution == SlimsRewardSolution.WATER: + return RewardSolution.WATER, None + if slims_reward_solution == SlimsRewardSolution.OTHER: + notes = getattr(reward_delivery, "other_reward_solution", None) + return RewardSolution.OTHER, notes + + return None, None + + def _map_reward_spouts( + self, reward_spout: SlimsRewardSpoutsRdrc + ) -> RewardSpoutConfig: + """Map reward spout info to RewardSpoutConfig model""" + + spout_side = getattr(reward_spout, "spout_side", None) + return RewardSpoutConfig.model_construct( + side=self._map_spout_side(spout_side) if spout_side else None, + starting_position=getattr(reward_spout, "starting_position", None), + variable_position=getattr(reward_spout, "variable_position", None), + ) + + @staticmethod + def _map_spout_side(spout_side: str) -> SpoutSide: + """Maps SLIMS input spout side to SpoutSide""" + + spout_side_lower = spout_side.lower() + if "left" in spout_side_lower: + return SpoutSide.LEFT + if "right" in spout_side_lower: + return SpoutSide.RIGHT + if "center" in spout_side_lower: + return SpoutSide.CENTER + + return SpoutSide.OTHER + + def _map_stimulus_epoch( + self, stimulus_epoch: SlimsStimulusEpochsResult + ) -> StimulusEpoch: + """Maps stimulus epoch data from SLIMS to StimulusEpoch model""" + stimulus_name = getattr(stimulus_epoch, "stimulus_name", None) + stimulus_device_names = getattr( + stimulus_epoch, "stimulus_device_names", None + ) + stimulus_modalities = [ + StimulusModality(modality) + for modality in getattr(stimulus_epoch, "stimulus_modalities", []) + ] + reward_consumed_during_epoch = getattr( + stimulus_epoch, "reward_consumed_during_epoch", None + ) + speaker_config = self._map_speaker_config( + speaker_name=getattr(stimulus_epoch, "speaker_name"), + speaker_volume=getattr(stimulus_epoch, "speaker_volume"), + ) + light_source_config = self._map_light_source_config(stimulus_epoch) + # Using model construct because missing start and end times + return StimulusEpoch.model_construct( + stimulus_name=stimulus_name, + stimulus_device_names=stimulus_device_names, + stimulus_modalities=stimulus_modalities, + reward_consumed_during_epoch=reward_consumed_during_epoch, + speaker_config=speaker_config, + light_source_config=light_source_config, + ) + + @staticmethod + def _map_light_source_config( + stimulus_epoch, + ) -> List[LaserConfig | LightEmittingDiodeConfig]: + """Maps light source data from SLIMS to list of configs""" + light_sources = [] + if getattr(stimulus_epoch, "laser_name", None) and getattr( + stimulus_epoch, "laser_wavelength", None + ): + laser = LaserConfig( + name=getattr(stimulus_epoch, "laser_name", None), + wavelength=getattr(stimulus_epoch, "laser_wavelength", None), + excitation_power=getattr( + stimulus_epoch, "laser_excitation_power", None + ), + ) + light_sources.append(laser) + if getattr(stimulus_epoch, "led_name", None): + led = LightEmittingDiodeConfig( + name=getattr(stimulus_epoch, "led_name"), + excitation_power=getattr( + stimulus_epoch, "led_excitation_power_mw", None + ), + ) + light_sources.append(led) + return light_sources + + @staticmethod + def _map_speaker_config( + speaker_name: Optional[str], speaker_volume: Optional[float] + ) -> SpeakerConfig: + """Maps speaker config""" + return ( + SpeakerConfig(name=speaker_name, volume=speaker_volume) + if speaker_name + else None + ) + + def _map_stream(self, stream: SlimsStream) -> Stream: + """Map stream data from SLIMS to the Stream model.""" + stream_modalities = [ + self._map_stream_modality(modality) + for modality in getattr(stream, "stream_modalities", []) + ] + daq_names = getattr(stream, "daq_names", []) + camera_names = getattr(stream, "camera_names", []) + + stick_microscopes, ephys_modules = self._map_stream_modules( + stream.stream_modules + ) + + return Stream.model_construct( + daq_names=daq_names, + camera_names=camera_names, + stream_modalities=stream_modalities, + stick_microscopes=stick_microscopes, + ephys_modules=ephys_modules, + ) + + def _map_stream_modules( + self, stream_modules: Optional[List[SlimsStreamModule]] + ) -> Tuple[List[DomeModule], List[ManipulatorModule]]: + """ + Map stream modules to either stick microscopes or manipulators. + Parameters + ---------- + stream_modules: List of stream modules from SLIMS + Returns + ------- + Tuple containing lists of stick microscopes and ephys modules + """ + stick_microscopes, ephys_modules = [], [] + + for stream_module in stream_modules: + if self._is_manipulator_module(stream_module): + ephys_modules.append( + self._map_manipulator_module(stream_module) + ) + else: + stick_microscopes.append(self._map_dome_module(stream_module)) + + return stick_microscopes, ephys_modules + + @staticmethod + def _is_manipulator_module(stream_module: SlimsStreamModule) -> bool: + """ + Checks if stream module contains fields for a manipulator module. + """ + return ( + getattr(stream_module, "primary_targeted_structure", None) + or getattr(stream_module, "ccf_coordinate_ap", None) + or getattr(stream_module, "manipulator_x", None) + or getattr(stream_module, "bregma_target_ap", None) + ) + + def _map_manipulator_module( + self, stream_module: SlimsStreamModule + ) -> ManipulatorModule: + """ + Map a stream module to a ManipulatorModule instance. + """ + primary_targeted_structure = self._map_targeted_structure( + getattr(stream_module, "primary_targeted_structure", None) + ) + other_targeted_structures = [ + self._map_targeted_structure(structure_name) + for structure_name in getattr( + stream_module, "secondary_targeted_structures", [] + ) + ] + return ManipulatorModule.model_construct( + assembly_name=getattr(stream_module, "assembly_name", None), + arc_angle=getattr(stream_module, "arc_angle", None), + module_angle=getattr(stream_module, "module_angle", None), + rotation_angle=getattr(stream_module, "rotation_angle", None), + coordinate_transform=getattr( + stream_module, "coordinate_transform", None + ), + primary_targeted_structure=primary_targeted_structure, + other_targeted_structures=other_targeted_structures, + targetted_ccf_coordinates=self._map_ccf_coords( + ap=getattr(stream_module, "ccf_coordinate_ap", None), + ml=getattr(stream_module, "ccf_coordinate_ml", None), + dv=getattr(stream_module, "ccf_coordinate_dv", None), + ), + manipulator_coordinates=self._map_3d_coords( + x=getattr(stream_module, "manipulator_x", None), + y=getattr(stream_module, "manipulator_y", None), + z=getattr(stream_module, "manipulator_z", None), + ), + # TODO: map anatomical coordinates once unit is defined + # anatomical_coordinates=self._map_3d_coords( + # x=getattr(stream_module, "bregma_target_ap", None), + # y=getattr(stream_module, "bregma_target_ml", None), + # z=getattr(stream_module, "bregma_target_dv", None), + # ), + anatomical_reference=( + CoordinateReferenceLocation.BREGMA + if getattr(stream_module, "bregma_target_ap", None) + else None + ), + surface_z=getattr(stream_module, "surface_z", None), + dye=getattr(stream_module, "dye", None), + implant_hole_number=getattr(stream_module, "implant_hole", None), + notes="Anatomical Coordinates mapped AP:X, ML:Y, DV:Z", + ) + + @staticmethod + def _map_dome_module(stream_module: SlimsStreamModule) -> DomeModule: + """ + Map a stream module to a DomeModule instance. + """ + return DomeModule.model_construct( + assembly_name=getattr(stream_module, "assembly_name", None), + arc_angle=getattr(stream_module, "arc_angle", None), + module_angle=getattr(stream_module, "module_angle", None), + rotation_angle=getattr(stream_module, "rotation_angle", None), + coordinate_transform=getattr( + stream_module, "coordinate_transform", None + ), + ) + + @staticmethod + def _map_targeted_structure(structure_record: SlimsBrainStructureRdrc): + """Map targeted structure""" + return getattr(structure_record, "name", None) + + @staticmethod + def _map_stream_modality(modality: str) -> Optional[Modality]: + """Map stream modality to the Modality enum.""" + modality_mapping = { + SlimsStreamModalities.ELECTROMYOGRAPHY.value: Modality.EMG, + SlimsStreamModalities.SPIM.value: Modality.SPIM, + SlimsStreamModalities.MRI.value: Modality.MRI, + SlimsStreamModalities.ISI.value: Modality.ISI, + SlimsStreamModalities.FMOST.value: Modality.FMOST, + } + return modality_mapping.get( + modality, + Modality.from_abbreviation(modality.lower().replace(" ", "-")), + ) + + @staticmethod + def _map_ccf_coords( + ml: Optional[float], ap: Optional[float], dv: Optional[float] + ) -> Optional[CcfCoords]: + """Map coordinates to CcfCoords.""" + return CcfCoords(ml=ml, ap=ap, dv=dv) if ml and ap and dv else None + + @staticmethod + def _map_3d_coords( + x: Optional[float], y: Optional[float], z: Optional[float] + ) -> Optional[Coordinates3d]: + """Map coordinates to 3D space.""" + return Coordinates3d(x=x, y=y, z=z) if x and y and z else None diff --git a/src/aind_metadata_service/tars/mapping.py b/src/aind_metadata_service/tars/mapping.py index 56c2f2b3..9fd93f19 100644 --- a/src/aind_metadata_service/tars/mapping.py +++ b/src/aind_metadata_service/tars/mapping.py @@ -223,7 +223,7 @@ def get_virus_strains(response: ModelResponse) -> List: and procedure.injection_materials ): virus_strains = [ - getattr(material, "name") + getattr(material, "name").strip() for material in procedure.injection_materials ] viruses.extend(virus_strains) @@ -253,7 +253,7 @@ def integrate_injection_materials( if isinstance( injection_material, ViralMaterial ) and hasattr(injection_material, "name"): - virus_strain = injection_material.name + virus_strain = injection_material.name.strip() tars_response = tars_mapping.get(virus_strain) if ( tars_response.status_code diff --git a/tests/resources/sharepoint/las2020/mapped/mapped_list_item4.json b/tests/resources/sharepoint/las2020/mapped/mapped_list_item4.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/tests/resources/sharepoint/las2020/mapped/mapped_list_item4.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/tests/resources/sharepoint/las2020/raw/list_item4.json b/tests/resources/sharepoint/las2020/raw/list_item4.json new file mode 100644 index 00000000..e242dadd --- /dev/null +++ b/tests/resources/sharepoint/las2020/raw/list_item4.json @@ -0,0 +1,250 @@ +{ + "FileSystemObjectType": 0, + "Id": 6709, + "ServerRedirectedEmbedUri": null, + "ServerRedirectedEmbedUrl": "", + "ContentTypeId": "0x01005C6009AF417C4948814EADD3589ACE13", + "Title": "000000 000001", + "ComplianceAssetId": null, + "AuthorId": 5358, + "RequestStatus2": "New", + "Protocol": "2212 - Investigating Brain States", + "ProjectID": "AAV production 102-88-004-10", + "AccommodationComment": "This animal will be used for an acute ephys experiment on Friday morning.", + "ACSFID1": null, + "ACSFID2": null, + "ACSFID3": null, + "ACSFID4": null, + "ACSFID5": null, + "ACSFTime1": null, + "ACSFTime2": null, + "ACSFTime3": null, + "ACSFTime4": null, + "ACSFTime5": null, + "custcontact": null, + "CustomCom1": "Recover headpost, if possible.", + "CustomCom2": null, + "custpresent": false, + "doseduration": "30", + "doseRoute": "Intraperitoneal (IP)", + "doseSub": null, + "dosevolume": "70.4", + "doseWhere": null, + "Doxycycline": false, + "LTaID1": null, + "LTaID2": null, + "LTaID3": null, + "LTaID4": null, + "LTaID5": null, + "LTdate1": null, + "LTdate2": null, + "LTdate3": null, + "LTdate4": null, + "LTdate5": null, + "LTTask1": null, + "LTTask2": null, + "LTTask3": null, + "LTTask4": null, + "LTTask5": null, + "nEnd_x0020_Date": null, + "nROID1": "000000", + "nROID2": "000000", + "nROID3": "000001", + "nROID4": "000001", + "nROID5": "000001", + "nStart_x0020_Date": "2024-06-21T07:00:00Z", + "OCT": false, + "PostFix": "1xPBS+ 0.01%Azide", + "ReqAge1": null, + "ReqAge2": null, + "ReqAge3": null, + "Reqdate1": "2024-06-21T07:00:00Z", + "Reqdate2": null, + "Reqdate3": null, + "ReqPro1": "Tissue Collection", + "ReqPro2": null, + "ReqPro3": null, + "roEye1": "Behind Left", + "roEye2": "Behind Right", + "roEye3": "Behind Either", + "roEye4": null, + "roEye5": null, + "roSpinDown": "Yes", + "roSub1": "AiP12017", + "roSub2": "AiP1586", + "roSub3": "AiP12017", + "roSub4": "AiP12017", + "roSub5": "AiP1586", + "roVol1": "90", + "roVol2": "90 uL", + "roVol3": "89.9", + "roVol4": "90", + "roVol5": "90", + "roWhere": null, + "SpecDeliLoc": "Tissuecyte", + "TAM": false, + "TAMAge": null, + "TAMFreq": null, + "TCPrep": { + "0": "Mouse Cardiac Perfusion (SOP AF0075)" + }, + "TCTiss": { + "0": "Brain" + }, + "TMP": false, + "TMPAge": null, + "TMPFreq": null, + "LASComments": null, + "LIMSEntry": true, + "LIMSProject": "TempletonPsychedelics", + "LIMSWorkflow": "2p serial Imaging", + "roComment": null, + "ProjectLead2Id": 3212, + "ProjectLead2StringId": "3212", + "WellnessReport_x0028_No_x002F_Ye": false, + "QCDoorSheet": false, + "roIce": false, + "roSOP": null, + "roDiscard": true, + "roLIMS": null, + "icvID1": null, + "icvID2": null, + "icvID3": null, + "icvID4": null, + "icvID5": null, + "icvRoute1": null, + "icvRoute2": null, + "icvRoute3": null, + "icvRoute4": null, + "icvRoute5": null, + "icvSub1": null, + "icvSub2": null, + "icvSub3": null, + "icvSub4": null, + "icvSub5": null, + "WheretoObtainsubstance_x0028_ICV": null, + "Species": "Mouse", + "icvComment": null, + "Modified": "2024-05-20T18:28:26Z", + "icvLIMS": null, + "Created": "2024-05-13T17:59:22Z", + "LIMSWorkflow_x0020_2": null, + "OData__ColorTag": null, + "AfternoonPF": false, + "roSub1b": "AiP12017", + "roSub1c": "AiP12017", + "roSub1d": "AiP12017", + "roSub2b": null, + "roSub2c": null, + "roSub2d": null, + "roSub3b": null, + "roSub3c": null, + "roSub3d": null, + "roSub4b": null, + "roSub4c": null, + "roSub4d": null, + "roSub5b": null, + "roSub5c": null, + "roSub5d": null, + "roVolV1": null, + "roVolV1b": null, + "roVolV1c": null, + "roVolV1d": null, + "roVolV2": null, + "roVolV2b": null, + "roVolV2c": null, + "roVolV2d": null, + "roVolV3": null, + "roVolV3b": null, + "roVolV3c": null, + "roVolV3d": null, + "roVolV4": null, + "roVolV4b": null, + "roVolV4c": null, + "roVolV4d": null, + "roVolV5": null, + "roVolV5b": null, + "roVolV5c": null, + "roVolV5d": null, + "roTite1": "50", + "roTite1b": "50 gc/mL", + "roTite1c": null, + "roTite1d": null, + "roTite2": null, + "roTite2b": null, + "roTite2c": null, + "roTite2d": null, + "roTite3": null, + "roTite3b": null, + "roTite3c": null, + "roTite3d": null, + "roTite4": null, + "roTite4b": null, + "roTite4c": null, + "roTite4d": null, + "roTite5": null, + "roTite5b": null, + "roTite5c": null, + "roTite5d": null, + "roGC1": null, + "roGC1b": null, + "roGC1c": null, + "roGC1d": null, + "roGC2": null, + "roGC2b": null, + "roGC2c": null, + "roGC2d": null, + "roGC3": null, + "roGC3b": null, + "roGC3c": null, + "roGC3d": null, + "roGC4": null, + "roGC4b": null, + "roGC4c": null, + "roGC4d": null, + "roGC5": null, + "roGC5b": null, + "roGC5c": null, + "roGC5d": null, + "roLot1": "GT340C", + "roLot1b": null, + "roLot1c": null, + "roLot1d": null, + "roLot2": null, + "roLot2b": null, + "roLot2c": null, + "roLot2d": null, + "roLot3": null, + "roLot3b": null, + "roLot3c": null, + "roLot3d": null, + "roLot4": null, + "roLot4b": null, + "roLot4c": null, + "roLot4d": null, + "roLot5": null, + "roLot5b": null, + "roLot5c": null, + "roLot5d": null, + "roBox1": null, + "roBox2": null, + "roBox3": null, + "roBox4": null, + "roBox5": null, + "roTube1": "1", + "roTube2": "tube", + "roTube3": null, + "roTube4": null, + "roTube5": null, + "BCAge": null, + "BCGenotypes": null, + "BCType": null, + "BCVolume": null, + "BCTube": null, + "BCLocation": null, + "ID": 6709, + "EditorId": 538, + "OData__UIVersionString": "1.0", + "Attachments": false, + "GUID": "cd8a888b-4462-47e8-ba04-61e35515c697" +} \ No newline at end of file diff --git a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item14.json b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item14.json index cc5476cc..5484f70b 100644 --- a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item14.json +++ b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item14.json @@ -1,221 +1,313 @@ [ { + "procedure_type": "Surgery", "start_date": "2022-01-03", "experimenter_full_name": "NSB-187", "iacuc_protocol": "2103", "animal_weight_prior": "25.2", "animal_weight_post": "28.2", + "weight_unit": "gram", "anaesthesia": { "type": "isoflurane", "duration": "90.0", - "level": "2.0", - "duration_unit": "minute" + "duration_unit": "minute", + "level": "2.0" }, "workstation_id": "SWS 4", "procedures": [ { + "procedure_type": "Headframe", "headframe_type": "Frontal Ctx", "headframe_part_number": "0160-100-46", + "headframe_material": null, "well_part_number": "0160-055-08", - "well_type": "WHC NP", - "procedure_type": "Headframe", - "headframe_material": null + "well_type": "WHC NP" }, { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-5.2", "injection_coordinate_ap": "-0.85", "injection_coordinate_depth": [ "-3.1" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, + "bregma_to_lambda_unit": "millimeter", "injection_angle": "0.0", + "injection_angle_unit": "degrees", + "targeted_structure": null, "injection_hemisphere": "Left", + "procedure_type": "Nanoject injection", "injection_volume": [ "600.0" ], - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", - "bregma_to_lambda_unit": "millimeter", - "injection_angle_unit": "degrees", - "targeted_structure": null, - "procedure_type": "Nanoject injection", "injection_volume_unit": "nanoliter" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "numerical_aperture": 0.37, + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-0.85", "stereotactic_coordinate_ml": "-5.2", "stereotactic_coordinate_dv": "-2.95", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] }, { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-0.5", "injection_coordinate_ap": "2.0", "injection_coordinate_depth": [ "5.0" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Left", - "injection_current": null, - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Left", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": null, + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": "2.5", + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "2.0", "stereotactic_coordinate_ml": "-0.5", "stereotactic_coordinate_dv": "-1.05", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] } ], - "notes": null, - "procedure_type": "Surgery", - "weight_unit": "gram" + "notes": null }, { + "procedure_type": "Surgery", "start_date": "2022-01-03", "experimenter_full_name": "NSB-187", "iacuc_protocol": "2103", "animal_weight_prior": "25.2", "animal_weight_post": "28.2", + "weight_unit": "gram", "anaesthesia": { "type": "isoflurane", "duration": "90.0", - "level": "2.0", - "duration_unit": "minute" + "duration_unit": "minute", + "level": "2.0" }, "workstation_id": "SWS 4", "procedures": [ { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-2.2", "injection_coordinate_ap": "-6.1", "injection_coordinate_depth": [ "3.1" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Left", - "injection_current": "5", - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Left", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": "5", + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-6.1", "stereotactic_coordinate_ml": "-2.2", "stereotactic_coordinate_dv": "-1.85", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] }, { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-2.5", "injection_coordinate_ap": "1.0", "injection_coordinate_depth": [ "3.0" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Right", - "injection_current": "5", - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Right", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": "5", + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "1.0", "stereotactic_coordinate_ml": "-2.5", "stereotactic_coordinate_dv": "-1.8", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] } ], - "notes": null, - "procedure_type": "Surgery", - "weight_unit": "gram" + "notes": null } ] \ No newline at end of file diff --git a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item15.json b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item15.json index 534e1c98..bf2dbd24 100644 --- a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item15.json +++ b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item15.json @@ -60,7 +60,30 @@ "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", "angle_unit": "degrees", - "notes": null + "notes": null, + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_2" + } } ], "procedure_type": "Fiber implant" @@ -103,7 +126,30 @@ "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", "angle_unit": "degrees", - "notes": null + "notes": null, + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": "2.5", + "length_unit": "millimeter", + "name": "Fiber_0" + } } ], "procedure_type": "Fiber implant" @@ -146,7 +192,30 @@ "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", "angle_unit": "degrees", - "notes": null + "notes": null, + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_3" + } } ], "procedure_type": "Fiber implant" @@ -189,7 +258,30 @@ "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", "angle_unit": "degrees", - "notes": null + "notes": null, + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + } } ], "procedure_type": "Fiber implant" diff --git a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item16.json b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item16.json index 05174976..c124692d 100644 --- a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item16.json +++ b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item16.json @@ -49,6 +49,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": "2.5", + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "2.0", "stereotactic_coordinate_ml": "-0.5", @@ -111,6 +134,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-6.1", "stereotactic_coordinate_ml": "-2.2", @@ -169,6 +215,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-0.85", "stereotactic_coordinate_ml": "-5.2", @@ -212,6 +281,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "1.0", "stereotactic_coordinate_ml": "-2.5", diff --git a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item17.json b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item17.json index 55158037..e6351a1b 100644 --- a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item17.json +++ b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item17.json @@ -49,6 +49,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": "2.5", + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "2.0", "stereotactic_coordinate_ml": "-0.5", @@ -111,6 +134,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-6.1", "stereotactic_coordinate_ml": "-2.2", @@ -169,6 +215,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-0.85", "stereotactic_coordinate_ml": "-5.2", @@ -212,6 +281,29 @@ { "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "1.0", "stereotactic_coordinate_ml": "-2.5", diff --git a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item19.json b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item19.json index 6737858e..20869872 100644 --- a/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item19.json +++ b/tests/resources/sharepoint/nsb2023/mapped/mapped_list_item19.json @@ -1,73 +1,93 @@ [ { + "procedure_type": "Surgery", "start_date": "2022-01-03", "experimenter_full_name": "NSB-187", "iacuc_protocol": "2103", "animal_weight_prior": "25.2", "animal_weight_post": "28.2", + "weight_unit": "gram", "anaesthesia": { "type": "isoflurane", "duration": "90.0", - "level": "2.0", - "duration_unit": "minute" + "duration_unit": "minute", + "level": "2.0" }, "workstation_id": "SWS 4", "procedures": [ { + "procedure_type": "Headframe", "headframe_type": "WHC 2P", "headframe_part_number": "0160-100-45", + "headframe_material": null, "well_part_number": "0160-200-62", - "well_type": "WHC 2P", - "procedure_type": "Headframe", - "headframe_material": null + "well_type": "WHC 2P" }, { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-0.5", "injection_coordinate_ap": "2.0", "injection_coordinate_depth": [ "5.0" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Left", - "injection_current": null, - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Left", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": null, + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": null, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter_unit": "micrometer", + "ferrule_material": null, + "active_length": null, + "total_length": "2.5", + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "2.0", "stereotactic_coordinate_ml": "-0.5", "stereotactic_coordinate_dv": "-1.05", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] }, { "injection_materials": [], "recovery_time": "25.0", + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-0.3", "injection_coordinate_ap": "1.2", @@ -75,188 +95,250 @@ "1.4", "1.2" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": null, - "injection_hemisphere": null, - "injection_current": null, - "alternating_current": null, - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": null, "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": null, "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": null, + "injection_current_unit": "microamps", + "alternating_current": null } ], - "notes": null, - "procedure_type": "Surgery", - "weight_unit": "gram" + "notes": null }, { + "procedure_type": "Surgery", "start_date": null, "experimenter_full_name": "NSB-187", "iacuc_protocol": "2103", "animal_weight_prior": null, "animal_weight_post": null, + "weight_unit": "gram", "anaesthesia": { "type": "isoflurane", "duration": null, - "level": null, - "duration_unit": "minute" + "duration_unit": "minute", + "level": null }, "workstation_id": null, "procedures": [ { "injection_materials": [], "recovery_time": null, + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-2.2", "injection_coordinate_ap": "-6.1", "injection_coordinate_depth": [ "3.1" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Left", - "injection_current": "5", - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Left", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": "5", + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-6.1", "stereotactic_coordinate_ml": "-2.2", "stereotactic_coordinate_dv": "-1.85", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] } ], - "notes": null, - "procedure_type": "Surgery", - "weight_unit": "gram" + "notes": null }, { + "procedure_type": "Surgery", "start_date": null, "experimenter_full_name": "NSB-187", "iacuc_protocol": "2103", "animal_weight_prior": null, "animal_weight_post": null, + "weight_unit": "gram", "anaesthesia": null, "workstation_id": null, "procedures": [ { "injection_materials": [], "recovery_time": null, + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-5.2", "injection_coordinate_ap": "-0.85", "injection_coordinate_depth": [ "-3.1" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, + "bregma_to_lambda_unit": "millimeter", "injection_angle": "0.0", + "injection_angle_unit": "degrees", + "targeted_structure": null, "injection_hemisphere": "Left", + "procedure_type": "Nanoject injection", "injection_volume": [ "600.0" ], - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", - "bregma_to_lambda_unit": "millimeter", - "injection_angle_unit": "degrees", - "targeted_structure": null, - "procedure_type": "Nanoject injection", "injection_volume_unit": "nanoliter" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": null, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": "Some custom specs.", + "core_diameter_unit": "micrometer", + "ferrule_material": null, + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_1" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "-0.85", "stereotactic_coordinate_ml": "-5.2", "stereotactic_coordinate_dv": "-2.95", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] }, { "injection_materials": [], "recovery_time": null, + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "-2.5", "injection_coordinate_ap": "1.0", "injection_coordinate_depth": [ "3.0" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": "0.0", - "injection_hemisphere": "Right", - "injection_current": "5", - "alternating_current": "7/7", - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": "0.0", "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Right", "procedure_type": "Iontophoresis injection", - "injection_current_unit": "microamps" + "injection_current": "5", + "injection_current_unit": "microamps", + "alternating_current": "7/7" }, { + "procedure_type": "Fiber implant", "probes": [ { + "ophys_probe": { + "device_type": "Fiber optic probe", + "serial_number": null, + "manufacturer": { + "name": "Neurophotometrics", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "path_to_cad": null, + "port_index": null, + "additional_settings": {}, + "notes": null, + "core_diameter": 200, + "numerical_aperture": 0.37, + "core_diameter_unit": "micrometer", + "ferrule_material": "Ceramic", + "active_length": null, + "total_length": null, + "length_unit": "millimeter", + "name": "Fiber_0" + }, "targeted_structure": null, "stereotactic_coordinate_ap": "1.0", "stereotactic_coordinate_ml": "-2.5", "stereotactic_coordinate_dv": "-1.8", + "stereotactic_coordinate_unit": "millimeter", "stereotactic_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "angle": "0.0", - "stereotactic_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "angle": "0.0", "angle_unit": "degrees", "notes": null } - ], - "procedure_type": "Fiber implant" + ] }, { "injection_materials": [], "recovery_time": null, + "recovery_time_unit": "minute", "injection_duration": null, + "injection_duration_unit": "minute", "instrument_id": null, "injection_coordinate_ml": "2.45", "injection_coordinate_ap": "1.0", @@ -265,23 +347,19 @@ "1.2", "2.0" ], + "injection_coordinate_unit": "millimeter", "injection_coordinate_reference": "Bregma", "bregma_to_lambda_distance": null, - "injection_angle": null, - "injection_hemisphere": "Left", - "injection_volume": null, - "recovery_time_unit": "minute", - "injection_duration_unit": "minute", - "injection_coordinate_unit": "millimeter", "bregma_to_lambda_unit": "millimeter", + "injection_angle": null, "injection_angle_unit": "degrees", "targeted_structure": null, + "injection_hemisphere": "Left", "procedure_type": "Nanoject injection", + "injection_volume": null, "injection_volume_unit": "nanoliter" } ], - "notes": null, - "procedure_type": "Surgery", - "weight_unit": "gram" + "notes": null } -] \ No newline at end of file +] diff --git a/tests/resources/sharepoint/nsb2023/raw/list_item19.json b/tests/resources/sharepoint/nsb2023/raw/list_item19.json index dea9ed3d..e1ed360c 100644 --- a/tests/resources/sharepoint/nsb2023/raw/list_item19.json +++ b/tests/resources/sharepoint/nsb2023/raw/list_item19.json @@ -39,7 +39,7 @@ "FirstInjRecovery": null, "Breg2Lamb": null, "SurgeryStatus": "Ready for Feedback", - "LongRequestorComments": null, + "LongRequestorComments": "Some custom specs.", "Procedure_x0020_Slots": "Single surgical session", "Procedure_x0020_Family": "Headpost only", "Procedure_x0020_T2": "Select...", @@ -153,11 +153,11 @@ "Burr_x0020_Hole_x0020_1_x0020_st": null, "Burr_x0020_1_x0020_DV_x0020_2": null, "Burr_x0020_1_x0020_D_x002f_V_x00": null, - "Burr_x0020_1_x0020_Fiber_x0020_t": "Standard (provided by NSB)", + "Burr_x0020_1_x0020_Fiber_x0020_t": "Custom", "Burr2_x0020_Status": null, "Burr_x0020_2_x0020_D_x002f_V_x00": null, "Burr_x0020_2_x0020_D_x002f_V_x000": null, - "Burr_x0020_2_x0020_Fiber_x0020_T": "Standard (Provided by NSB)", + "Burr_x0020_2_x0020_Fiber_x0020_T": "Edge case type", "Burr3_x0020_Status": null, "Inj3volperdepth": null, "Burr_x0020_3_x0020_D_x002f_V_x00": null, diff --git a/tests/resources/slims/mapped/ecephys_session.json b/tests/resources/slims/mapped/ecephys_session.json new file mode 100644 index 00000000..5975ef8a --- /dev/null +++ b/tests/resources/slims/mapped/ecephys_session.json @@ -0,0 +1,136 @@ +{ + "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/session.py", + "schema_version": "1.0.3", + "protocol_id": [], + "session_end_time": null, + "session_type": null, + "iacuc_protocol": null, + "rig_id": "323_EPHYS1_OPTO_20240212", + "calibrations": [], + "maintenance": [], + "subject_id": "000000", + "animal_weight_prior": 10.0, + "animal_weight_post": 10.0, + "weight_unit": "gram", + "anaesthesia": null, + "data_streams": [ + { + "daq_names": [ + "Harp Behavior", + "NPopto Basestation" + ], + "camera_names": ["Camera1", "Camera2"], + "light_sources": [], + "ephys_modules": [ + { + "assembly_name": "assembly name test", + "arc_angle": 3.0, + "module_angle": 3.0, + "angle_unit": "degrees", + "rotation_angle": 3.0, + "coordinate_transform": null, + "calibration_date": null, + "notes": "Anatomical Coordinates mapped AP:X, ML:Y, DV:Z", + "primary_targeted_structure": "Nucleus accumbens", + "other_targeted_structure": null, + "targeted_ccf_coordinates": [], + "manipulator_coordinates": { + "x": "1.0", + "y": "1.0", + "z": "1.0", + "unit": "micrometer" + }, + "anatomical_coordinates": null, + "anatomical_reference": "Bregma", + "surface_z": null, + "surface_z_unit": "micrometer", + "dye": null, + "implant_hole_number": 9.0 + } + ], + "stick_microscopes": [ + { + "assembly_name": "45881", + "arc_angle": 1.0, + "module_angle": 1.0, + "angle_unit": "degrees", + "rotation_angle": 1.0, + "coordinate_transform": null, + "calibration_date": null, + "notes": null + } + ], + "manipulator_modules": [], + "detectors": [], + "fiber_connections": [], + "fiber_modules": [], + "ophys_fovs": [], + "slap_fovs": [], + "stack_parameters": null, + "mri_scans": [], + "stream_modalities": [ + { + "name": "Behavior", + "abbreviation": "behavior" + }, + { + "name": "Extracellular electrophysiology", + "abbreviation": "ecephys" + } + ], + "software": [], + "notes": null + } + ], + "stimulus_epochs": [ + { + "stimulus_name": "stim1", + "session_number": null, + "software": [], + "script": null, + "stimulus_modalities": [ + "Auditory", + "Olfactory" + ], + "stimulus_parameters": null, + "stimulus_device_names": "device1, device2", + "speaker_config": { + "name": "speaker1", + "volume": "1.0", + "volume_unit": "decibels" + }, + "light_source_config": [ + { + "device_type": "Light emitting diode", + "name": "diode", + "excitation_power": "12.0", + "excitation_power_unit": "milliwatt" + } + ], + "output_parameters": {}, + "reward_consumed_during_epoch": 12.0, + "reward_consumed_unit": "microliter", + "trials_total": null, + "trials_finished": null, + "trials_rewarded": null, + "notes": null + } + ], + "mouse_platform_name": null, + "active_mouse_platform": false, + "headframe_registration": null, + "reward_delivery": { + "reward_solution": "Water", + "reward_spouts": [ + { + "side": "Right", + "starting_position": "xyz", + "variable_position": true + } + ], + "notes": null + }, + "reward_consumed_total": 12.0, + "reward_consumed_unit": "milliliter", + "notes": null +} diff --git a/tests/resources/slims/mapped/ecephys_session2.json b/tests/resources/slims/mapped/ecephys_session2.json new file mode 100644 index 00000000..07d8e9e0 --- /dev/null +++ b/tests/resources/slims/mapped/ecephys_session2.json @@ -0,0 +1,128 @@ +{ + "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/session.py", + "schema_version": "1.0.3", + "protocol_id": [], + "session_end_time": null, + "session_type": null, + "iacuc_protocol": null, + "rig_id": "323_EPHYS1_OPTO_20240212", + "calibrations": [], + "maintenance": [], + "subject_id": "000000", + "animal_weight_prior": 10.0, + "animal_weight_post": 10.0, + "weight_unit": "gram", + "anaesthesia": null, + "data_streams": [ + { + "daq_names": [ + "Harp Behavior", + "NPopto Basestation" + ], + "camera_names": ["Camera1", "Camera2"], + "light_sources": [], + "ephys_modules": [ + { + "assembly_name": "assembly name test", + "arc_angle": 3.0, + "module_angle": 3.0, + "angle_unit": "degrees", + "rotation_angle": 3.0, + "coordinate_transform": null, + "calibration_date": null, + "notes": "Anatomical Coordinates mapped AP:X, ML:Y, DV:Z", + "primary_targeted_structure": "Nucleus accumbens", + "other_targeted_structure": null, + "targeted_ccf_coordinates": [], + "manipulator_coordinates": { + "x": "1.0", + "y": "1.0", + "z": "1.0", + "unit": "micrometer" + }, + "anatomical_coordinates": null, + "anatomical_reference": "Bregma", + "surface_z": null, + "surface_z_unit": "micrometer", + "dye": null, + "implant_hole_number": 9.0 + } + ], + "stick_microscopes": [ + { + "assembly_name": "45881", + "arc_angle": 1.0, + "module_angle": 1.0, + "angle_unit": "degrees", + "rotation_angle": 1.0, + "coordinate_transform": null, + "calibration_date": null, + "notes": null + } + ], + "manipulator_modules": [], + "detectors": [], + "fiber_connections": [], + "fiber_modules": [], + "ophys_fovs": [], + "slap_fovs": [], + "stack_parameters": null, + "mri_scans": [], + "stream_modalities": [ + { + "name": "Behavior", + "abbreviation": "behavior" + }, + { + "name": "Extracellular electrophysiology", + "abbreviation": "ecephys" + } + ], + "software": [], + "notes": null + } + ], + "stimulus_epochs": [ + { + "stimulus_name": "stim1", + "session_number": null, + "software": [], + "script": null, + "stimulus_modalities": [ + "Auditory", + "Olfactory" + ], + "stimulus_parameters": null, + "stimulus_device_names": "device1, device2", + "speaker_config": { + "name": "speaker1", + "volume": "1.0", + "volume_unit": "decibels" + }, + "light_source_config": [ + { + "device_type": "Laser", + "name": "laserA", + "wavelength": 12, + "wavelength_unit": "nanometer", + "excitation_power": "1.0", + "excitation_power_unit": "milliwatt" + } + ], + "output_parameters": {}, + "reward_consumed_during_epoch": 12.0, + "reward_consumed_unit": "microliter", + "trials_total": null, + "trials_finished": null, + "trials_rewarded": null, + "notes": null + } + ], + "mouse_platform_name": null, + "active_mouse_platform": false, + "headframe_registration": null, + "reward_delivery": null, + "reward_consumed_total": 12.0, + "reward_consumed_unit": "milliliter", + "notes": null +} diff --git a/tests/resources/slims/ephys_rig.json b/tests/resources/slims/mapped/ephys_rig.json similarity index 100% rename from tests/resources/slims/ephys_rig.json rename to tests/resources/slims/mapped/ephys_rig.json diff --git a/tests/resources/slims/instrument.json b/tests/resources/slims/mapped/instrument.json similarity index 100% rename from tests/resources/slims/instrument.json rename to tests/resources/slims/mapped/instrument.json diff --git a/tests/resources/slims/attachment_json_entity.json b/tests/resources/slims/raw/attachment_json_entity.json similarity index 100% rename from tests/resources/slims/attachment_json_entity.json rename to tests/resources/slims/raw/attachment_json_entity.json diff --git a/tests/resources/slims/raw/ecephys_session_response.json b/tests/resources/slims/raw/ecephys_session_response.json new file mode 100644 index 00000000..e982fed6 --- /dev/null +++ b/tests/resources/slims/raw/ecephys_session_response.json @@ -0,0 +1,167 @@ +{ + "session_group": { + "pk": 64952, + "created_on": 1729807856372, + "json_entity": {}, + "name": "Group of Sessions", + "experimentrun_pk": 41181 + }, + "session_instrument": { + "pk": 1743, + "created_on": 1711642574300, + "json_entity": {}, + "name": "323_EPHYS1_OPTO_20240212" + }, + "session_result": { + "pk": 2894, + "created_on": 1729807858852, + "json_entity": {}, + "test_label": "Mouse Session", + "mouse_session_id": "RSLT0000002946", + "mouse_session": null, + "weight_prior_g": 10.0, + "weight_post_g": 10.0, + "reward_consumed_vol": 12.0, + "reward_delivery_pk": 3671, + "mouse_pk": 3280, + "mouse_session_pk": null, + "experiment_run_step_pk": 64953 + }, + "streams": [ + { + "pk": 2895, + "created_on": 1729807859729, + "json_entity": {}, + "test_label": "Streams", + "mouse_session": null, + "stream_modalities": [ + "Behavior", + "Ecephys" + ], + "daq_names": [ + "Harp Behavior", + "NPopto Basestation" + ], + "camera_names": [ + "Camera1", + "Camera2" + ], + "stream_modules_pk": [ + 3467, + 3672 + ], + "mouse_pk": 3280, + "mouse_session_pk": 2894, + "experiment_run_step_pk": 64954, + "stream_modules": [ + { + "pk": 3467, + "created_on": 1726521670522, + "json_entity": {}, + "implant_hole": 9.0, + "assembly_name": "assembly name test", + "probe_name": "ephys probe name test", + "primary_targeted_structure_pk": 1897, + "secondary_targeted_structures_pk": null, + "arc_angle": 3.0, + "module_angle": 3.0, + "rotation_angle": 3.0, + "coordinate_transform": null, + "ccf_coordinate_ap": 3.0, + "ccf_coordinate_ml": 3.0, + "ccf_coordinate_dv": 3.0, + "ccf_version": 3.0, + "bregma_target_ap": 3.0, + "bregma_target_ml": 3.0, + "bregma_target_dv": 3.0, + "surface_z": null, + "manipulator_x": 1.0, + "manipulator_y": 1.0, + "manipulator_z": 1.0, + "dye": null, + "fiber_connections_pk": null, + "primary_targeted_structure": { + "pk": 1897, + "created_on": 1714684334335, + "json_entity": {}, + "name": "Nucleus accumbens" + }, + "secondary_targeted_structures": [] + }, + { + "pk": 3672, + "created_on": 1729812275378, + "json_entity": {}, + "implant_hole": null, + "assembly_name": "45881", + "probe_name": null, + "primary_targeted_structure_pk": null, + "secondary_targeted_structures_pk": null, + "arc_angle": 1.0, + "module_angle": 1.0, + "rotation_angle": 1.0, + "coordinate_transform": null, + "ccf_coordinate_ap": null, + "ccf_coordinate_ml": null, + "ccf_coordinate_dv": null, + "ccf_version": null, + "bregma_target_ap": null, + "bregma_target_ml": null, + "bregma_target_dv": null, + "surface_z": null, + "manipulator_x": null, + "manipulator_y": null, + "manipulator_z": null, + "dye": null, + "fiber_connections_pk": null, + "primary_targeted_structure": null, + "secondary_targeted_structures": [] + } + ] + } + ], + "reward_delivery": { + "reward_delivery": { + "pk": 3671, + "created_on": 1729808236573, + "json_entity": {}, + "reward_spouts_pk": 3670, + "reward_solution": "Water", + "other_reward_solution": null + }, + "reward_spouts": { + "pk": 3670, + "created_on": 1729808198683, + "json_entity": {}, + "spout_side": "Right", + "starting_position": "xyz", + "variable_position": true + } + }, + "stimulus_epochs": [ + { + "pk": 2896, + "created_on": 1729807860197, + "json_entity": {}, + "test_label": "Stimulus Epochs", + "mouse_session": null, + "stimulus_device_names": "device1, device2", + "stimulus_name": "stim1", + "stimulus_modalities": [ + "Auditory", + "Olfactory" + ], + "reward_consumed_during_epoch": 12.0, + "led_name": "diode", + "led_excitation_power_mw": 12.0, + "laser_name": "laserA", + "laser_wavelength": null, + "laser_excitation_power": 1.0, + "speaker_name": "speaker1", + "speaker_volume": 1.0, + "mouse_pk": 3280, + "mouse_session_pk": 2894, + "experiment_run_step_pk": 64955 + } + ] +} diff --git a/tests/resources/slims/raw/ecephys_session_response2.json b/tests/resources/slims/raw/ecephys_session_response2.json new file mode 100644 index 00000000..f542da21 --- /dev/null +++ b/tests/resources/slims/raw/ecephys_session_response2.json @@ -0,0 +1,146 @@ +{ + "session_group": { + "pk": 64952, + "created_on": 1729807856372, + "json_entity": {}, + "name": "Group of Sessions", + "experimentrun_pk": 41181 + }, + "session_instrument": { + "pk": 1743, + "created_on": 1711642574300, + "json_entity": {}, + "name": "323_EPHYS1_OPTO_20240212" + }, + "session_result": { + "pk": 2894, + "created_on": 1729807858852, + "json_entity": {}, + "test_label": "Mouse Session", + "mouse_session_id": "RSLT0000002946", + "mouse_session": null, + "weight_prior_g": 10.0, + "weight_post_g": 10.0, + "reward_consumed_vol": 12.0, + "mouse_pk": 3280, + "mouse_session_pk": null, + "experiment_run_step_pk": 64953 + }, + "streams": [ + { + "pk": 2895, + "created_on": 1729807859729, + "json_entity": {}, + "test_label": "Streams", + "mouse_session": null, + "stream_modalities": [ + "Behavior", + "Ecephys" + ], + "daq_names": [ + "Harp Behavior", + "NPopto Basestation" + ], + "camera_names": [ + "Camera1", + "Camera2" + ], + "stream_modules_pk": [ + 3467, + 3672 + ], + "mouse_pk": 3280, + "mouse_session_pk": 2894, + "experiment_run_step_pk": 64954, + "stream_modules": [ + { + "pk": 3467, + "created_on": 1726521670522, + "json_entity": {}, + "implant_hole": 9.0, + "assembly_name": "assembly name test", + "probe_name": "ephys probe name test", + "primary_targeted_structure_pk": 1897, + "secondary_targeted_structures_pk": null, + "arc_angle": 3.0, + "module_angle": 3.0, + "rotation_angle": 3.0, + "coordinate_transform": null, + "ccf_coordinate_ap": 3.0, + "ccf_coordinate_ml": 3.0, + "ccf_coordinate_dv": 3.0, + "ccf_version": 3.0, + "bregma_target_ap": 3.0, + "bregma_target_ml": 3.0, + "bregma_target_dv": 3.0, + "surface_z": null, + "manipulator_x": 1.0, + "manipulator_y": 1.0, + "manipulator_z": 1.0, + "dye": null, + "fiber_connections_pk": null, + "primary_targeted_structure": { + "pk": 1897, + "created_on": 1714684334335, + "json_entity": {}, + "name": "Nucleus accumbens" + }, + "secondary_targeted_structures": [] + }, + { + "pk": 3672, + "created_on": 1729812275378, + "json_entity": {}, + "implant_hole": null, + "assembly_name": "45881", + "probe_name": null, + "primary_targeted_structure_pk": null, + "secondary_targeted_structures_pk": null, + "arc_angle": 1.0, + "module_angle": 1.0, + "rotation_angle": 1.0, + "coordinate_transform": null, + "ccf_coordinate_ap": null, + "ccf_coordinate_ml": null, + "ccf_coordinate_dv": null, + "ccf_version": null, + "bregma_target_ap": null, + "bregma_target_ml": null, + "bregma_target_dv": null, + "surface_z": null, + "manipulator_x": null, + "manipulator_y": null, + "manipulator_z": null, + "dye": null, + "fiber_connections_pk": null, + "primary_targeted_structure": null, + "secondary_targeted_structures": [] + } + ] + } + ], + "stimulus_epochs": [ + { + "pk": 2896, + "created_on": 1729807860197, + "json_entity": {}, + "test_label": "Stimulus Epochs", + "mouse_session": null, + "stimulus_device_names": "device1, device2", + "stimulus_name": "stim1", + "stimulus_modalities": [ + "Auditory", + "Olfactory" + ], + "reward_consumed_during_epoch": 12.0, + "laser_name": "laserA", + "laser_wavelength": 12.0, + "laser_excitation_power": 1.0, + "speaker_name": "speaker1", + "speaker_volume": 1.0, + "mouse_pk": 3280, + "mouse_session_pk": 2894, + "experiment_run_step_pk": 64955 + } + ] +} diff --git a/tests/resources/slims/instrument_json_entity.json b/tests/resources/slims/raw/instrument_json_entity.json similarity index 100% rename from tests/resources/slims/instrument_json_entity.json rename to tests/resources/slims/raw/instrument_json_entity.json diff --git a/tests/resources/slims/json_entity.json b/tests/resources/slims/raw/json_entity.json similarity index 100% rename from tests/resources/slims/json_entity.json rename to tests/resources/slims/raw/json_entity.json diff --git a/tests/sharepoint/las2020/test_mapping.py b/tests/sharepoint/las2020/test_mapping.py index 734652cc..c4d3b64e 100644 --- a/tests/sharepoint/las2020/test_mapping.py +++ b/tests/sharepoint/las2020/test_mapping.py @@ -64,8 +64,16 @@ def test_parser(self): las_model = LASList.model_validate(raw_data) mapper = MappedLASList(las=las_model) mapped_procedure = mapper.get_procedure(subject_id="000000") - mapped_procedure_json = mapped_procedure.model_dump_json() - mapped_procedure_json_parsed = json.loads(mapped_procedure_json) + mapped_procedure_json = ( + mapped_procedure.model_dump_json() + if mapped_procedure + else None + ) + mapped_procedure_json_parsed = ( + json.loads(mapped_procedure_json) + if mapped_procedure_json + else None + ) self.assertEqual( expected_mapped_data, mapped_procedure_json_parsed ) diff --git a/tests/sharepoint/nsb2023/test_mapping.py b/tests/sharepoint/nsb2023/test_mapping.py index 0b9e48a8..8ebbf0a6 100644 --- a/tests/sharepoint/nsb2023/test_mapping.py +++ b/tests/sharepoint/nsb2023/test_mapping.py @@ -163,7 +163,6 @@ def test_burr_hole_to_probe_edge_case(self): raw_data = deepcopy(list_item[0]) nsb_model = NSBList.model_validate(raw_data) mapper = MappedNSBList(nsb=nsb_model) - self.assertIsNone(mapper._map_burr_hole_number_to_probe(7)) self.assertEqual(BurrHoleInfo(), mapper.burr_hole_info(7)) diff --git a/tests/slims/test_client.py b/tests/slims/test_client.py index f6e56fc1..c385ad82 100644 --- a/tests/slims/test_client.py +++ b/tests/slims/test_client.py @@ -1,17 +1,32 @@ """Testing SlimsHandler""" +import json +import os import unittest +from pathlib import Path from unittest.mock import MagicMock, patch from aind_data_schema.core.instrument import Instrument from aind_data_schema.core.rig import Rig from aind_slims_api.exceptions import SlimsRecordNotFound from aind_slims_api.models.instrument import SlimsInstrumentRdrc +from aind_slims_api.operations.ecephys_session import ( + EcephysSession as SlimsEcephysSession, +) from requests.models import Response from aind_metadata_service.client import StatusCodes from aind_metadata_service.slims.client import SlimsHandler, SlimsSettings +RESOURCES_DIR = ( + Path(os.path.dirname(os.path.realpath(__file__))) + / ".." + / "resources" + / "slims" +) +RAW_DIR = RESOURCES_DIR / "raw" +MAPPED_DIR = RESOURCES_DIR / "mapped" + class TestSlimsHandler(unittest.TestCase): """Test class for SlimsHandler""" @@ -25,6 +40,15 @@ def setUp(self, mock_slims_client): self.mock_client = mock_slims_client.return_value self.handler = SlimsHandler(settings) + with open(RAW_DIR / "ecephys_session_response.json", "r") as f: + slims_data1 = json.load(f) + with open(MAPPED_DIR / "ecephys_session.json", encoding="utf-8") as f: + expected_data1 = json.load(f) + self.slims_sessions = [ + SlimsEcephysSession.model_validate(slims_data1), + ] + self.expected_sessions = [expected_data1] + def test_is_json_file_true(self): """Test that _is_json_file returns True for valid JSON response.""" mock_response = MagicMock(spec=Response) @@ -39,11 +63,11 @@ def test_is_json_file_false(self): @patch( "aind_metadata_service.slims.client.SlimsHandler._is_json_file", - return_value=True, ) def test_get_instrument_model_response_success(self, mock_is_json_file): """Test successful response from get_instrument_model_response.""" mock_inst = MagicMock() + mock_is_json_file.return_value = True self.mock_client.fetch_model.return_value = mock_inst mock_attachment = MagicMock() self.mock_client.fetch_attachment.return_value = mock_attachment @@ -195,6 +219,49 @@ def test_get_rig_model_response_invalid_response(self): response.status_code, StatusCodes.INTERNAL_SERVER_ERROR ) + @patch("aind_metadata_service.slims.client.SlimsSessionMapper") + @patch("aind_metadata_service.slims.client.fetch_ecephys_sessions") + def test_get_sessions_model_response_success( + self, mock_fetch_sessions, mock_mapper + ): + """Tests that sessions data is fetched as expected.""" + mock_fetch_sessions.return_value = self.slims_sessions + mock_mapper_instance = mock_mapper.return_value + mock_mapper_instance.map_sessions.return_value = self.expected_sessions + response = self.handler.get_sessions_model_response("test_id") + + self.assertEqual(response.aind_models, self.expected_sessions) + self.assertEqual(response.status_code, StatusCodes.DB_RESPONDED) + + @patch("aind_metadata_service.slims.client.fetch_ecephys_sessions") + def test_get_sessions_model_response_no_data(self, mock_fetch_sessions): + """Tests no data found response.""" + mock_fetch_sessions.return_value = [] + response = self.handler.get_sessions_model_response("test_id") + + self.assertEqual(response.status_code, StatusCodes.NO_DATA_FOUND) + + @patch("aind_metadata_service.slims.client.fetch_ecephys_sessions") + def test_get_sessions_model_response_unexpected_error( + self, mock_fetch_sessions + ): + """Tests internal server error.""" "" + mock_fetch_sessions.side_effect = Exception("Unexpected error") + + response = self.handler.get_sessions_model_response("test_id") + + # Assert that the response is internal server error + self.assertEqual( + response.status_code, StatusCodes.INTERNAL_SERVER_ERROR + ) + + def test_get_sessions_model_response_not_found(self): + """Test response when SlimsRecordNotFound is raised.""" + self.mock_client.fetch_model.side_effect = SlimsRecordNotFound + + response = self.handler.get_sessions_model_response("test_id") + self.assertEqual(response.status_code, StatusCodes.NO_DATA_FOUND) + if __name__ == "__main__": unittest.main() diff --git a/tests/slims/test_mapping.py b/tests/slims/test_mapping.py new file mode 100644 index 00000000..dab85367 --- /dev/null +++ b/tests/slims/test_mapping.py @@ -0,0 +1,112 @@ +"""Tests Slims Mapper""" + +import json +import os +import unittest +from pathlib import Path + +from aind_data_schema.components.devices import SpoutSide +from aind_data_schema.core.session import RewardSolution +from aind_slims_api.models.ecephys_session import SlimsRewardDeliveryRdrc +from aind_slims_api.operations.ecephys_session import ( + EcephysSession as SlimsEcephysSession, +) + +from aind_metadata_service.slims.mapping import SlimsSessionMapper + +RESOURCES_DIR = ( + Path(os.path.dirname(os.path.realpath(__file__))) + / ".." + / "resources" + / "slims" +) +RAW_DIR = RESOURCES_DIR / "raw" +MAPPED_DIR = RESOURCES_DIR / "mapped" + + +class TestSlimsSessionMapper(unittest.TestCase): + """Class to test methods of SLimsSessionMapper""" + + def setUp(self): + """Sets up test class""" + self.mapper = SlimsSessionMapper() + with open(RAW_DIR / "ecephys_session_response.json") as f: + slims_data1 = json.load(f) + with open(RAW_DIR / "ecephys_session_response2.json") as f: + slims_data2 = json.load(f) + with open(MAPPED_DIR / "ecephys_session.json") as f: + expected_data1 = json.load(f) + with open(MAPPED_DIR / "ecephys_session2.json") as f: + expected_data2 = json.load(f) + self.slims_sessions = [ + SlimsEcephysSession.model_validate(slims_data1), + SlimsEcephysSession.model_validate(slims_data2), + ] + self.expected_sessions = [expected_data1, expected_data2] + + def test_map_sessions(self): + """Tests map sessions""" + sessions = self.mapper.map_sessions( + sessions=self.slims_sessions, subject_id="000000" + ) + self.assertEqual(len(sessions), 2) + mapped_session_json1 = sessions[0].model_dump_json() + mapped_session_json_parsed1 = json.loads(mapped_session_json1) + self.assertEqual( + mapped_session_json_parsed1, self.expected_sessions[0] + ) + mapped_session_json2 = sessions[1].model_dump_json() + mapped_session_json_parsed2 = json.loads(mapped_session_json2) + self.assertEqual( + mapped_session_json_parsed2, self.expected_sessions[1] + ) + + def test_map_spout_side(self): + """Tests spout side is mapped correctly.""" + self.assertEqual(self.mapper._map_spout_side("left"), SpoutSide.LEFT) + self.assertEqual(self.mapper._map_spout_side("LEFT"), SpoutSide.LEFT) + self.assertEqual( + self.mapper._map_spout_side("Left side"), SpoutSide.LEFT + ) + self.assertEqual(self.mapper._map_spout_side("right"), SpoutSide.RIGHT) + self.assertEqual(self.mapper._map_spout_side("RIGHT"), SpoutSide.RIGHT) + self.assertEqual( + self.mapper._map_spout_side("Right spout"), SpoutSide.RIGHT + ) + self.assertEqual( + self.mapper._map_spout_side("center"), SpoutSide.CENTER + ) + self.assertEqual( + self.mapper._map_spout_side("CENTER"), SpoutSide.CENTER + ) + self.assertEqual( + self.mapper._map_spout_side("Center spout"), SpoutSide.CENTER + ) + self.assertEqual( + self.mapper._map_spout_side("unknown"), SpoutSide.OTHER + ) + self.assertEqual( + self.mapper._map_spout_side("random text"), SpoutSide.OTHER + ) + self.assertEqual(self.mapper._map_spout_side(""), SpoutSide.OTHER) + + def test_map_reward_solution(self): + """Tests that reward solution is mapper correctly.""" + reward_delivery = SlimsRewardDeliveryRdrc.model_construct( + reward_solution="Other, (if Other, specify below)", + other_reward_solution="Some Solution", + ) + solution, notes = self.mapper._map_reward_solution(reward_delivery) + self.assertEqual(solution, RewardSolution.OTHER) + self.assertEqual(notes, "Some Solution") + + reward_delivery2 = SlimsRewardDeliveryRdrc.model_construct( + reward_solution=None, other_reward_solution=None + ) + self.assertEqual( + (None, None), self.mapper._map_reward_solution(reward_delivery2) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tars/test_mapping.py b/tests/tars/test_mapping.py index 05975cd4..1fae9931 100644 --- a/tests/tars/test_mapping.py +++ b/tests/tars/test_mapping.py @@ -28,10 +28,10 @@ class TestTarsResponseHandler(unittest.TestCase): handler = TarsResponseHandler() inj1 = NanojectInjection.model_construct( - injection_materials=[ViralMaterial.model_construct(name="12345")] + injection_materials=[ViralMaterial.model_construct(name="\n12345 ")] ) inj2 = NanojectInjection.model_construct( - injection_materials=[ViralMaterial.model_construct(name="67890")] + injection_materials=[ViralMaterial.model_construct(name=" 67890\t")] ) surgery = Surgery.model_construct(procedures=[inj1, inj2]) procedures_response = ModelResponse( diff --git a/tests/test_response_handler.py b/tests/test_response_handler.py index 95c4dcdb..8e02e469 100644 --- a/tests/test_response_handler.py +++ b/tests/test_response_handler.py @@ -195,13 +195,41 @@ def test_no_validation(self): self.assertEqual(expected_json.body, actual_json.body) def test_multiple_items_response(self): - """Test multiple item response""" + """Test multiple item response with validation.""" models = [sp_valid_model, sp_valid_model] model_response = ModelResponse( status_code=StatusCodes.DB_RESPONDED, aind_models=models ) actual_json = model_response.map_to_json_response() + models_json = [ + jsonable_encoder(json.loads(model.model_dump_json())) + for model in models + ] + validation_error = ModelResponse._validate_model(sp_valid_model) + expected_json = JSONResponse( + status_code=300, + content=( + { + "message": f"Multiple Items Found. Validation Errors:" + f" {validation_error}, {validation_error}", + "data": models_json, + } + ), + ) + + self.assertEqual(StatusCodes.DB_RESPONDED, model_response.status_code) + self.assertEqual(expected_json.status_code, actual_json.status_code) + self.assertEqual(expected_json.body, actual_json.body) + + def test_multiple_items_response_no_validation(self): + """Test multiple item response""" + models = [sp_valid_model, sp_valid_model] + model_response = ModelResponse( + status_code=StatusCodes.DB_RESPONDED, aind_models=models + ) + actual_json = model_response.map_to_json_response(validate=False) + models_json = [ jsonable_encoder(json.loads(model.model_dump_json())) for model in models @@ -210,7 +238,8 @@ def test_multiple_items_response(self): status_code=300, content=( { - "message": "Multiple Items Found.", + "message": "Multiple Items Found." + " Models have not been validated.", "data": models_json, } ),