diff --git a/app/db/model.py b/app/db/model.py index f9373e8d..eecfece3 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -2271,4 +2271,114 @@ class SkeletonizationExecution(Activity, ExecutionActivityMixin): __mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012 +class Mapping(ScientificArtifact, NameDescriptionVectorMixin): + """Represents a mapping between two classification schemes. + + A mapping defines relationships between two classification schemes, such as + mapping from OBI_mtypes to OBI_etypes. Each mapping contains multiple + mapping relations that define specific relationships between classifications. + + Attributes: + id (uuid.UUID): Primary key, inherited from ScientificArtifact. + name (str): Name of mapping, inherited from NameDescriptionVectorMixin. + version (str): Version of mapping. + source_schema_id (uuid.UUID): Foreign key to source EntityClassificationScheme. + target_schema_id (uuid.UUID): Foreign key to target EntityClassificationScheme. + mapping_relations (list[MappingRelation]): Relations within this mapping. + """ + + __tablename__ = EntityType.mapping.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("scientific_artifact.id"), primary_key=True) + + version: Mapped[str] + + # Foreign keys to EntityClassificationScheme (placeholder entities for now) + source_schema_id: Mapped[uuid.UUID] = mapped_column(index=True) + target_schema_id: Mapped[uuid.UUID] = mapped_column(index=True) + + mapping_relations: Mapped[list["MappingRelation"]] = relationship( + "MappingRelation", + back_populates="mapping", + cascade="all, delete-orphan", + passive_deletes=True, + ) + + __table_args__ = ( + UniqueConstraint("source_schema_id", "target_schema_id", "version", name="uq_mapping_source_target_version"), + ) + + __mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012 + + +class MappingRelation(ScientificArtifact): + """Represents a specific relation within a mapping. + + A mapping relation defines the relationship between a specific source classification + and a target classification within a mapping. It can contain data values or + references to parameterisation relations for complex data. + + Attributes: + id (uuid.UUID): Primary key, inherited from ScientificArtifact. + mapping_id (uuid.UUID): Foreign key to parent mapping. + mapping (Mapping): The parent mapping this relation belongs to. + source_id (uuid.UUID): Foreign key to source EntityClassification. + target_id (uuid.UUID): Foreign key to target EntityClassification. + data (str | int | float | bool | None): Simple data value or reference to parameterisation. + """ + + __tablename__ = EntityType.mapping_relation.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("scientific_artifact.id"), primary_key=True) + + mapping_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("mapping.id"), index=True + ) + mapping: Mapped["Mapping"] = relationship( + "Mapping", + back_populates="mapping_relations", + uselist=False, + ) + + # Foreign keys to EntityClassification (placeholder entities for now) + source_id: Mapped[uuid.UUID] = mapped_column(index=True) + target_id: Mapped[uuid.UUID] = mapped_column(index=True) + + # Can store simple data values (int/float/bool/str) + data: Mapped[str | int | float | bool | None] = mapped_column(JSONB) + + parameterisation_relation_id: Mapped[uuid.UUID | None] = mapped_column( + ForeignKey("parameterisation_relation.id"), index=True + ) + parameterisation_relation: Mapped["ParameterisationRelation"] = relationship( + "ParameterisationRelation", + foreign_keys=[parameterisation_relation_id], + uselist=False, + ) + + __table_args__ = ( + UniqueConstraint("mapping_id", "source_id", "target_id", name="uq_mapping_relation_source_target"), + ) + + __mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012 + + +class ParameterisationRelation(ScientificArtifact): + """Represents complex parameter data for mapping relations. + + A parameterisation relation stores complex data structures as key-value pairs, + used when a mapping relation needs to contain multiple data values + (e.g., synaptic physiology parameters). + + Attributes: + id (uuid.UUID): Primary key, inherited from ScientificArtifact. + data (dict): Dictionary of key-value pairs containing the parameter data. + """ + + __tablename__ = EntityType.parameterisation_relation.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("scientific_artifact.id"), primary_key=True) + + data: Mapped[JSON_DICT] + + __mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012 + + register_model_events() diff --git a/app/db/types.py b/app/db/types.py index 19f00a83..66114f83 100644 --- a/app/db/types.py +++ b/app/db/types.py @@ -141,6 +141,9 @@ class EntityType(StrEnum): analysis_notebook_result = auto() skeletonization_config = auto() skeletonization_campaign = auto() + mapping = auto() + mapping_relation = auto() + parameterisation_relation = auto() class AgentType(StrEnum): diff --git a/app/filters/mapping.py b/app/filters/mapping.py new file mode 100644 index 00000000..266e0371 --- /dev/null +++ b/app/filters/mapping.py @@ -0,0 +1,22 @@ +import uuid +from typing import Annotated + +from app.db.model import Mapping +from app.dependencies.filter import FilterDepends +from app.filters.common import ILikeSearchFilterMixin, NameFilterMixin +from app.filters.scientific_artifact import ScientificArtifactFilter + + +class MappingFilter(ScientificArtifactFilter, NameFilterMixin, ILikeSearchFilterMixin): + version: str | None = None + source_schema_id: uuid.UUID | None = None + target_schema_id: uuid.UUID | None = None + + order_by: list[str] = ["-creation_date"] # noqa: RUF012 + + class Constants(ScientificArtifactFilter.Constants): + model = Mapping + ordering_model_fields = ["creation_date", "update_date", "name", "version"] # noqa: RUF012 + + +MappingFilterDep = Annotated[MappingFilter, FilterDepends(MappingFilter)] diff --git a/app/filters/mapping_relation.py b/app/filters/mapping_relation.py new file mode 100644 index 00000000..80574ce9 --- /dev/null +++ b/app/filters/mapping_relation.py @@ -0,0 +1,23 @@ +import uuid +from typing import Annotated + +from app.db.model import MappingRelation +from app.dependencies.filter import FilterDepends +from app.filters.common import ILikeSearchFilterMixin +from app.filters.scientific_artifact import ScientificArtifactFilter + + +class MappingRelationFilter(ScientificArtifactFilter, ILikeSearchFilterMixin): + mapping_id: uuid.UUID | None = None + source_id: uuid.UUID | None = None + target_id: uuid.UUID | None = None + parameterisation_relation_id: uuid.UUID | None = None + + order_by: list[str] = ["-creation_date"] # noqa: RUF012 + + class Constants(ScientificArtifactFilter.Constants): + model = MappingRelation + ordering_model_fields = ["creation_date", "update_date"] # noqa: RUF012 + + +MappingRelationFilterDep = Annotated[MappingRelationFilter, FilterDepends(MappingRelationFilter)] diff --git a/app/filters/parameterisation_relation.py b/app/filters/parameterisation_relation.py new file mode 100644 index 00000000..d38d9fad --- /dev/null +++ b/app/filters/parameterisation_relation.py @@ -0,0 +1,20 @@ +from typing import Annotated + +from app.db.model import ParameterisationRelation +from app.dependencies.filter import FilterDepends +from app.filters.common import ILikeSearchFilterMixin +from app.filters.scientific_artifact import ScientificArtifactFilter + + +class ParameterisationRelationFilter(ScientificArtifactFilter, ILikeSearchFilterMixin): + order_by: list[str] = ["-creation_date"] # noqa: RUF012 + + class Constants(ScientificArtifactFilter.Constants): + model = ParameterisationRelation + ordering_model_fields = ["creation_date", "update_date"] # noqa: RUF012 + + +ParameterisationRelationFilterDep = Annotated[ + ParameterisationRelationFilter, + FilterDepends(ParameterisationRelationFilter), +] diff --git a/app/routers/__init__.py b/app/routers/__init__.py index b0c898ab..bd369ce8 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -48,6 +48,8 @@ license, measurement_annotation, measurement_label, + mapping, + mapping_relation, memodel, memodel_calibration_result, mtype, @@ -59,6 +61,7 @@ root, scientific_artifact_external_url_link, scientific_artifact_publication_link, + parameterisation_relation, simulation, simulation_campaign, simulation_execution, @@ -127,6 +130,8 @@ license.router, measurement_annotation.router, measurement_label.router, + mapping.router, + mapping_relation.router, memodel.router, memodel_calibration_result.router, mtype.router, @@ -137,6 +142,7 @@ role.router, scientific_artifact_external_url_link.router, scientific_artifact_publication_link.router, + parameterisation_relation.router, skeletonization_campaign.router, skeletonization_config.router, skeletonization_config_generation.router, diff --git a/app/routers/mapping.py b/app/routers/mapping.py new file mode 100644 index 00000000..ff4ef19e --- /dev/null +++ b/app/routers/mapping.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +import app.service.mapping +from app.routers.admin import router as admin_router + +ROUTE = "mapping" + +router = APIRouter( + prefix=f"/{ROUTE}", + tags=[ROUTE], +) + +read_many = router.get("")(app.service.mapping.read_many) +read_one = router.get("/{id_}")(app.service.mapping.read_one) +create_one = router.post("")(app.service.mapping.create_one) +update_one = router.patch("/{id_}")(app.service.mapping.update_one) +delete_one = router.delete("/{id_}")(app.service.mapping.delete_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.mapping.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.mapping.admin_update_one) diff --git a/app/routers/mapping_relation.py b/app/routers/mapping_relation.py new file mode 100644 index 00000000..4f025b0b --- /dev/null +++ b/app/routers/mapping_relation.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter + +import app.service.mapping_relation +from app.routers.admin import router as admin_router + +ROUTE = "mapping-relation" + +router = APIRouter( + prefix=f"/{ROUTE}", + tags=[ROUTE], +) + +read_many = router.get("")(app.service.mapping_relation.read_many) +read_one = router.get("/{id_}")(app.service.mapping_relation.read_one) +create_one = router.post("")(app.service.mapping_relation.create_one) +update_one = router.patch("/{id_}")(app.service.mapping_relation.update_one) +delete_one = router.delete("/{id_}")(app.service.mapping_relation.delete_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( + app.service.mapping_relation.admin_read_one +) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.mapping_relation.admin_update_one +) diff --git a/app/routers/parameterisation_relation.py b/app/routers/parameterisation_relation.py new file mode 100644 index 00000000..7e8b097d --- /dev/null +++ b/app/routers/parameterisation_relation.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter + +import app.service.parameterisation_relation +from app.routers.admin import router as admin_router + +ROUTE = "parameterisation-relation" + +router = APIRouter( + prefix=f"/{ROUTE}", + tags=[ROUTE], +) + +read_many = router.get("")(app.service.parameterisation_relation.read_many) +read_one = router.get("/{id_}")(app.service.parameterisation_relation.read_one) +create_one = router.post("")(app.service.parameterisation_relation.create_one) +update_one = router.patch("/{id_}")(app.service.parameterisation_relation.update_one) +delete_one = router.delete("/{id_}")(app.service.parameterisation_relation.delete_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( + app.service.parameterisation_relation.admin_read_one +) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.parameterisation_relation.admin_update_one +) diff --git a/app/schemas/mapping.py b/app/schemas/mapping.py new file mode 100644 index 00000000..e76329d1 --- /dev/null +++ b/app/schemas/mapping.py @@ -0,0 +1,36 @@ +import uuid + +from pydantic import BaseModel, ConfigDict + +from app.schemas.base import NameDescriptionMixin +from app.schemas.scientific_artifact import ScientificArtifactCreate, ScientificArtifactRead +from app.schemas.utils import make_update_schema + + +class MappingBase(BaseModel, NameDescriptionMixin): + model_config = ConfigDict(from_attributes=True) + version: str + source_schema_id: uuid.UUID + target_schema_id: uuid.UUID + + +class MappingCreate( + MappingBase, + ScientificArtifactCreate, +): + pass + + +MappingUserUpdate = make_update_schema(MappingCreate, "MappingUserUpdate") # pyright: ignore [reportInvalidTypeForm] +MappingAdminUpdate = make_update_schema( + MappingCreate, + "MappingAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + +class MappingRead( + MappingBase, + ScientificArtifactRead, +): + pass diff --git a/app/schemas/mapping_relation.py b/app/schemas/mapping_relation.py new file mode 100644 index 00000000..8127b0ea --- /dev/null +++ b/app/schemas/mapping_relation.py @@ -0,0 +1,43 @@ +import uuid +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from app.schemas.parameterisation_relation import NestedParameterisationRelationRead +from app.schemas.scientific_artifact import ScientificArtifactCreate, ScientificArtifactRead +from app.schemas.utils import make_update_schema + + +class MappingRelationBase(BaseModel): + model_config = ConfigDict(from_attributes=True) + + mapping_id: uuid.UUID + source_id: uuid.UUID + target_id: uuid.UUID + data: Any | None = None + parameterisation_relation_id: uuid.UUID | None = None + + +class MappingRelationCreate( + MappingRelationBase, + ScientificArtifactCreate, +): + pass + + +MappingRelationUserUpdate = make_update_schema( + MappingRelationCreate, + "MappingRelationUserUpdate", +) # pyright: ignore [reportInvalidTypeForm] +MappingRelationAdminUpdate = make_update_schema( + MappingRelationCreate, + "MappingRelationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + +class MappingRelationRead( + MappingRelationBase, + ScientificArtifactRead, +): + parameterisation_relation: NestedParameterisationRelationRead | None = None diff --git a/app/schemas/parameterisation_relation.py b/app/schemas/parameterisation_relation.py new file mode 100644 index 00000000..2c80c8ab --- /dev/null +++ b/app/schemas/parameterisation_relation.py @@ -0,0 +1,46 @@ +import uuid +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from app.schemas.base import EntityTypeMixin, IdentifiableMixin +from app.schemas.scientific_artifact import ScientificArtifactCreate, ScientificArtifactRead +from app.schemas.utils import make_update_schema + + +class ParameterisationRelationBase(BaseModel): + model_config = ConfigDict(from_attributes=True) + data: dict[str, Any] + + +class ParameterisationRelationCreate( + ParameterisationRelationBase, + ScientificArtifactCreate, +): + pass + + +ParameterisationRelationUserUpdate = make_update_schema( + ParameterisationRelationCreate, + "ParameterisationRelationUserUpdate", +) # pyright: ignore [reportInvalidTypeForm] +ParameterisationRelationAdminUpdate = make_update_schema( + ParameterisationRelationCreate, + "ParameterisationRelationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + +class NestedParameterisationRelationRead( + ParameterisationRelationBase, + IdentifiableMixin, + EntityTypeMixin, +): + pass + + +class ParameterisationRelationRead( + ParameterisationRelationBase, + ScientificArtifactRead, +): + pass diff --git a/app/service/mapping.py b/app/service/mapping.py new file mode 100644 index 00000000..8eed393e --- /dev/null +++ b/app/service/mapping.py @@ -0,0 +1,190 @@ +import uuid +from typing import TYPE_CHECKING + +from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload + +from app.db.model import Mapping, Person, 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.mapping import MappingFilterDep +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, + router_user_delete_one, +) +from app.queries.factory import query_params_factory +from app.schemas.mapping import ( + MappingAdminUpdate, + MappingCreate, + MappingRead, + MappingUserUpdate, +) +from app.schemas.routers import DeleteResponse +from app.schemas.types import ListResponse, Select + +if TYPE_CHECKING: + from app.filters.base import Aliases + + +def _load(q: Select[Mapping]) -> Select[Mapping]: + db_model_class = Mapping + return q.options( + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.species), + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.strain), + joinedload(db_model_class.brain_region, innerjoin=True), + joinedload(db_model_class.created_by), + joinedload(db_model_class.updated_by), + joinedload(db_model_class.license), + selectinload(db_model_class.contributions), + selectinload(db_model_class.assets), + raiseload("*"), + ) + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MappingRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=Mapping, + user_context=user_context, + response_schema_class=MappingRead, + apply_operations=_load, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MappingAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MappingRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Mapping, + user_context=None, + json_model=json_model, + response_schema_class=MappingRead, + apply_operations=_load, + ) + + +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> MappingRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=Mapping, + user_context=None, + response_schema_class=MappingRead, + apply_operations=_load, + ) + + +def create_one( + user_context: UserContextWithProjectIdDep, + db: SessionDep, + json_model: MappingCreate, +) -> MappingRead: + return router_create_one( + db=db, + apply_operations=_load, + user_context=user_context, + json_model=json_model, + db_model_class=Mapping, + response_schema_class=MappingRead, + ) + + +def update_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, + json_model: MappingUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MappingRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Mapping, + user_context=user_context, + json_model=json_model, + response_schema_class=MappingRead, + apply_operations=_load, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: MappingFilterDep, + with_search: SearchDep, + in_brain_region: InBrainRegionDep, + facets: FacetsDep, +) -> ListResponse[MappingRead]: + subject_alias = aliased(Subject, flat=True) + aliases: Aliases = { + Subject: subject_alias, + Person: { + "created_by": aliased(Person, flat=True), + "updated_by": aliased(Person, flat=True), + }, + } + facet_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject.species", + "subject.strain", + ] + filter_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject", + "subject.species", + "subject.strain", + ] + name_to_facet_query_params, filter_joins = query_params_factory( + db_model_class=Mapping, + facet_keys=facet_keys, + filter_keys=filter_keys, + aliases=aliases, + ) + return router_read_many( + db=db, + db_model_class=Mapping, + authorized_project_id=user_context.project_id, + with_search=with_search, + with_in_brain_region=in_brain_region, + facets=facets, + aliases=aliases, + apply_data_query_operations=_load, + apply_filter_query_operations=None, + pagination_request=pagination_request, + response_schema_class=MappingRead, + name_to_facet_query_params=name_to_facet_query_params, + filter_model=filter_model, + filter_joins=filter_joins, + ) + + +def delete_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> DeleteResponse: + return router_user_delete_one( + id_=id_, + db=db, + db_model_class=Mapping, + user_context=user_context, + ) diff --git a/app/service/mapping_relation.py b/app/service/mapping_relation.py new file mode 100644 index 00000000..32a9be5e --- /dev/null +++ b/app/service/mapping_relation.py @@ -0,0 +1,198 @@ +import uuid +from typing import TYPE_CHECKING + +from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload + +from app.db.model import Mapping, MappingRelation, ParameterisationRelation, Person, 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.mapping_relation import MappingRelationFilterDep +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, + router_user_delete_one, +) +from app.queries.factory import query_params_factory +from app.schemas.mapping_relation import ( + MappingRelationAdminUpdate, + MappingRelationCreate, + MappingRelationRead, + MappingRelationUserUpdate, +) +from app.schemas.routers import DeleteResponse +from app.schemas.types import ListResponse, Select + +if TYPE_CHECKING: + from app.filters.base import Aliases + + +def _load(q: Select[MappingRelation]) -> Select[MappingRelation]: + db_model_class = MappingRelation + return q.options( + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.species), + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.strain), + joinedload(db_model_class.brain_region, innerjoin=True), + joinedload(db_model_class.created_by), + joinedload(db_model_class.updated_by), + joinedload(db_model_class.license), + joinedload(db_model_class.mapping), + joinedload(db_model_class.parameterisation_relation), + selectinload(db_model_class.contributions), + selectinload(db_model_class.assets), + raiseload("*"), + ) + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MappingRelationRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=MappingRelation, + user_context=user_context, + response_schema_class=MappingRelationRead, + apply_operations=_load, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MappingRelationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MappingRelationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MappingRelation, + user_context=None, + json_model=json_model, + response_schema_class=MappingRelationRead, + apply_operations=_load, + ) + + +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> MappingRelationRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=MappingRelation, + user_context=None, + response_schema_class=MappingRelationRead, + apply_operations=_load, + ) + + +def create_one( + user_context: UserContextWithProjectIdDep, + db: SessionDep, + json_model: MappingRelationCreate, +) -> MappingRelationRead: + return router_create_one( + db=db, + apply_operations=_load, + user_context=user_context, + json_model=json_model, + db_model_class=MappingRelation, + response_schema_class=MappingRelationRead, + ) + + +def update_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, + json_model: MappingRelationUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MappingRelationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MappingRelation, + user_context=user_context, + json_model=json_model, + response_schema_class=MappingRelationRead, + apply_operations=_load, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: MappingRelationFilterDep, + with_search: SearchDep, + in_brain_region: InBrainRegionDep, + facets: FacetsDep, +) -> ListResponse[MappingRelationRead]: + subject_alias = aliased(Subject, flat=True) + mapping_alias = aliased(Mapping, flat=True) + parameterisation_alias = aliased(ParameterisationRelation, flat=True) + aliases: Aliases = { + Subject: subject_alias, + Mapping: mapping_alias, + ParameterisationRelation: parameterisation_alias, + Person: { + "created_by": aliased(Person, flat=True), + "updated_by": aliased(Person, flat=True), + }, + } + facet_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject.species", + "subject.strain", + ] + filter_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject", + "subject.species", + "subject.strain", + "mapping", + "parameterisation_relation", + ] + name_to_facet_query_params, filter_joins = query_params_factory( + db_model_class=MappingRelation, + facet_keys=facet_keys, + filter_keys=filter_keys, + aliases=aliases, + ) + return router_read_many( + db=db, + db_model_class=MappingRelation, + authorized_project_id=user_context.project_id, + with_search=with_search, + with_in_brain_region=in_brain_region, + facets=facets, + aliases=aliases, + apply_data_query_operations=_load, + apply_filter_query_operations=None, + pagination_request=pagination_request, + response_schema_class=MappingRelationRead, + name_to_facet_query_params=name_to_facet_query_params, + filter_model=filter_model, + filter_joins=filter_joins, + ) + + +def delete_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> DeleteResponse: + return router_user_delete_one( + id_=id_, + db=db, + db_model_class=MappingRelation, + user_context=user_context, + ) diff --git a/app/service/parameterisation_relation.py b/app/service/parameterisation_relation.py new file mode 100644 index 00000000..5053b5ea --- /dev/null +++ b/app/service/parameterisation_relation.py @@ -0,0 +1,190 @@ +import uuid +from typing import TYPE_CHECKING + +from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload + +from app.db.model import ParameterisationRelation, Person, 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.parameterisation_relation import ParameterisationRelationFilterDep +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, + router_user_delete_one, +) +from app.queries.factory import query_params_factory +from app.schemas.parameterisation_relation import ( + ParameterisationRelationAdminUpdate, + ParameterisationRelationCreate, + ParameterisationRelationRead, + ParameterisationRelationUserUpdate, +) +from app.schemas.routers import DeleteResponse +from app.schemas.types import ListResponse, Select + +if TYPE_CHECKING: + from app.filters.base import Aliases + + +def _load(q: Select[ParameterisationRelation]) -> Select[ParameterisationRelation]: + db_model_class = ParameterisationRelation + return q.options( + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.species), + joinedload(db_model_class.subject, innerjoin=True).selectinload(Subject.strain), + joinedload(db_model_class.brain_region, innerjoin=True), + joinedload(db_model_class.created_by), + joinedload(db_model_class.updated_by), + joinedload(db_model_class.license), + selectinload(db_model_class.contributions), + selectinload(db_model_class.assets), + raiseload("*"), + ) + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ParameterisationRelationRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=ParameterisationRelation, + user_context=user_context, + response_schema_class=ParameterisationRelationRead, + apply_operations=_load, + ) + + +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> ParameterisationRelationRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=ParameterisationRelation, + user_context=None, + response_schema_class=ParameterisationRelationRead, + apply_operations=_load, + ) + + +def create_one( + user_context: UserContextWithProjectIdDep, + db: SessionDep, + json_model: ParameterisationRelationCreate, +) -> ParameterisationRelationRead: + return router_create_one( + db=db, + apply_operations=_load, + user_context=user_context, + json_model=json_model, + db_model_class=ParameterisationRelation, + response_schema_class=ParameterisationRelationRead, + ) + + +def update_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, + json_model: ParameterisationRelationUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ParameterisationRelationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ParameterisationRelation, + user_context=user_context, + json_model=json_model, + response_schema_class=ParameterisationRelationRead, + apply_operations=_load, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ParameterisationRelationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ParameterisationRelationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ParameterisationRelation, + user_context=None, + json_model=json_model, + response_schema_class=ParameterisationRelationRead, + apply_operations=_load, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: ParameterisationRelationFilterDep, + with_search: SearchDep, + in_brain_region: InBrainRegionDep, + facets: FacetsDep, +) -> ListResponse[ParameterisationRelationRead]: + subject_alias = aliased(Subject, flat=True) + aliases: Aliases = { + Subject: subject_alias, + Person: { + "created_by": aliased(Person, flat=True), + "updated_by": aliased(Person, flat=True), + }, + } + facet_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject.species", + "subject.strain", + ] + filter_keys = [ + "created_by", + "updated_by", + "brain_region", + "subject", + "subject.species", + "subject.strain", + ] + name_to_facet_query_params, filter_joins = query_params_factory( + db_model_class=ParameterisationRelation, + facet_keys=facet_keys, + filter_keys=filter_keys, + aliases=aliases, + ) + return router_read_many( + db=db, + db_model_class=ParameterisationRelation, + authorized_project_id=user_context.project_id, + with_search=with_search, + with_in_brain_region=in_brain_region, + facets=facets, + aliases=aliases, + apply_data_query_operations=_load, + apply_filter_query_operations=None, + pagination_request=pagination_request, + response_schema_class=ParameterisationRelationRead, + name_to_facet_query_params=name_to_facet_query_params, + filter_model=filter_model, + filter_joins=filter_joins, + ) + + +def delete_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> DeleteResponse: + return router_user_delete_one( + id_=id_, + db=db, + db_model_class=ParameterisationRelation, + user_context=user_context, + )