diff --git a/app/db/model.py b/app/db/model.py index c62ba41a..cf4e2cca 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -841,6 +841,27 @@ class ValidationResult(Entity): } +class CalibrationResult(Entity): + __tablename__ = EntityType.calibration_result.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) + value: Mapped[float] = mapped_column(default=False) + + name: Mapped[str] = mapped_column(index=True) + description: Mapped[str] = mapped_column(index=True) + + calibrated_entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) + calibrated_entity: Mapped[Entity] = relationship( + "Entity", + uselist=False, + foreign_keys=[calibrated_entity_id], + ) + + __mapper_args__ = { # noqa: RUF012 + "polymorphic_identity": __tablename__, + "inherit_condition": id == Entity.id, + } + + class Asset(Identifiable): """Asset table.""" diff --git a/app/db/types.py b/app/db/types.py index 3b4d2f22..425d6152 100644 --- a/app/db/types.py +++ b/app/db/types.py @@ -67,6 +67,7 @@ class EntityType(StrEnum): ion_channel_model = auto() subject = auto() validation_result = auto() + calibration_result = auto() class AgentType(StrEnum): diff --git a/app/filters/calibration_result.py b/app/filters/calibration_result.py new file mode 100644 index 00000000..ba825465 --- /dev/null +++ b/app/filters/calibration_result.py @@ -0,0 +1,27 @@ +import uuid +from typing import Annotated + +from fastapi_filter import FilterDepends + +from app.db.model import CalibrationResult +from app.filters.base import CustomFilter +from app.filters.common import EntityFilterMixin + + +class CalibrationResultFilter( + CustomFilter, + EntityFilterMixin, +): + value: float | None = None + validated_entity_id: uuid.UUID | None = None + + order_by: list[str] = ["name"] # noqa: RUF012 + + class Constants(CustomFilter.Constants): + model = CalibrationResult + ordering_model_fields = ["name"] # noqa: RUF012 + + +CalibrationResultFilterDep = Annotated[ + CalibrationResultFilter, FilterDepends(CalibrationResultFilter) +] diff --git a/app/routers/__init__.py b/app/routers/__init__.py index 18740a08..61488cd0 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -7,6 +7,7 @@ asset, brain_region, brain_region_hierarchy, + calibration_result, cell_composition, contribution, electrical_cell_recording, @@ -40,6 +41,7 @@ asset.router, brain_region.router, brain_region_hierarchy.router, + calibration_result.router, cell_composition.router, contribution.router, electrical_cell_recording.router, diff --git a/app/routers/calibration_result.py b/app/routers/calibration_result.py new file mode 100644 index 00000000..9a857b8b --- /dev/null +++ b/app/routers/calibration_result.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +import app.service.calibration_result + +router = APIRouter( + prefix="/calibration-result", + tags=["calibration-result"], +) + +read_many = router.get("")(app.service.calibration_result.read_many) +read_one = router.get("/{id_}")(app.service.calibration_result.read_one) +create_one = router.post("")(app.service.calibration_result.create_one) diff --git a/app/schemas/calibration.py b/app/schemas/calibration.py new file mode 100644 index 00000000..738e0e83 --- /dev/null +++ b/app/schemas/calibration.py @@ -0,0 +1,26 @@ +import uuid + +from pydantic import BaseModel + +from app.schemas.agent import CreatedByUpdatedByMixin +from app.schemas.base import ( + CreationMixin, + IdentifiableMixin, +) + + +class CalibrationResultBase(BaseModel): + name: str + description: str + value: float + calibrated_entity_id: uuid.UUID + + +class CalibrationResultRead( + CalibrationResultBase, CreationMixin, IdentifiableMixin, CreatedByUpdatedByMixin +): + pass + + +class CalibrationResultCreate(CalibrationResultBase): + pass diff --git a/app/service/calibration_result.py b/app/service/calibration_result.py new file mode 100644 index 00000000..28477c6a --- /dev/null +++ b/app/service/calibration_result.py @@ -0,0 +1,82 @@ +import uuid + +import sqlalchemy as sa +from sqlalchemy.orm import joinedload + +from app.db.model import CalibrationResult, Subject +from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.common import ( + FacetsDep, + InBrainRegionDep, + PaginationQuery, + SearchDep, +) +from app.dependencies.db import SessionDep +from app.filters.calibration_result import CalibrationResultFilterDep +from app.queries.common import router_create_one, router_read_many, router_read_one +from app.schemas.calibration import CalibrationResultCreate, CalibrationResultRead +from app.schemas.types import ListResponse + + +def _load(query: sa.Select): + return query.options( + joinedload(Subject.species), + ) + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> CalibrationResultRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=CalibrationResult, + authorized_project_id=user_context.project_id, + response_schema_class=CalibrationResultRead, + apply_operations=_load, + ) + + +def create_one( + user_context: UserContextWithProjectIdDep, + json_model: CalibrationResultCreate, + db: SessionDep, +) -> CalibrationResultRead: + return router_create_one( + db=db, + user_context=user_context, + db_model_class=CalibrationResult, + json_model=json_model, + response_schema_class=CalibrationResultRead, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: CalibrationResultFilterDep, + with_search: SearchDep, + facets: FacetsDep, + in_brain_region: InBrainRegionDep, +) -> ListResponse[CalibrationResultRead]: + aliases = {} + name_to_facet_query_params = {} + return router_read_many( + db=db, + filter_model=filter_model, + db_model_class=CalibrationResult, + with_search=with_search, + with_in_brain_region=in_brain_region, + facets=facets, + name_to_facet_query_params=name_to_facet_query_params, + apply_filter_query_operations=None, + apply_data_query_operations=_load, + aliases=aliases, + pagination_request=pagination_request, + response_schema_class=CalibrationResultRead, + authorized_project_id=user_context.project_id, + filter_joins=None, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 38173532..855dcd05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -386,6 +386,21 @@ def validation_result_id(client, morphology_id): ).json()["id"] +@pytest.fixture +def calibration_result_id(client, memodel_id): + return assert_request( + client.post, + url="/calibration-result", + json={ + "name": "threshold_current", + "description": "threshold_current (mV) of the memodel for testing", + "value": 0.0654321, + "calibrated_entity_id": str(memodel_id), + "authorized_public": False, + }, + ).json()["id"] + + CreateIds = Callable[[int], list[str]] diff --git a/tests/test_calibration_result.py b/tests/test_calibration_result.py new file mode 100644 index 00000000..5c671524 --- /dev/null +++ b/tests/test_calibration_result.py @@ -0,0 +1,136 @@ +import uuid + +import pytest +from fastapi.testclient import TestClient + +from app.db.model import CalibrationResult + +from .utils import assert_request + +MODEL = CalibrationResult +ROUTE = "/calibration-result" + + +@pytest.fixture +def json_data(memodel_id): + return { + "value": 0.0654321, + "calibrated_entity_id": str(memodel_id), + "name": "threshold_current", + "description": "threshold_current (mV) of the memodel for testing", + "authorized_public": False, + } + + +@pytest.fixture +def create(client, json_data): + def _create(**kwargs): + return assert_request(client.post, url=ROUTE, json=json_data | kwargs).json() + + return _create + + +def _assert_read_response(data, json_data): + assert data["name"] == json_data["name"] + assert data["description"] == json_data["description"] + assert data["value"] == json_data["value"] + assert data["calibrated_entity_id"] == json_data["calibrated_entity_id"] + assert data["createdBy"]["id"] == data["updatedBy"]["id"] + assert data["creation_date"] == data["update_date"] + + +def test_read_one(client: TestClient, calibrated_result_id, json_data): + data = assert_request(client.get, url=f"{ROUTE}/{calibrated_result_id}").json() + _assert_read_response(data, json_data) + assert data["id"] == calibrated_result_id + + +def test_create_one(client: TestClient, json_data): + data = assert_request(client.post, url=ROUTE, json=json_data).json() + _assert_read_response(data, json_data) + + data = assert_request(client.get, url=f"{ROUTE}/{data['id']}").json() + _assert_read_response(data, json_data) + + data = assert_request(client.get, url=ROUTE).json()["data"][0] + _assert_read_response(data, json_data) + + +def test_missing(client): + response = client.get(f"{ROUTE}/{uuid.uuid4()}") + assert response.status_code == 404 + + response = client.get(f"{ROUTE}/notauuid") + assert response.status_code == 422 + + +def test_filtering__one_entry(client, calibration_result_id, memodel_id): + # no results expected for unrelated id + data = assert_request( + client.get, + url=ROUTE, + params={"calibrated_entity_id": str(calibration_result_id)}, + ).json()["data"] + + assert len(data) == 0 + + data = assert_request( + client.get, + url=ROUTE, + params={"calibrated_entity_id": str(memodel_id)}, + ).json()["data"] + + assert len(data) == 1 + assert data[0]["calibrated_entity_id"] == str(memodel_id) + + +@pytest.fixture +def models(create, memodel_id, emodel_id): + return [ + create( + name="me1", + description="test memodel 1", + value=0.1234567, + validated_entity_id=str(memodel_id), + ), + create( + name="me2", + description="test memodel 2", + value=0.0, + validated_entity_id=str(memodel_id), + ), + create( + name="e1", + description="test emodel 1", + value=0.1234567, + validated_entity_id=str(emodel_id), + ), + create( + name="e2", + description="test emodel 2", + value=0.0, + validated_entity_id=str(emodel_id), + ), + ] + + +def test_filtering__many_entries(client, models, memodel_id, emodel_id): + data = assert_request( + client.get, + url=ROUTE, + params={"calibrated_entity_id": str(memodel_id)}, + ).json()["data"] + + assert len(data) == 2 + assert data[0]["calibrated_entity_id"] == models[0]["calibrated_entity_id"] + assert data[1]["calibrated_entity_id"] == models[1]["calibrated_entity_id"] + + data = assert_request( + client.get, + url=ROUTE, + params={"calibrated_entity_id": str(emodel_id)}, + ).json()["data"] + + assert len(data) == 2 + assert data[0]["calibrated_entity_id"] == models[2]["calibrated_entity_id"] + assert data[1]["calibrated_entity_id"] == models[3]["calibrated_entity_id"]