Skip to content

Commit

Permalink
[ENH] add annotation-analysis endpoint (#737)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jdkent authored Mar 13, 2024
1 parent c637bc3 commit feda937
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 44 deletions.
20 changes: 12 additions & 8 deletions store/neurostore/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@

def orjson_serializer(obj):
"""
Note that `orjson.dumps()` return byte array,
while sqlalchemy expects string, thus `decode()` call.
Note that `orjson.dumps()` return byte array,
while sqlalchemy expects string, thus `decode()` call.
"""
return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NAIVE_UTC).decode()
return orjson.dumps(
obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NAIVE_UTC
).decode()


db = SQLAlchemy(engine_options={
"future": True,
"json_serializer": orjson_serializer,
"json_deserializer": orjson.loads,
})
db = SQLAlchemy(
engine_options={
"future": True,
"json_serializer": orjson_serializer,
"json_deserializer": orjson.loads,
}
)
Base = declarative_base()


Expand Down
9 changes: 6 additions & 3 deletions store/neurostore/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class Annotation(BaseMixin, db.Model):
)


class AnnotationAnalysis(db.Model):
class AnnotationAnalysis(BaseMixin, db.Model):
__tablename__ = "annotation_analyses"
__table_args__ = (
ForeignKeyConstraint(
Expand All @@ -126,22 +126,25 @@ class AnnotationAnalysis(db.Model):
)
__mapper_args__ = {"confirm_deleted_rows": False}

user_id = db.Column(db.Text, db.ForeignKey("users.external_id"), index=True)
study_id = db.Column(db.Text, nullable=False)
studyset_id = db.Column(db.Text, nullable=False)
annotation_id = db.Column(
db.Text,
db.ForeignKey("annotations.id", ondelete="CASCADE"),
index=True,
primary_key=True,
)
analysis_id = db.Column(
db.Text,
db.ForeignKey("analyses.id", ondelete="CASCADE"),
index=True,
primary_key=True,
)
note = db.Column(MutableDict.as_mutable(JSONB))

user = relationship(
"User", backref=backref("annotation_analyses", passive_deletes=True)
)


class BaseStudy(BaseMixin, db.Model):
__tablename__ = "base_studies"
Expand Down
2 changes: 1 addition & 1 deletion store/neurostore/openapi
Submodule openapi updated 1 files
+99 −2 neurostore-openapi.yml
2 changes: 2 additions & 0 deletions store/neurostore/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .data import (
StudysetsView,
AnnotationsView,
AnnotationAnalysesView,
BaseStudiesView,
StudiesView,
AnalysesView,
Expand All @@ -17,6 +18,7 @@
__all__ = [
"StudysetsView",
"AnnotationsView",
"AnnotationAnalysesView",
"BaseStudiesView",
"StudiesView",
"AnalysesView",
Expand Down
2 changes: 1 addition & 1 deletion store/neurostore/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def put(self, id):

try:
self.update_base_studies(unique_ids.get("base-studies"))
if self._model is not Annotation:
if self._model is not Annotation and self._model is not AnnotationAnalysis:
self.update_annotations(unique_ids.get("annotations"))
except SQLAlchemyError as e:
db.session.rollback()
Expand Down
78 changes: 55 additions & 23 deletions store/neurostore/resources/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from ..schemas import (
BooleanOrString,
AnalysisConditionSchema,
AnnotationAnalysisSchema,
StudysetStudySchema,
EntitySchema,
)
Expand All @@ -44,6 +43,7 @@
__all__ = [
"StudysetsView",
"AnnotationsView",
"AnnotationAnalysesView",
"BaseStudiesView",
"StudiesView",
"AnalysesView",
Expand Down Expand Up @@ -200,10 +200,10 @@ def serialize_records(self, records, args):
@view_maker
class AnnotationsView(ObjectView, ListView):
_view_fields = {**LIST_CLONE_ARGS, "studyset_id": fields.String(load_default=None)}
_o2m = {"annotation_analyses": "AnnotationAnalysesResource"}
_o2m = {"annotation_analyses": "AnnotationAnalysesView"}
_m2o = {"studyset": "StudysetsView"}

_nested = {"annotation_analyses": "AnnotationAnalysesResource"}
_nested = {"annotation_analyses": "AnnotationAnalysesView"}
_linked = {
"studyset": "StudysetsView",
}
Expand Down Expand Up @@ -255,7 +255,16 @@ def eager_load(self, q, args=None):
selectinload(Annotation.user)
.load_only(User.name, User.external_id)
.options(raiseload("*", sql_only=True)),
selectinload(Annotation.annotation_analyses).options(
selectinload(Annotation.annotation_analyses)
.load_only(
AnnotationAnalysis.id,
AnnotationAnalysis.analysis_id,
AnnotationAnalysis.created_at,
AnnotationAnalysis.study_id,
AnnotationAnalysis.studyset_id,
AnnotationAnalysis.annotation_id,
)
.options(
joinedload(AnnotationAnalysis.analysis)
.load_only(Analysis.id, Analysis.name)
.options(raiseload("*", sql_only=True)),
Expand Down Expand Up @@ -339,8 +348,13 @@ def join_tables(self, q, args):
def db_validation(self, record, data):
db_analysis_ids = {aa.analysis_id for aa in record.annotation_analyses}
data_analysis_ids = {
aa["analysis"]["id"] for aa in data.get("annotation_analyses")
aa.get("analysis", {}).get("id", "")
for aa in data.get("annotation_analyses", [])
}

if not data_analysis_ids:
return

if db_analysis_ids != data_analysis_ids:
abort(
400,
Expand Down Expand Up @@ -779,7 +793,7 @@ class AnalysesView(ObjectView, ListView):
"images": "ImagesView",
"points": "PointsView",
"analysis_conditions": "AnalysisConditionsResource",
"annotation_analyses": "AnnotationAnalysesResource",
"annotation_analyses": "AnnotationAnalysesView",
}
_m2o = {
"study": "StudiesView",
Expand All @@ -794,7 +808,7 @@ class AnalysesView(ObjectView, ListView):
"study": "StudiesView",
}
_linked = {
"annotation_analyses": "AnnotationAnalysesResource",
"annotation_analyses": "AnnotationAnalysesView",
}
_search_fields = ("name", "description")

Expand Down Expand Up @@ -1087,20 +1101,8 @@ class PointValuesView(ObjectView, ListView):
}


# Utility resources for updating data
class AnalysisConditionsResource(BaseView):
_m2o = {
"analysis": "AnalysesView",
"condition": "ConditionsView",
}
_nested = {"condition": "ConditionsView"}
_parent = {"analysis": "AnalysesView"}
_model = AnalysisConditions
_schema = AnalysisConditionSchema
_composite_key = {}


class AnnotationAnalysesResource(BaseView):
@view_maker
class AnnotationAnalysesView(ObjectView, ListView):
_m2o = {
"annotation": "AnnotationsView",
"analysis": "AnalysesView",
Expand All @@ -1114,8 +1116,38 @@ class AnnotationAnalysesResource(BaseView):
"analysis": "AnalysesView",
"studyset_study": "StudysetStudiesResource",
}
_model = AnnotationAnalysis
_schema = AnnotationAnalysisSchema

def eager_load(self, q, args=None):
q = q.options(
joinedload(AnnotationAnalysis.analysis)
.load_only(Analysis.id, Analysis.name)
.options(raiseload("*", sql_only=True)),
joinedload(AnnotationAnalysis.studyset_study).options(
joinedload(StudysetStudy.study)
.load_only(
Study.id,
Study.name,
Study.year,
Study.authors,
Study.publication,
)
.options(raiseload("*", sql_only=True))
),
)

return q


# Utility resources for updating data
class AnalysisConditionsResource(BaseView):
_m2o = {
"analysis": "AnalysesView",
"condition": "ConditionsView",
}
_nested = {"condition": "ConditionsView"}
_parent = {"analysis": "AnalysesView"}
_model = AnalysisConditions
_schema = AnalysisConditionSchema
_composite_key = {}


Expand Down
8 changes: 7 additions & 1 deletion store/neurostore/resources/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ def get_current_user():

def view_maker(cls):
proc_name = cls.__name__.removesuffix("View").removesuffix("Resource")
basename = singularize(proc_name, custom={"MetaAnalyses": "MetaAnalysis"})
basename = singularize(
proc_name,
custom={
"MetaAnalyses": "MetaAnalysis",
"AnnotationAnalyses": "AnnotationAnalysis",
},
)

class ClassView(cls):
_model = getattr(models, basename)
Expand Down
3 changes: 2 additions & 1 deletion store/neurostore/schemas/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ class Meta:


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

@post_load
def add_id(self, data, **kwargs):
if isinstance(data["analysis_id"], str):
if isinstance(data.get("analysis_id"), str):
data["analysis"] = {"id": data.pop("analysis_id")}
if isinstance(data.get("study_id"), str) and isinstance(
data.get("studyset_id"), str
Expand Down
2 changes: 1 addition & 1 deletion store/neurostore/tests/api/test_base_studies.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_post_list_of_studies(auth_client, ingest_neuroquery):
"doi": "",
"pmid": "",
"name": "no ids",
}
},
]

result = auth_client.post("/api/base-studies/", data=test_input)
Expand Down
13 changes: 11 additions & 2 deletions store/neurostore/tests/api/test_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
BaseStudy,
Study,
Annotation,
AnnotationAnalysis,
Analysis,
Condition,
Image,
Expand All @@ -16,6 +17,7 @@
BaseStudySchema,
StudySchema,
AnnotationSchema,
AnnotationAnalysisSchema,
AnalysisSchema,
ConditionSchema,
ImageSchema,
Expand All @@ -28,7 +30,7 @@
"endpoint,model,schema",
[
("studysets", Studyset, StudysetSchema),
# ("annotations", Annotation, AnnotationSchema), FIX
("annotations", Annotation, AnnotationSchema),
("base-studies", BaseStudy, BaseStudySchema),
("studies", Study, StudySchema),
("analyses", Analysis, AnalysisSchema),
Expand Down Expand Up @@ -74,6 +76,7 @@ def test_create(auth_client, user_data, endpoint, model, schema, session):
[
("studysets", Studyset, StudysetSchema),
("annotations", Annotation, AnnotationSchema),
("annotation-analyses", AnnotationAnalysis, AnnotationAnalysisSchema),
("base-studies", BaseStudy, BaseStudySchema),
("studies", Study, StudySchema),
("analyses", Analysis, AnalysisSchema),
Expand Down Expand Up @@ -114,7 +117,13 @@ def test_read(auth_client, user_data, endpoint, model, schema, session):
"endpoint,model,schema,update",
[
("studysets", Studyset, StudysetSchema, {"description": "mine"}),
# ("annotations", Annotation, AnnotationSchema, {'description': 'mine'}), FIX
("annotations", Annotation, AnnotationSchema, {"description": "mine"}),
(
"annotation-analyses",
AnnotationAnalysis,
AnnotationAnalysisSchema,
{"note": {"new": "note"}},
),
("base-studies", BaseStudy, BaseStudySchema, {"description": "mine"}),
("studies", Study, StudySchema, {"description": "mine"}),
("analyses", Analysis, AnalysisSchema, {"description": "mine"}),
Expand Down
4 changes: 1 addition & 3 deletions store/neurostore/tests/api/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ def test_mass_creation(auth_client, session):
"analyses": [
{
"name": f"analysis{i}",
"points": [
{"x": 0, "y": 0, "z": 0, "space": "mni", "order": 1}
],
"points": [{"x": 0, "y": 0, "z": 0, "space": "mni", "order": 1}],
}
],
}
Expand Down
1 change: 1 addition & 0 deletions store/neurostore/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ def user_data(session, mock_add_users):
annotation=annotation,
analysis=analysis,
note={"food": "bar"},
user=user,
)
annotation.annotation_analyses.append(aa)

Expand Down

0 comments on commit feda937

Please sign in to comment.