Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from app.db.session import configure_database_session_manager
from app.dependencies.common import forbid_extra_query_params
from app.errors import ApiError, ApiErrorCode
from app.graphql.router import graphql_router
from app.logger import L
from app.routers import router
from app.schemas.api import ErrorResponse
Expand Down Expand Up @@ -106,3 +107,4 @@ async def validation_exception_handler(
},
dependencies=[Depends(forbid_extra_query_params)],
)
app.include_router(graphql_router)
Empty file added app/graphql/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions app/graphql/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from app.graphql.filters import common, species, morphology # noqa: I001

__all__ = [
"common",
"morphology",
"species",
]
18 changes: 18 additions & 0 deletions app/graphql/filters/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import strawberry

from app.filters.common import AgentFilter, MTypeClassFilter, StrainFilter


@strawberry.experimental.pydantic.input(model=MTypeClassFilter, all_fields=True)
class MTypeClassFilterInput:
pass


@strawberry.experimental.pydantic.input(model=StrainFilter, all_fields=True)
class StrainFilterInput:
pass


@strawberry.experimental.pydantic.input(model=AgentFilter, all_fields=True)
class AgentFilterInput:
pass
8 changes: 8 additions & 0 deletions app/graphql/filters/morphology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import strawberry

from app.filters.morphology import MorphologyFilter


@strawberry.experimental.pydantic.input(model=MorphologyFilter, all_fields=True)
class MorphologyFilterInput:
pass
8 changes: 8 additions & 0 deletions app/graphql/filters/species.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import strawberry

from app.filters.common import SpeciesFilter


@strawberry.experimental.pydantic.input(model=SpeciesFilter, all_fields=True)
class SpeciesFilterInput:
pass
Empty file.
74 changes: 74 additions & 0 deletions app/graphql/resolvers/morphology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import uuid
from typing import TYPE_CHECKING

import strawberry

import app.service.morphology
from app.dependencies.auth import user_with_project_id
from app.filters.morphology import MorphologyFilter
from app.graphql.filters.morphology import MorphologyFilterInput
from app.graphql.types.morphology import MorphologyInput, MorphologyType
from app.graphql.types.pagination import ListResponseType, PaginationRequestInput
from app.schemas.morphology import ReconstructionMorphologyCreate, ReconstructionMorphologyRead
from app.schemas.types import PaginationRequest

if TYPE_CHECKING:
from sqlalchemy.orm import Session

from app.schemas.auth import UserContext, UserContextWithProjectId


@strawberry.type
class MorphologyQuery:
@strawberry.field
def read_many_morphologies(
self,
*,
info: strawberry.Info,
pagination_request: PaginationRequestInput,
morphology_filter: MorphologyFilterInput,
search: str | None = None,
with_facets: bool = False,
) -> ListResponseType[MorphologyType, ReconstructionMorphologyRead]:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
user_context: UserContext = info.context.user_context
validated_pagination_request = PaginationRequest.model_validate(
pagination_request, from_attributes=True
)
validated_morphology_filter = MorphologyFilter.model_validate(
morphology_filter, from_attributes=True
)
result = app.service.morphology.read_many(
user_context=user_context,
db=db,
pagination_request=validated_pagination_request,
morphology_filter=validated_morphology_filter,
search=search,
with_facets=with_facets,
)
return ListResponseType[MorphologyType, ReconstructionMorphologyRead].from_pydantic(result)

@strawberry.field
def read_morphology(self, id_: uuid.UUID, info: strawberry.Info) -> MorphologyType | None:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
user_context: UserContext = info.context.user_context
result = app.service.morphology.read_one(user_context=user_context, db=db, id_=id_)
return MorphologyType.from_pydantic(result) if result else None


@strawberry.type
class MorphologyMutation:
@strawberry.mutation
def create_morphology(
self, morphology: MorphologyInput, info: strawberry.Info
) -> MorphologyType:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
user_context: UserContextWithProjectId = user_with_project_id(info.context.user_context)
validated = ReconstructionMorphologyCreate.model_validate(morphology)
result = app.service.morphology.create_one(
user_context=user_context, db=db, reconstruction=validated
)
return MorphologyType.from_pydantic(result)
59 changes: 59 additions & 0 deletions app/graphql/resolvers/species.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import uuid
from typing import TYPE_CHECKING

import strawberry

import app.service.species
from app.filters.common import SpeciesFilter
from app.graphql.filters.species import SpeciesFilterInput
from app.graphql.types.common import SpeciesInput, SpeciesType
from app.graphql.types.pagination import ListResponseType, PaginationRequestInput
from app.schemas.base import SpeciesCreate, SpeciesRead
from app.schemas.types import PaginationRequest

if TYPE_CHECKING:
from sqlalchemy.orm import Session


@strawberry.type
class SpeciesQuery:
@strawberry.field
def read_many_species(
self,
*,
pagination_request: PaginationRequestInput,
species_filter: SpeciesFilterInput,
info: strawberry.Info,
) -> ListResponseType[SpeciesType, SpeciesRead]:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
validated_pagination_request = PaginationRequest.model_validate(
pagination_request, from_attributes=True
)
validated_species_filter = SpeciesFilter.model_validate(
species_filter, from_attributes=True
)
result = app.service.species.read_many(
db=db,
pagination_request=validated_pagination_request,
species_filter=validated_species_filter,
)
return ListResponseType[SpeciesType, SpeciesRead].from_pydantic(result)

@strawberry.field
def read_species(self, *, id_: uuid.UUID, info: strawberry.Info) -> SpeciesType | None:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
result = app.service.species.read_one(db=db, id_=id_)
return SpeciesType.from_pydantic(result) if result else None


@strawberry.type
class SpeciesMutation:
@strawberry.mutation
def create_species(self, *, species: SpeciesInput, info: strawberry.Info) -> SpeciesType:
# for proper validation, validate the input and return a pydantic model
db: Session = info.context.db
validated = SpeciesCreate.model_validate(species)
result = app.service.species.create_one(db=db, species=validated)
return SpeciesType.from_pydantic(result)
30 changes: 30 additions & 0 deletions app/graphql/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi import Depends
from sqlalchemy.orm import Session
from strawberry.fastapi import BaseContext, GraphQLRouter

from app.dependencies.auth import UserContextDep, user_with_service_admin_role
from app.dependencies.db import SessionDep
from app.graphql.schema import schema
from app.schemas.auth import UserContext


class Context(BaseContext):
def __init__(self, *, db: Session, user_context: UserContext) -> None:
"""Initialize a new Context."""
super().__init__()
self.db = db
self.user_context = user_context


def get_context(db: SessionDep, user_context: UserContextDep) -> Context:
return Context(db=db, user_context=user_context)


graphql_router = GraphQLRouter(
schema,
prefix="/graphql",
graphql_ide="apollo-sandbox",
context_getter=get_context,
dependencies=[Depends(user_with_service_admin_role)],
include_in_schema=False,
)
44 changes: 44 additions & 0 deletions app/graphql/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import strawberry
from graphql import GraphQLError
from strawberry.schema.config import StrawberryConfig
from strawberry.types import ExecutionContext

from app.graphql.resolvers.morphology import MorphologyMutation, MorphologyQuery
from app.graphql.resolvers.species import SpeciesMutation, SpeciesQuery


@strawberry.type
class Query(
MorphologyQuery,
SpeciesQuery,
):
pass


@strawberry.type
class Mutation(
MorphologyMutation,
SpeciesMutation,
):
pass


class CustomSchema(strawberry.Schema):
def process_errors(
self,
errors: list[GraphQLError],
execution_context: ExecutionContext | None = None,
) -> None:
for error in errors:
# temporary workaround to propagate the exception to the FastAPI handlers,
# although this does not follow the GraphQL specifications
if err := getattr(error, "original_error", None):
raise err
super().process_errors(errors, execution_context)


schema = CustomSchema(
query=Query,
mutation=Mutation,
config=StrawberryConfig(auto_camel_case=False),
)
10 changes: 10 additions & 0 deletions app/graphql/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from app.graphql.types import common, agent, annotation, asset, role, contribution # noqa: I001

__all__ = [
"agent",
"annotation",
"asset",
"common",
"contribution",
"role",
]
16 changes: 16 additions & 0 deletions app/graphql/types/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import strawberry

from app.schemas.agent import OrganizationRead, PersonRead


@strawberry.experimental.pydantic.type(model=PersonRead, all_fields=True)
class PersonReadType:
pass


@strawberry.experimental.pydantic.type(model=OrganizationRead, all_fields=True)
class OrganizationReadType:
pass


AgentReadType = PersonReadType | OrganizationReadType
8 changes: 8 additions & 0 deletions app/graphql/types/annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import strawberry

from app.schemas.annotation import Annotation


@strawberry.experimental.pydantic.type(model=Annotation, all_fields=True)
class AnnotationType:
pass
16 changes: 16 additions & 0 deletions app/graphql/types/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import strawberry

from app.schemas.asset import AssetRead


@strawberry.experimental.pydantic.type(model=AssetRead)
class AssetReadType:
id: strawberry.auto
status: strawberry.auto
path: strawberry.auto
full_path: strawberry.auto
is_directory: strawberry.auto
content_type: strawberry.auto
size: strawberry.auto
sha256_digest: strawberry.auto
meta: strawberry.scalars.JSON
45 changes: 45 additions & 0 deletions app/graphql/types/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import strawberry

from app.schemas.base import (
BrainRegionRead,
LicenseRead,
PointLocationBase,
SpeciesCreate,
SpeciesRead,
StrainRead,
)


@strawberry.experimental.pydantic.type(model=LicenseRead, all_fields=True)
class LicenseReadType:
pass


@strawberry.experimental.pydantic.type(model=PointLocationBase, all_fields=True)
class PointLocationBaseType:
pass


@strawberry.experimental.pydantic.input(model=PointLocationBase, all_fields=True)
class PointLocationBaseInput:
pass


@strawberry.experimental.pydantic.type(model=BrainRegionRead, all_fields=True)
class BrainRegionReadType:
pass


@strawberry.experimental.pydantic.type(model=StrainRead, all_fields=True)
class StrainReadType:
pass


@strawberry.experimental.pydantic.type(model=SpeciesRead, all_fields=True)
class SpeciesType:
pass


@strawberry.experimental.pydantic.input(model=SpeciesCreate, all_fields=True)
class SpeciesInput:
pass
13 changes: 13 additions & 0 deletions app/graphql/types/contribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import strawberry

from app.graphql.types.agent import AgentReadType
from app.schemas.contribution import ContributionReadWithoutEntity


@strawberry.experimental.pydantic.type(model=ContributionReadWithoutEntity)
class ContributionReadWithoutEntityType:
id: strawberry.auto
agent: AgentReadType
role: strawberry.auto
creation_date: strawberry.auto
update_date: strawberry.auto
13 changes: 13 additions & 0 deletions app/graphql/types/morphology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import strawberry

from app.schemas.morphology import ReconstructionMorphologyCreate, ReconstructionMorphologyRead


@strawberry.experimental.pydantic.type(model=ReconstructionMorphologyRead, all_fields=True)
class MorphologyType:
pass


@strawberry.experimental.pydantic.input(model=ReconstructionMorphologyCreate, all_fields=True)
class MorphologyInput:
pass
Loading