Skip to content

Commit feda937

Browse files
authored
[ENH] add annotation-analysis endpoint (#737)
* add annotation-analysis endpoint * add additional info to annotationanalyses * run black * fix loading procedure * do eager loading * do not run update for annotationanalysis as well * switch to main branch
1 parent c637bc3 commit feda937

File tree

12 files changed

+100
-44
lines changed

12 files changed

+100
-44
lines changed

store/neurostore/database.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55

66
def orjson_serializer(obj):
77
"""
8-
Note that `orjson.dumps()` return byte array,
9-
while sqlalchemy expects string, thus `decode()` call.
8+
Note that `orjson.dumps()` return byte array,
9+
while sqlalchemy expects string, thus `decode()` call.
1010
"""
11-
return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NAIVE_UTC).decode()
11+
return orjson.dumps(
12+
obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NAIVE_UTC
13+
).decode()
1214

1315

14-
db = SQLAlchemy(engine_options={
15-
"future": True,
16-
"json_serializer": orjson_serializer,
17-
"json_deserializer": orjson.loads,
18-
})
16+
db = SQLAlchemy(
17+
engine_options={
18+
"future": True,
19+
"json_serializer": orjson_serializer,
20+
"json_deserializer": orjson.loads,
21+
}
22+
)
1923
Base = declarative_base()
2024

2125

store/neurostore/models/data.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class Annotation(BaseMixin, db.Model):
115115
)
116116

117117

118-
class AnnotationAnalysis(db.Model):
118+
class AnnotationAnalysis(BaseMixin, db.Model):
119119
__tablename__ = "annotation_analyses"
120120
__table_args__ = (
121121
ForeignKeyConstraint(
@@ -126,22 +126,25 @@ class AnnotationAnalysis(db.Model):
126126
)
127127
__mapper_args__ = {"confirm_deleted_rows": False}
128128

129+
user_id = db.Column(db.Text, db.ForeignKey("users.external_id"), index=True)
129130
study_id = db.Column(db.Text, nullable=False)
130131
studyset_id = db.Column(db.Text, nullable=False)
131132
annotation_id = db.Column(
132133
db.Text,
133134
db.ForeignKey("annotations.id", ondelete="CASCADE"),
134135
index=True,
135-
primary_key=True,
136136
)
137137
analysis_id = db.Column(
138138
db.Text,
139139
db.ForeignKey("analyses.id", ondelete="CASCADE"),
140140
index=True,
141-
primary_key=True,
142141
)
143142
note = db.Column(MutableDict.as_mutable(JSONB))
144143

144+
user = relationship(
145+
"User", backref=backref("annotation_analyses", passive_deletes=True)
146+
)
147+
145148

146149
class BaseStudy(BaseMixin, db.Model):
147150
__tablename__ = "base_studies"

store/neurostore/openapi

store/neurostore/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .data import (
22
StudysetsView,
33
AnnotationsView,
4+
AnnotationAnalysesView,
45
BaseStudiesView,
56
StudiesView,
67
AnalysesView,
@@ -17,6 +18,7 @@
1718
__all__ = [
1819
"StudysetsView",
1920
"AnnotationsView",
21+
"AnnotationAnalysesView",
2022
"BaseStudiesView",
2123
"StudiesView",
2224
"AnalysesView",

store/neurostore/resources/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ def put(self, id):
472472

473473
try:
474474
self.update_base_studies(unique_ids.get("base-studies"))
475-
if self._model is not Annotation:
475+
if self._model is not Annotation and self._model is not AnnotationAnalysis:
476476
self.update_annotations(unique_ids.get("annotations"))
477477
except SQLAlchemyError as e:
478478
db.session.rollback()

store/neurostore/resources/data.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from ..schemas import (
3636
BooleanOrString,
3737
AnalysisConditionSchema,
38-
AnnotationAnalysisSchema,
3938
StudysetStudySchema,
4039
EntitySchema,
4140
)
@@ -44,6 +43,7 @@
4443
__all__ = [
4544
"StudysetsView",
4645
"AnnotationsView",
46+
"AnnotationAnalysesView",
4747
"BaseStudiesView",
4848
"StudiesView",
4949
"AnalysesView",
@@ -200,10 +200,10 @@ def serialize_records(self, records, args):
200200
@view_maker
201201
class AnnotationsView(ObjectView, ListView):
202202
_view_fields = {**LIST_CLONE_ARGS, "studyset_id": fields.String(load_default=None)}
203-
_o2m = {"annotation_analyses": "AnnotationAnalysesResource"}
203+
_o2m = {"annotation_analyses": "AnnotationAnalysesView"}
204204
_m2o = {"studyset": "StudysetsView"}
205205

206-
_nested = {"annotation_analyses": "AnnotationAnalysesResource"}
206+
_nested = {"annotation_analyses": "AnnotationAnalysesView"}
207207
_linked = {
208208
"studyset": "StudysetsView",
209209
}
@@ -255,7 +255,16 @@ def eager_load(self, q, args=None):
255255
selectinload(Annotation.user)
256256
.load_only(User.name, User.external_id)
257257
.options(raiseload("*", sql_only=True)),
258-
selectinload(Annotation.annotation_analyses).options(
258+
selectinload(Annotation.annotation_analyses)
259+
.load_only(
260+
AnnotationAnalysis.id,
261+
AnnotationAnalysis.analysis_id,
262+
AnnotationAnalysis.created_at,
263+
AnnotationAnalysis.study_id,
264+
AnnotationAnalysis.studyset_id,
265+
AnnotationAnalysis.annotation_id,
266+
)
267+
.options(
259268
joinedload(AnnotationAnalysis.analysis)
260269
.load_only(Analysis.id, Analysis.name)
261270
.options(raiseload("*", sql_only=True)),
@@ -339,8 +348,13 @@ def join_tables(self, q, args):
339348
def db_validation(self, record, data):
340349
db_analysis_ids = {aa.analysis_id for aa in record.annotation_analyses}
341350
data_analysis_ids = {
342-
aa["analysis"]["id"] for aa in data.get("annotation_analyses")
351+
aa.get("analysis", {}).get("id", "")
352+
for aa in data.get("annotation_analyses", [])
343353
}
354+
355+
if not data_analysis_ids:
356+
return
357+
344358
if db_analysis_ids != data_analysis_ids:
345359
abort(
346360
400,
@@ -779,7 +793,7 @@ class AnalysesView(ObjectView, ListView):
779793
"images": "ImagesView",
780794
"points": "PointsView",
781795
"analysis_conditions": "AnalysisConditionsResource",
782-
"annotation_analyses": "AnnotationAnalysesResource",
796+
"annotation_analyses": "AnnotationAnalysesView",
783797
}
784798
_m2o = {
785799
"study": "StudiesView",
@@ -794,7 +808,7 @@ class AnalysesView(ObjectView, ListView):
794808
"study": "StudiesView",
795809
}
796810
_linked = {
797-
"annotation_analyses": "AnnotationAnalysesResource",
811+
"annotation_analyses": "AnnotationAnalysesView",
798812
}
799813
_search_fields = ("name", "description")
800814

@@ -1087,20 +1101,8 @@ class PointValuesView(ObjectView, ListView):
10871101
}
10881102

10891103

1090-
# Utility resources for updating data
1091-
class AnalysisConditionsResource(BaseView):
1092-
_m2o = {
1093-
"analysis": "AnalysesView",
1094-
"condition": "ConditionsView",
1095-
}
1096-
_nested = {"condition": "ConditionsView"}
1097-
_parent = {"analysis": "AnalysesView"}
1098-
_model = AnalysisConditions
1099-
_schema = AnalysisConditionSchema
1100-
_composite_key = {}
1101-
1102-
1103-
class AnnotationAnalysesResource(BaseView):
1104+
@view_maker
1105+
class AnnotationAnalysesView(ObjectView, ListView):
11041106
_m2o = {
11051107
"annotation": "AnnotationsView",
11061108
"analysis": "AnalysesView",
@@ -1114,8 +1116,38 @@ class AnnotationAnalysesResource(BaseView):
11141116
"analysis": "AnalysesView",
11151117
"studyset_study": "StudysetStudiesResource",
11161118
}
1117-
_model = AnnotationAnalysis
1118-
_schema = AnnotationAnalysisSchema
1119+
1120+
def eager_load(self, q, args=None):
1121+
q = q.options(
1122+
joinedload(AnnotationAnalysis.analysis)
1123+
.load_only(Analysis.id, Analysis.name)
1124+
.options(raiseload("*", sql_only=True)),
1125+
joinedload(AnnotationAnalysis.studyset_study).options(
1126+
joinedload(StudysetStudy.study)
1127+
.load_only(
1128+
Study.id,
1129+
Study.name,
1130+
Study.year,
1131+
Study.authors,
1132+
Study.publication,
1133+
)
1134+
.options(raiseload("*", sql_only=True))
1135+
),
1136+
)
1137+
1138+
return q
1139+
1140+
1141+
# Utility resources for updating data
1142+
class AnalysisConditionsResource(BaseView):
1143+
_m2o = {
1144+
"analysis": "AnalysesView",
1145+
"condition": "ConditionsView",
1146+
}
1147+
_nested = {"condition": "ConditionsView"}
1148+
_parent = {"analysis": "AnalysesView"}
1149+
_model = AnalysisConditions
1150+
_schema = AnalysisConditionSchema
11191151
_composite_key = {}
11201152

11211153

store/neurostore/resources/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ def get_current_user():
2929

3030
def view_maker(cls):
3131
proc_name = cls.__name__.removesuffix("View").removesuffix("Resource")
32-
basename = singularize(proc_name, custom={"MetaAnalyses": "MetaAnalysis"})
32+
basename = singularize(
33+
proc_name,
34+
custom={
35+
"MetaAnalyses": "MetaAnalysis",
36+
"AnnotationAnalyses": "AnnotationAnalysis",
37+
},
38+
)
3339

3440
class ClassView(cls):
3541
_model = getattr(models, basename)

store/neurostore/schemas/data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ class Meta:
410410

411411

412412
class AnnotationAnalysisSchema(BaseSchema):
413+
id = fields.String(metadata={"info_field": True, "id_field": True})
413414
note = fields.Dict()
414415
annotation = StringOrNested("AnnotationSchema", load_only=True)
415416
analysis_id = fields.String(
@@ -436,7 +437,7 @@ class AnnotationAnalysisSchema(BaseSchema):
436437

437438
@post_load
438439
def add_id(self, data, **kwargs):
439-
if isinstance(data["analysis_id"], str):
440+
if isinstance(data.get("analysis_id"), str):
440441
data["analysis"] = {"id": data.pop("analysis_id")}
441442
if isinstance(data.get("study_id"), str) and isinstance(
442443
data.get("studyset_id"), str

store/neurostore/tests/api/test_base_studies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_post_list_of_studies(auth_client, ingest_neuroquery):
2828
"doi": "",
2929
"pmid": "",
3030
"name": "no ids",
31-
}
31+
},
3232
]
3333

3434
result = auth_client.post("/api/base-studies/", data=test_input)

store/neurostore/tests/api/test_crud.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
BaseStudy,
77
Study,
88
Annotation,
9+
AnnotationAnalysis,
910
Analysis,
1011
Condition,
1112
Image,
@@ -16,6 +17,7 @@
1617
BaseStudySchema,
1718
StudySchema,
1819
AnnotationSchema,
20+
AnnotationAnalysisSchema,
1921
AnalysisSchema,
2022
ConditionSchema,
2123
ImageSchema,
@@ -28,7 +30,7 @@
2830
"endpoint,model,schema",
2931
[
3032
("studysets", Studyset, StudysetSchema),
31-
# ("annotations", Annotation, AnnotationSchema), FIX
33+
("annotations", Annotation, AnnotationSchema),
3234
("base-studies", BaseStudy, BaseStudySchema),
3335
("studies", Study, StudySchema),
3436
("analyses", Analysis, AnalysisSchema),
@@ -74,6 +76,7 @@ def test_create(auth_client, user_data, endpoint, model, schema, session):
7476
[
7577
("studysets", Studyset, StudysetSchema),
7678
("annotations", Annotation, AnnotationSchema),
79+
("annotation-analyses", AnnotationAnalysis, AnnotationAnalysisSchema),
7780
("base-studies", BaseStudy, BaseStudySchema),
7881
("studies", Study, StudySchema),
7982
("analyses", Analysis, AnalysisSchema),
@@ -114,7 +117,13 @@ def test_read(auth_client, user_data, endpoint, model, schema, session):
114117
"endpoint,model,schema,update",
115118
[
116119
("studysets", Studyset, StudysetSchema, {"description": "mine"}),
117-
# ("annotations", Annotation, AnnotationSchema, {'description': 'mine'}), FIX
120+
("annotations", Annotation, AnnotationSchema, {"description": "mine"}),
121+
(
122+
"annotation-analyses",
123+
AnnotationAnalysis,
124+
AnnotationAnalysisSchema,
125+
{"note": {"new": "note"}},
126+
),
118127
("base-studies", BaseStudy, BaseStudySchema, {"description": "mine"}),
119128
("studies", Study, StudySchema, {"description": "mine"}),
120129
("analyses", Analysis, AnalysisSchema, {"description": "mine"}),

0 commit comments

Comments
 (0)