From 7287f5496b2c91ab932e33513a45f75490c8bd1c Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 10 Dec 2024 17:15:28 +0100 Subject: [PATCH] fix: add maps endpoint --- .../templates/_helpers.tpl | 1 + api/common/data_handlers/core/misc.py | 59 +++++++++++++++---- api/models/volumes/parcellationmap.py | 4 +- api/serialization/core/_concept.py | 2 +- api/serialization/volumes/parcellationmap.py | 12 ++-- api/server/volumes/__init__.py | 2 + api/server/volumes/maps.py | 43 ++++++++++++++ api/server/volumes/parcellationmap.py | 6 +- 8 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 api/server/volumes/maps.py diff --git a/.helm/siibra-api-v4-worker/templates/_helpers.tpl b/.helm/siibra-api-v4-worker/templates/_helpers.tpl index 3c1d2915..e992402e 100644 --- a/.helm/siibra-api-v4-worker/templates/_helpers.tpl +++ b/.helm/siibra-api-v4-worker/templates/_helpers.tpl @@ -11,6 +11,7 @@ Expand the name of the chart. {{- else -}} {{- printf "%s:%s" .Values.image.repository .Values.image.spec }} {{- end -}} +{{- end -}} {{/* diff --git a/api/common/data_handlers/core/misc.py b/api/common/data_handlers/core/misc.py index 437288d6..3cb07069 100644 --- a/api/common/data_handlers/core/misc.py +++ b/api/common/data_handlers/core/misc.py @@ -1,7 +1,14 @@ from api.common import data_decorator, get_filename, NotFound from api.models.volumes.volume import MapType from api.siibra_api_config import ROLE -from typing import Union, Dict, Tuple +from typing import Union, Dict, Tuple, List + +def parse_maptype(maptype: Union[MapType, str]): + if isinstance(maptype, MapType): + assert maptype.name == maptype.value, f"str enum, expecting .name and .value to equal" + return maptype.name + if isinstance(maptype, str): + return maptype @data_decorator(ROLE) def get_map(parcellation_id: str, space_id: str, maptype: Union[MapType, str]) -> Dict: @@ -22,13 +29,7 @@ def get_map(parcellation_id: str, space_id: str, maptype: Union[MapType, str]) - import siibra from api.serialization.util import instance_to_model - maptype_string = None - # check maptype name and value both matches - if isinstance(maptype, MapType): - assert maptype.name == maptype.value, f"str enum, expecting .name and .value to equal" - maptype_string = maptype.name - if isinstance(maptype, str): - maptype_string = maptype + maptype_string = parse_maptype(maptype) assert maptype_string is not None, f"maptype is neither MapType nor str" @@ -41,7 +42,7 @@ def get_map(parcellation_id: str, space_id: str, maptype: Union[MapType, str]) - raise NotFound return instance_to_model( - returned_map + returned_map, detail=True ).dict() @@ -213,4 +214,42 @@ def get_resampled_map(parcellation_id: str, space_id: str): "parcellation_id": parcellation_id, "space_id": space_id, }, indent="\t", fp=fp) - return full_filename, False \ No newline at end of file + return full_filename, False + + +@data_decorator(ROLE) +def get_filtered_maps(parcellation_id: str=None, space_id: str=None, maptype: Union[MapType, str, None]=None): + import siibra + from api.serialization.util import instance_to_model + + return_arr: List[siibra._parcellationmap.Map] = [] + for mp in siibra.maps: + mp: siibra._parcellationmap.Map = mp + if ( + parcellation_id is not None + and mp.parcellation.id != parcellation_id + ): + continue + if ( + space_id is not None + and mp.space.id != space_id + ): + continue + if ( + maptype is not None + and mp.maptype != parse_maptype(maptype) + ): + continue + return_arr.append(mp) + return [ instance_to_model(m).dict() for m in return_arr] + + +@data_decorator(ROLE) +def get_single_map(map_id: str): + import siibra + from api.serialization.util import instance_to_model + for mp in siibra.maps: + mp: siibra._parcellationmap.Map = mp + if mp.id == map_id: + return instance_to_model(mp, detail=True).dict() + raise NotFound(f"map with id {map_id} not found.") diff --git a/api/models/volumes/parcellationmap.py b/api/models/volumes/parcellationmap.py index 52ce25f3..17ae2e10 100644 --- a/api/models/volumes/parcellationmap.py +++ b/api/models/volumes/parcellationmap.py @@ -9,5 +9,7 @@ class MapModel(_SiibraAtlasConcept): species: str indices: Dict[str, List[MapIndexModel]] volumes: List[VolumeModel] + parcellation: SiibraAtIdModel + space: SiibraAtIdModel + maptype: str # affine: List[float] - \ No newline at end of file diff --git a/api/serialization/core/_concept.py b/api/serialization/core/_concept.py index 577e866d..d4c55455 100644 --- a/api/serialization/core/_concept.py +++ b/api/serialization/core/_concept.py @@ -3,7 +3,7 @@ from api.models.core._concept import SiibraAtlasConcept, SiibraPublication @serialize(AtlasConcept) -def atlasconcept_to_model(concept: AtlasConcept) -> SiibraAtlasConcept: +def atlasconcept_to_model(concept: AtlasConcept, **kwargs) -> SiibraAtlasConcept: """Serialize base concept. Args: diff --git a/api/serialization/volumes/parcellationmap.py b/api/serialization/volumes/parcellationmap.py index b3ea797b..987ceecb 100644 --- a/api/serialization/volumes/parcellationmap.py +++ b/api/serialization/volumes/parcellationmap.py @@ -1,9 +1,10 @@ from api.models.volumes.parcellationmap import MapModel +from api.models._commons import SiibraAtIdModel from api.serialization.util import serialize, instance_to_model from api.serialization.util.siibra import Map @serialize(Map, pass_super_model=True) -def map_to_model(map: Map, super_model_dict={}, **kwargs) -> MapModel: +def map_to_model(map: Map, super_model_dict={}, detail=False, **kwargs) -> MapModel: """Serialize map instance Args: @@ -15,10 +16,13 @@ def map_to_model(map: Map, super_model_dict={}, **kwargs) -> MapModel: return MapModel( **super_model_dict, species=str(map.species), + parcellation=SiibraAtIdModel(id=map.parcellation.id), + space=SiibraAtIdModel(id=map.space.id), + maptype=map.maptype.name, indices={ - regionname: instance_to_model(mapindex, **kwargs) + regionname: instance_to_model(mapindex, detail=detail, **kwargs) for regionname, mapindex in map._indices.items() - }, - volumes=[instance_to_model(v, **kwargs) for v in map.volumes], + } if detail else {}, + volumes=[instance_to_model(v, detail=detail, **kwargs) for v in map.volumes] if detail else [], # affine=map.affine.tolist() ) diff --git a/api/server/volumes/__init__.py b/api/server/volumes/__init__.py index 2ecb24fb..9465bd7c 100644 --- a/api/server/volumes/__init__.py +++ b/api/server/volumes/__init__.py @@ -1,6 +1,8 @@ from . import parcellationmap +from . import maps from ..const import PrefixedRouter prefixed_routers = ( PrefixedRouter(prefix="/map", router=parcellationmap.router), + PrefixedRouter(prefix="/maps", router=maps.router), ) diff --git a/api/server/volumes/maps.py b/api/server/volumes/maps.py new file mode 100644 index 00000000..3bb5891a --- /dev/null +++ b/api/server/volumes/maps.py @@ -0,0 +1,43 @@ +from typing import Union + +from pydantic import BaseModel +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse +from fastapi_versioning import version +from fastapi_pagination import Page, paginate + +from api.models.volumes.volume import MapType +from api.siibra_api_config import ROLE +from api.server import FASTAPI_VERSION, cache_header +from api.server.util import SapiCustomRoute +from api.models.volumes.parcellationmap import MapModel +from api.common import router_decorator, get_filename, logger, NotFound +from api.common.data_handlers.core.misc import ( + get_filtered_maps, + get_single_map, +) + +TAGS=["maps"] +"""HTTP map tags""" + +router = APIRouter(route_class=SapiCustomRoute, tags=TAGS) +"""HTTP map router""" + +@router.get("", response_model=Page[MapModel]) +@version(*FASTAPI_VERSION) +@router_decorator(ROLE, func=get_filtered_maps) +def filter_map(parcellation_id: str=None, space_id: str=None, map_type: Union[MapType, None]=None, *, func): + """Get a list of maps according to specification""" + if func is None: + raise HTTPException(500, f"func: None passsed") + return paginate(func(parcellation_id, space_id, map_type)) + + +@router.get("/{map_id:lazy_path}", response_model=MapModel) +@version(*FASTAPI_VERSION) +@router_decorator(ROLE, func=get_single_map) +def single_map(map_id: str, *, func): + """Get a list of maps according to specification""" + if func is None: + raise HTTPException(500, f"func: None passsed") + return func(map_id) diff --git a/api/server/volumes/parcellationmap.py b/api/server/volumes/parcellationmap.py index 79d085da..57da6edc 100644 --- a/api/server/volumes/parcellationmap.py +++ b/api/server/volumes/parcellationmap.py @@ -28,11 +28,13 @@ """HTTP map router""" # still use the old worker. New worker not stable (?) -@router.get("", response_model=MapModel) +@router.get("", response_model=MapModel, deprecated=True) @version(*FASTAPI_VERSION) @router_decorator(ROLE, func=old_get_map) def get_siibra_map(parcellation_id: str, space_id: str, map_type: MapType, name: str= "", *, func): - """Get map according to specification""" + """Get map according to specification. + + Deprecated. use /maps/{map_id} instead.""" if func is None: raise HTTPException(500, f"func: None passsed") return func(parcellation_id, space_id, map_type)