Skip to content

Commit adcae06

Browse files
jdcourcolJean-Denis Courcoleleftherioszisis
authored
validation result model (#171)
* Add validation result model * Fix migration, update tests, filters, service --------- Co-authored-by: Jean-Denis Courcol <[email protected]> Co-authored-by: Eleftherios Zisis <[email protected]>
1 parent b6627f7 commit adcae06

File tree

10 files changed

+425
-1
lines changed

10 files changed

+425
-1
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Default migration message
2+
3+
Revision ID: 9730c55381f3
4+
Revises: b8490e92310f
5+
Create Date: 2025-05-21 13:18:02.105223
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
from alembic_postgresql_enum import TableReference
14+
15+
from sqlalchemy import Text
16+
import app.db.types
17+
18+
# revision identifiers, used by Alembic.
19+
revision: str = "9730c55381f3"
20+
down_revision: Union[str, None] = "b8490e92310f"
21+
branch_labels: Union[str, Sequence[str], None] = None
22+
depends_on: Union[str, Sequence[str], None] = None
23+
24+
25+
def upgrade() -> None:
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.create_table(
28+
"validation_result",
29+
sa.Column("id", sa.Uuid(), nullable=False),
30+
sa.Column("passed", sa.Boolean(), nullable=False),
31+
sa.Column("name", sa.String(), nullable=False),
32+
sa.Column("validated_entity_id", sa.Uuid(), nullable=False),
33+
sa.ForeignKeyConstraint(["id"], ["entity.id"], name=op.f("fk_validation_result_id_entity")),
34+
sa.ForeignKeyConstraint(
35+
["validated_entity_id"],
36+
["entity.id"],
37+
name=op.f("fk_validation_result_validated_entity_id_entity"),
38+
),
39+
sa.PrimaryKeyConstraint("id", name=op.f("pk_validation_result")),
40+
)
41+
op.create_index(op.f("ix_validation_result_name"), "validation_result", ["name"], unique=False)
42+
op.create_index(
43+
op.f("ix_validation_result_validated_entity_id"),
44+
"validation_result",
45+
["validated_entity_id"],
46+
unique=False,
47+
)
48+
op.sync_enum_values(
49+
enum_schema="public",
50+
enum_name="entitytype",
51+
new_values=[
52+
"analysis_software_source_code",
53+
"brain_atlas",
54+
"emodel",
55+
"cell_composition",
56+
"experimental_bouton_density",
57+
"experimental_neuron_density",
58+
"experimental_synapses_per_connection",
59+
"memodel",
60+
"mesh",
61+
"me_type_density",
62+
"reconstruction_morphology",
63+
"electrical_cell_recording",
64+
"electrical_recording_stimulus",
65+
"single_neuron_simulation",
66+
"single_neuron_synaptome",
67+
"single_neuron_synaptome_simulation",
68+
"ion_channel_model",
69+
"subject",
70+
"validation_result",
71+
],
72+
affected_columns=[
73+
TableReference(table_schema="public", table_name="entity", column_name="type")
74+
],
75+
enum_values_to_rename=[],
76+
)
77+
# ### end Alembic commands ###
78+
79+
80+
def downgrade() -> None:
81+
# ### commands auto generated by Alembic - please adjust! ###
82+
op.sync_enum_values(
83+
enum_schema="public",
84+
enum_name="entitytype",
85+
new_values=[
86+
"analysis_software_source_code",
87+
"brain_atlas",
88+
"emodel",
89+
"cell_composition",
90+
"experimental_bouton_density",
91+
"experimental_neuron_density",
92+
"experimental_synapses_per_connection",
93+
"memodel",
94+
"mesh",
95+
"me_type_density",
96+
"reconstruction_morphology",
97+
"electrical_cell_recording",
98+
"electrical_recording_stimulus",
99+
"single_neuron_simulation",
100+
"single_neuron_synaptome",
101+
"single_neuron_synaptome_simulation",
102+
"ion_channel_model",
103+
"subject",
104+
],
105+
affected_columns=[
106+
TableReference(table_schema="public", table_name="entity", column_name="type")
107+
],
108+
enum_values_to_rename=[],
109+
)
110+
op.drop_index(op.f("ix_validation_result_validated_entity_id"), table_name="validation_result")
111+
op.drop_index(op.f("ix_validation_result_name"), table_name="validation_result")
112+
op.drop_table("validation_result")
113+
# ### end Alembic commands ###

app/db/model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,26 @@ class IonChannelModelToEModel(Base):
821821
)
822822

823823

824+
class ValidationResult(Entity):
825+
__tablename__ = EntityType.validation_result.value
826+
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True)
827+
passed: Mapped[bool] = mapped_column(default=False)
828+
829+
name: Mapped[str] = mapped_column(index=True)
830+
831+
validated_entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True)
832+
validated_entity: Mapped[Entity] = relationship(
833+
"Entity",
834+
uselist=False,
835+
foreign_keys=[validated_entity_id],
836+
)
837+
838+
__mapper_args__ = { # noqa: RUF012
839+
"polymorphic_identity": __tablename__,
840+
"inherit_condition": id == Entity.id,
841+
}
842+
843+
824844
class Asset(Identifiable):
825845
"""Asset table."""
826846

app/db/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class EntityType(StrEnum):
6666
single_neuron_synaptome_simulation = auto()
6767
ion_channel_model = auto()
6868
subject = auto()
69+
validation_result = auto()
6970

7071

7172
class AgentType(StrEnum):

app/filters/validation_result.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import uuid
2+
from typing import Annotated
3+
4+
from fastapi_filter import FilterDepends
5+
6+
from app.db.model import ValidationResult
7+
from app.filters.base import CustomFilter
8+
from app.filters.common import EntityFilterMixin
9+
10+
11+
class ValidationResultFilter(
12+
CustomFilter,
13+
EntityFilterMixin,
14+
):
15+
passed: bool | None = None
16+
validated_entity_id: uuid.UUID | None = None
17+
18+
order_by: list[str] = ["name"] # noqa: RUF012
19+
20+
class Constants(CustomFilter.Constants):
21+
model = ValidationResult
22+
ordering_model_fields = ["name"] # noqa: RUF012
23+
24+
25+
ValidationResultFilterDep = Annotated[ValidationResultFilter, FilterDepends(ValidationResultFilter)]

app/routers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
species,
3232
strain,
3333
subject,
34+
validation_result,
3435
)
3536

3637
router = APIRouter()
@@ -47,6 +48,7 @@
4748
experimental_bouton_density.router,
4849
experimental_neuron_density.router,
4950
experimental_synapses_per_connection.router,
51+
ion_channel_model.router,
5052
license.router,
5153
measurement_annotation.router,
5254
memodel.router,
@@ -61,7 +63,7 @@
6163
species.router,
6264
strain.router,
6365
subject.router,
64-
ion_channel_model.router,
66+
validation_result.router,
6567
]
6668
for r in authenticated_routers:
6769
router.include_router(r, dependencies=[Depends(user_verified)])

app/routers/validation_result.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from fastapi import APIRouter
2+
3+
import app.service.validation_result
4+
5+
router = APIRouter(
6+
prefix="/validation-result",
7+
tags=["validation-result"],
8+
)
9+
10+
read_many = router.get("")(app.service.validation_result.read_many)
11+
read_one = router.get("/{id_}")(app.service.validation_result.read_one)
12+
create_one = router.post("")(app.service.validation_result.create_one)

app/schemas/validation.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import uuid
2+
3+
from pydantic import BaseModel
4+
5+
from app.schemas.agent import CreatedByUpdatedByMixin
6+
from app.schemas.base import (
7+
CreationMixin,
8+
IdentifiableMixin,
9+
)
10+
11+
12+
class ValidationResultBase(BaseModel):
13+
name: str
14+
passed: bool
15+
validated_entity_id: uuid.UUID
16+
17+
18+
class ValidationResultRead(
19+
ValidationResultBase, CreationMixin, IdentifiableMixin, CreatedByUpdatedByMixin
20+
):
21+
pass
22+
23+
24+
class ValidationResultCreate(ValidationResultBase):
25+
pass

app/service/validation_result.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import uuid
2+
3+
import sqlalchemy as sa
4+
from sqlalchemy.orm import joinedload
5+
6+
from app.db.model import Subject, ValidationResult
7+
from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep
8+
from app.dependencies.common import (
9+
FacetsDep,
10+
InBrainRegionDep,
11+
PaginationQuery,
12+
SearchDep,
13+
)
14+
from app.dependencies.db import SessionDep
15+
from app.filters.validation_result import ValidationResultFilterDep
16+
from app.queries.common import router_create_one, router_read_many, router_read_one
17+
from app.schemas.types import ListResponse
18+
from app.schemas.validation import ValidationResultCreate, ValidationResultRead
19+
20+
21+
def _load(query: sa.Select):
22+
return query.options(
23+
joinedload(Subject.species),
24+
)
25+
26+
27+
def read_one(
28+
user_context: UserContextDep,
29+
db: SessionDep,
30+
id_: uuid.UUID,
31+
) -> ValidationResultRead:
32+
return router_read_one(
33+
db=db,
34+
id_=id_,
35+
db_model_class=ValidationResult,
36+
authorized_project_id=user_context.project_id,
37+
response_schema_class=ValidationResultRead,
38+
apply_operations=_load,
39+
)
40+
41+
42+
def create_one(
43+
user_context: UserContextWithProjectIdDep,
44+
json_model: ValidationResultCreate,
45+
db: SessionDep,
46+
) -> ValidationResultRead:
47+
return router_create_one(
48+
db=db,
49+
user_context=user_context,
50+
db_model_class=ValidationResult,
51+
json_model=json_model,
52+
response_schema_class=ValidationResultRead,
53+
)
54+
55+
56+
def read_many(
57+
user_context: UserContextDep,
58+
db: SessionDep,
59+
pagination_request: PaginationQuery,
60+
filter_model: ValidationResultFilterDep,
61+
with_search: SearchDep,
62+
facets: FacetsDep,
63+
in_brain_region: InBrainRegionDep,
64+
) -> ListResponse[ValidationResultRead]:
65+
aliases = {}
66+
name_to_facet_query_params = {}
67+
return router_read_many(
68+
db=db,
69+
filter_model=filter_model,
70+
db_model_class=ValidationResult,
71+
with_search=with_search,
72+
with_in_brain_region=in_brain_region,
73+
facets=facets,
74+
name_to_facet_query_params=name_to_facet_query_params,
75+
apply_filter_query_operations=None,
76+
apply_data_query_operations=_load,
77+
aliases=aliases,
78+
pagination_request=pagination_request,
79+
response_schema_class=ValidationResultRead,
80+
authorized_project_id=user_context.project_id,
81+
filter_joins=None,
82+
)

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,20 @@ def mtype_class_id(db):
372372
)
373373

374374

375+
@pytest.fixture
376+
def validation_result_id(client, morphology_id):
377+
return assert_request(
378+
client.post,
379+
url="/validation-result",
380+
json={
381+
"name": "test_validation_result",
382+
"passed": True,
383+
"validated_entity_id": str(morphology_id),
384+
"authorized_public": False,
385+
},
386+
).json()["id"]
387+
388+
375389
CreateIds = Callable[[int], list[str]]
376390

377391

0 commit comments

Comments
 (0)