diff --git a/app/filters/brain_region_mesh.py b/app/filters/brain_region_mesh.py new file mode 100644 index 00000000..082916d5 --- /dev/null +++ b/app/filters/brain_region_mesh.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi_filter import FilterDepends + +from app.db.model import Mesh as BrainRegionMesh +from app.filters.base import CustomFilter +from app.filters.common import BrainRegionFilterMixin, EntityFilterMixin + + +class BrainRegionMeshFilter(CustomFilter, BrainRegionFilterMixin, EntityFilterMixin): + order_by: list[str] = ["-creation_date"] # noqa: RUF012 + + class Constants(CustomFilter.Constants): + model = BrainRegionMesh + ordering_model_fields = ["creation_date", "update_date", "name"] # noqa: RUF012 + + +BrainRegionMeshFilterDep = Annotated[BrainRegionMeshFilter, FilterDepends(BrainRegionMeshFilter)] diff --git a/app/filters/common.py b/app/filters/common.py index ef9d09ba..4cecf539 100644 --- a/app/filters/common.py +++ b/app/filters/common.py @@ -140,9 +140,10 @@ class SubjectFilterMixin: class BrainRegionFilter(NameFilterMixin, CustomFilter): - # TODO: Use IdFilterMixin when brain region keys migrate from int to uuid - id: int | None = None - id__in: list[int] | None = None + id: uuid.UUID | None = None + id__in: list[uuid.UUID] | None = None + hierarchy_id: uuid.UUID | None = None + annotation_value: int | None = None acronym: str | None = None acronym__in: list[str] | None = None order_by: list[str] = ["name"] # noqa: RUF012 diff --git a/app/routers/__init__.py b/app/routers/__init__.py index 89bd8499..9786025b 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -7,6 +7,7 @@ asset, brain_region, brain_region_hierarchy, + brain_region_mesh, cell_composition, contribution, electrical_cell_recording, @@ -38,6 +39,7 @@ asset.router, brain_region.router, brain_region_hierarchy.router, + brain_region_mesh.router, cell_composition.router, contribution.router, electrical_cell_recording.router, diff --git a/app/routers/brain_region_mesh.py b/app/routers/brain_region_mesh.py new file mode 100644 index 00000000..f1abd17a --- /dev/null +++ b/app/routers/brain_region_mesh.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +import app.service.brain_region_mesh + +router = APIRouter( + prefix="/brain-region-mesh", + tags=["brain-region-mesh"], +) + +read_many = router.get("")(app.service.brain_region_mesh.read_many) +read_one = router.get("/{id_}")(app.service.brain_region_mesh.read_one) +create_one = router.post("")(app.service.brain_region_mesh.create_one) diff --git a/app/schemas/brain_region_mesh.py b/app/schemas/brain_region_mesh.py new file mode 100644 index 00000000..f90b2b02 --- /dev/null +++ b/app/schemas/brain_region_mesh.py @@ -0,0 +1,28 @@ +import uuid + +from pydantic import BaseModel, ConfigDict + +from app.schemas.base import ( + AuthorizationMixin, + AuthorizationOptionalPublicMixin, + BrainRegionRead, + CreationMixin, + EntityTypeMixin, + IdentifiableMixin, +) + + +class BrainRegionMeshBase(BaseModel): + model_config = ConfigDict(from_attributes=True) + name: str + description: str + + +class BrainRegionMeshCreate(BrainRegionMeshBase, AuthorizationOptionalPublicMixin): + brain_region_id: uuid.UUID + + +class BrainRegionMeshRead( + BrainRegionMeshBase, CreationMixin, IdentifiableMixin, AuthorizationMixin, EntityTypeMixin +): + brain_region: BrainRegionRead diff --git a/app/service/brain_region_mesh.py b/app/service/brain_region_mesh.py new file mode 100644 index 00000000..256a9495 --- /dev/null +++ b/app/service/brain_region_mesh.py @@ -0,0 +1,87 @@ +import uuid + +from sqlalchemy.orm import joinedload, raiseload, selectinload + +from app.db.model import BrainRegion, Mesh as BrainRegionMesh +from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.common import ( + FacetQueryParams, + FacetsDep, + InBrainRegionDep, + PaginationQuery, + SearchDep, +) +from app.dependencies.db import SessionDep +from app.filters.brain_region_mesh import BrainRegionMeshFilterDep +from app.queries import facets as fc +from app.queries.common import router_create_one, router_read_many, router_read_one +from app.schemas.brain_region_mesh import BrainRegionMeshCreate, BrainRegionMeshRead +from app.schemas.types import ListResponse + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> BrainRegionMeshRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=BrainRegionMesh, + authorized_project_id=user_context.project_id, + response_schema_class=BrainRegionMeshRead, + apply_operations=lambda q: q.options( + joinedload(BrainRegionMesh.brain_region), + joinedload(BrainRegionMesh.assets), + raiseload("*"), + ), + ) + + +def create_one( + user_context: UserContextWithProjectIdDep, + json_model: BrainRegionMeshCreate, + db: SessionDep, +) -> BrainRegionMeshRead: + return router_create_one( + db=db, + json_model=json_model, + db_model_class=BrainRegionMesh, + authorized_project_id=user_context.project_id, + response_schema_class=BrainRegionMeshRead, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: BrainRegionMeshFilterDep, + with_search: SearchDep, + in_brain_region: InBrainRegionDep, + facets: FacetsDep, +) -> ListResponse[BrainRegionMeshRead]: + name_to_facet_query_params: dict[str, FacetQueryParams] = fc.brain_region + apply_filter_query = lambda query: ( + query.join(BrainRegion, BrainRegionMesh.brain_region_id == BrainRegion.id) + ) + apply_data_options = lambda query: ( + query.options(joinedload(BrainRegionMesh.brain_region)) + .options(selectinload(BrainRegionMesh.assets)) + .options(raiseload("*")) + ) + return router_read_many( + db=db, + filter_model=filter_model, + db_model_class=BrainRegionMesh, + 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=apply_filter_query, + apply_data_query_operations=apply_data_options, + aliases={}, + pagination_request=pagination_request, + response_schema_class=BrainRegionMeshRead, + authorized_project_id=user_context.project_id, + ) diff --git a/tests/test_brain_region_mesh.py b/tests/test_brain_region_mesh.py new file mode 100644 index 00000000..01144e4f --- /dev/null +++ b/tests/test_brain_region_mesh.py @@ -0,0 +1,103 @@ +import pytest + +from .utils import ( + assert_request, + check_authorization, + check_missing, + check_pagination, +) + +ROUTE = "/brain-region-mesh" + + +@pytest.fixture +def json_data( + brain_region_id, +): + return { + "brain_region_id": str(brain_region_id), + "description": "my-description", + "name": "my-name", + "authorized_public": False, + } + + +def _assert_read_response(data, json_data): + assert data["brain_region"]["id"] == json_data["brain_region_id"] + assert data["description"] == json_data["description"] + assert data["name"] == json_data["name"] + assert data["type"] == "mesh" + + +@pytest.fixture +def create_id(client, json_data): + def _create_id(**kwargs): + return assert_request(client.post, url=ROUTE, json=json_data | kwargs).json()["id"] + + return _create_id + + +@pytest.fixture +def model_id(create_id): + return create_id() + + +def test_create_one(client, json_data): + data = assert_request(client.post, url=ROUTE, json=json_data).json() + _assert_read_response(data, json_data) + + +def test_read_one(client, model_id, json_data): + data = assert_request(client.get, url=f"{ROUTE}/{model_id}").json() + _assert_read_response(data, json_data) + + data = assert_request(client.get, url=ROUTE).json() + assert len(data["data"]) == 1 + + +def test_missing(client): + check_missing(ROUTE, client) + + +def test_authorization( + client_user_1, + client_user_2, + client_no_project, + json_data, +): + check_authorization(ROUTE, client_user_1, client_user_2, client_no_project, json_data) + + +def test_pagination(client, create_id): + check_pagination(ROUTE, client, create_id) + + +def test_filtering(client, brain_region_id, brain_region_hierarchy_id, model_id): + data = assert_request( + client.get, + url=ROUTE, + params={"brain_region__id": str(model_id)}, + ).json()["data"] + + assert len(data) == 0 + + data = assert_request( + client.get, + url=ROUTE, + params={"brain_region__id": str(brain_region_id)}, + ).json()["data"] + + assert len(data) == 1 + assert data[0]["brain_region"]["id"] == str(brain_region_id) + + data = assert_request( + client.get, + url=ROUTE, + params={ + "brain_region__name": "RedRegion", + "brain_region__hierarchy_id": str(brain_region_hierarchy_id), + }, + ).json()["data"] + + assert len(data) == 1 + assert data[0]["brain_region"]["id"] == str(brain_region_id)