From 08d86d7c0d9f7d42cddccd79d2ce95dc0f1ba43f Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Wed, 10 Jan 2024 16:35:10 -0500 Subject: [PATCH 1/5] Added views endpoints --- pephub/routers/api/v1/project.py | 240 ++++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 2 deletions(-) diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index f2daf073..a9aac23c 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -2,6 +2,7 @@ import yaml import pandas as pd import peppy +import logging from typing import Callable, Literal, Union, Optional, List, Annotated from fastapi import APIRouter, Depends, Query, Body from fastapi.exceptions import HTTPException @@ -17,8 +18,12 @@ ) from pepdbagent import PEPDatabaseAgent -from pepdbagent.exceptions import ProjectUniqueNameError -from pepdbagent.models import AnnotationModel, AnnotationList +from pepdbagent.exceptions import ( + ProjectUniqueNameError, + SampleAlreadyInView, + SampleNotFoundError, +) +from pepdbagent.models import AnnotationModel, AnnotationList, CreateViewDictModel from dotenv import load_dotenv @@ -36,6 +41,7 @@ ) from ....const import SAMPLE_CONVERSION_FUNCTIONS, VALID_UPDATE_KEYS +_LOGGER = logging.getLogger(__name__) load_dotenv() @@ -739,3 +745,233 @@ async def get_project_annotation( Get project annotation from a certain project and namespace """ return proj_annotation + + +#### Views #### +@project.get( + "/views", + summary="get list of views for a project", + tags=["views"], + response_model=List[str], # TODO: rethink response model +) +def get_views( + namespace: str, + project: str, + tag: str = DEFAULT_TAG, + agent: PEPDatabaseAgent = Depends(get_db), +): + return agent.annotation.get_views(namespace, project, tag=tag) + + +@project.get( + "/{view}", + summary="Fetch a project view", + response_model=ProjectRawModel, + tags=["views"], +) +async def get_view_of_the_project( + namespace: str, + project: str, + view: str, + tag: str = DEFAULT_TAG, + raw: bool = True, + agent: PEPDatabaseAgent = Depends(get_db), +): + """ + Fetch a view of the project. + """ + if raw: + return ProjectRawModel( + **agent.view.get( + namespace=namespace, + name=project, + view_name=view, + tag=tag, + raw=raw, + ) + ) + else: + return ProjectRawModel( + **agent.view.get( + namespace=namespace, + name=project, + view_name=view, + tag=tag, + raw=raw, + ).to_dict() + ) + + +@project.post( + "/{view}", + summary="Create a view", + tags=["views"], +) +async def create_view_of_the_project( + namespace: str, + project: str, + view: str, + tag: str = DEFAULT_TAG, + description: str = "", + sample_names: List[str] = None, + namespace_access_list: List[str] = Depends(get_namespace_access_list), + agent: PEPDatabaseAgent = Depends(get_db), +): + """ + Create a view of the project. + """ + # if namespace not in namespace_access_list: + # raise HTTPException( + # detail="You do not have permission to create this view.", + # status_code=401, + # ) + try: + agent.view.create( + view_name=view, + description=description, + view_dict=CreateViewDictModel( + project_namespace=namespace, + project_name=project, + project_tag=tag, + sample_list=sample_names, + ), + ) + except Exception as e: + _LOGGER.error(f"Could not create view. Error: {e}") + raise HTTPException( + status_code=400, + detail=f"Could not create view. Server error", + ) + return JSONResponse( + content={ + "message": "View created successfully.", + "registry": f"{namespace}/{project}:{tag}", + }, + status_code=202, + ) + + +@project.post( + "/{view}/{sample_name}", + summary="Add sample to the view", + tags=["views"], +) +async def add_sample_to_view( + namespace: str, + project: str, + view: str, + sample_name: str, + tag: str = DEFAULT_TAG, + namespace_access_list: List[str] = Depends(get_namespace_access_list), + agent: PEPDatabaseAgent = Depends(get_db), +): + # if namespace not in namespace_access_list: + # raise HTTPException( + # detail="You do not have permission to add sample to this view.", + # status_code=401, + # ) + try: + agent.view.add_sample( + namespace=namespace, + name=project, + tag=tag, + view_name=view, + sample_name=sample_name, + ) + except SampleAlreadyInView: + raise HTTPException( + status_code=400, + detail=f"Sample '{sample_name}' already in view '{view}'", + ) + except SampleNotFoundError: + raise HTTPException( + status_code=400, + detail=f"Sample '{sample_name}' not found in project '{namespace}/{project}:{tag}'", + ) + return JSONResponse( + content={ + "message": "Sample added to view successfully.", + "registry": f"{namespace}/{project}:{tag}", + }, + status_code=202, + ) + + +@project.delete( + "/{view}/{sample_name}", + summary="Delete sample from the view", + tags=["views"], +) +def delete_sample_from_view( + namespace: str, + project: str, + view: str, + sample_name: str, + tag: str = DEFAULT_TAG, + namespace_access_list: List[str] = Depends(get_namespace_access_list), + agent: PEPDatabaseAgent = Depends(get_db), +): + # if namespace not in namespace_access_list: + # raise HTTPException( + # detail="You do not have permission to delete sample from this view.", + # status_code=401, + # ) + try: + agent.view.remove_sample( + namespace=namespace, + name=project, + tag=tag, + view_name=view, + sample_name=sample_name, + ) + except SampleNotFoundError: + raise HTTPException( + status_code=400, + detail=f"Sample '{sample_name}' not found in view '{view}'", + ) + return JSONResponse( + content={ + "message": "Sample deleted from view successfully.", + "registry": f"{namespace}/{project}:{tag}", + }, + status_code=202, + ) + + +@project.delete( + "/{view}", + summary="Delete a view", + tags=["views"], +) +def delete_view( + namespace: str, + project: str, + view: str, + tag: str = DEFAULT_TAG, + namespace_access_list: List[str] = Depends(get_namespace_access_list), + agent: PEPDatabaseAgent = Depends(get_db), +): + if namespace not in namespace_access_list: + raise HTTPException( + detail="You do not have permission to delete this view.", + status_code=401, + ) + try: + agent.view.delete( + project_namespace=namespace, + project_name=project, + project_tag=tag, + view_name=view, + ) + except Exception: + raise HTTPException( + status_code=400, + detail=f"Could not delete view. Server error!", + ) + return JSONResponse( + content={ + "message": "View deleted successfully.", + "registry": f"{namespace}/{project}:{tag}", + }, + status_code=202, + ) From 816a17afbd250ec0f57b3a6f84bde719cdbac2f2 Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Thu, 11 Jan 2024 18:54:17 -0500 Subject: [PATCH 2/5] Added views endpoints 2 --- pephub/routers/api/v1/project.py | 75 ++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index a9aac23c..f1e598a2 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -23,7 +23,7 @@ SampleAlreadyInView, SampleNotFoundError, ) -from pepdbagent.models import AnnotationModel, AnnotationList, CreateViewDictModel +from pepdbagent.models import AnnotationModel, AnnotationList, CreateViewDictModel, ProjectViews from dotenv import load_dotenv @@ -752,7 +752,7 @@ async def get_project_annotation( "/views", summary="get list of views for a project", tags=["views"], - response_model=List[str], # TODO: rethink response model + response_model=ProjectViews, ) def get_views( namespace: str, @@ -760,13 +760,13 @@ def get_views( tag: str = DEFAULT_TAG, agent: PEPDatabaseAgent = Depends(get_db), ): - return agent.annotation.get_views(namespace, project, tag=tag) + return agent.view.get_views_annotation(namespace, project, tag=tag) @project.get( - "/{view}", + "/views/{view}", summary="Fetch a project view", - response_model=ProjectRawModel, + response_model=Union[ProjectRawModel, dict], tags=["views"], ) async def get_view_of_the_project( @@ -791,19 +791,17 @@ async def get_view_of_the_project( ) ) else: - return ProjectRawModel( - **agent.view.get( + return agent.view.get( namespace=namespace, name=project, view_name=view, tag=tag, raw=raw, ).to_dict() - ) @project.post( - "/{view}", + "/views/{view}", summary="Create a view", tags=["views"], ) @@ -820,11 +818,11 @@ async def create_view_of_the_project( """ Create a view of the project. """ - # if namespace not in namespace_access_list: - # raise HTTPException( - # detail="You do not have permission to create this view.", - # status_code=401, - # ) + if namespace not in namespace_access_list: + raise HTTPException( + detail="You do not have permission to create this view.", + status_code=401, + ) try: agent.view.create( view_name=view, @@ -850,9 +848,32 @@ async def create_view_of_the_project( status_code=202, ) +@project.get( + "/views/{view}/zip", + summary="Zip a view", + tags=["views"], +) +async def zip_view_of_the_view( + namespace: str, + project: str, + view: str, + tag: str = DEFAULT_TAG, + agent: PEPDatabaseAgent = Depends(get_db), +): + """ + Zip a view of the project. + """ + return zip_pep(agent.view.get( + namespace=namespace, + name=project, + view_name=view, + tag=tag, + raw=False, + )) + @project.post( - "/{view}/{sample_name}", + "/views/{view}/{sample_name}", summary="Add sample to the view", tags=["views"], ) @@ -865,11 +886,11 @@ async def add_sample_to_view( namespace_access_list: List[str] = Depends(get_namespace_access_list), agent: PEPDatabaseAgent = Depends(get_db), ): - # if namespace not in namespace_access_list: - # raise HTTPException( - # detail="You do not have permission to add sample to this view.", - # status_code=401, - # ) + if namespace not in namespace_access_list: + raise HTTPException( + detail="You do not have permission to add sample to this view.", + status_code=401, + ) try: agent.view.add_sample( namespace=namespace, @@ -898,7 +919,7 @@ async def add_sample_to_view( @project.delete( - "/{view}/{sample_name}", + "/views/{view}/{sample_name}", summary="Delete sample from the view", tags=["views"], ) @@ -911,11 +932,11 @@ def delete_sample_from_view( namespace_access_list: List[str] = Depends(get_namespace_access_list), agent: PEPDatabaseAgent = Depends(get_db), ): - # if namespace not in namespace_access_list: - # raise HTTPException( - # detail="You do not have permission to delete sample from this view.", - # status_code=401, - # ) + if namespace not in namespace_access_list: + raise HTTPException( + detail="You do not have permission to delete sample from this view.", + status_code=401, + ) try: agent.view.remove_sample( namespace=namespace, @@ -939,7 +960,7 @@ def delete_sample_from_view( @project.delete( - "/{view}", + "/views/{view}", summary="Delete a view", tags=["views"], ) From 5ec0f06b4c30f13b3e38438b8ac84d3c1a88a596 Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Thu, 11 Jan 2024 19:05:12 -0500 Subject: [PATCH 3/5] lint --- pephub/routers/api/v1/project.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index f1e598a2..bf6f63d1 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -23,7 +23,12 @@ SampleAlreadyInView, SampleNotFoundError, ) -from pepdbagent.models import AnnotationModel, AnnotationList, CreateViewDictModel, ProjectViews +from pepdbagent.models import ( + AnnotationModel, + AnnotationList, + CreateViewDictModel, + ProjectViews, +) from dotenv import load_dotenv @@ -792,12 +797,12 @@ async def get_view_of_the_project( ) else: return agent.view.get( - namespace=namespace, - name=project, - view_name=view, - tag=tag, - raw=raw, - ).to_dict() + namespace=namespace, + name=project, + view_name=view, + tag=tag, + raw=raw, + ).to_dict() @project.post( @@ -848,6 +853,7 @@ async def create_view_of_the_project( status_code=202, ) + @project.get( "/views/{view}/zip", summary="Zip a view", @@ -863,13 +869,15 @@ async def zip_view_of_the_view( """ Zip a view of the project. """ - return zip_pep(agent.view.get( + return zip_pep( + agent.view.get( namespace=namespace, name=project, view_name=view, tag=tag, raw=False, - )) + ) + ) @project.post( From a9911b9aec4c51eb54a565ab67590d223512322e Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Fri, 12 Jan 2024 10:24:28 -0500 Subject: [PATCH 4/5] updated forking --- pephub/routers/api/v1/project.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index bf6f63d1..35de4246 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -705,6 +705,8 @@ async def fork_pep_to_namespace( fork_request: ForkRequest, proj_annotation: AnnotationModel = Depends(get_project_annotation), agent: PEPDatabaseAgent = Depends(get_db), + description: Optional[str] = "", + private: Optional[bool] = False, ): """ Fork a project for a particular namespace you have write access to. @@ -723,6 +725,8 @@ async def fork_pep_to_namespace( fork_namespace=fork_to, fork_name=fork_name, fork_tag=fork_tag, + description=description or proj_annotation.description, + private=private or proj_annotation.is_private, ) except ProjectUniqueNameError as _: From 5d99b9947fa474fd984928c65920c7d5439e595e Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Fri, 12 Jan 2024 11:10:45 -0500 Subject: [PATCH 5/5] Fixed uploading projects issue --- pephub/routers/api/v1/namespace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pephub/routers/api/v1/namespace.py b/pephub/routers/api/v1/namespace.py index 0dbedcae..d8d99eca 100644 --- a/pephub/routers/api/v1/namespace.py +++ b/pephub/routers/api/v1/namespace.py @@ -168,7 +168,7 @@ async def create_pep( tag: str = Form(DEFAULT_TAG), description: Union[str, None] = Form(None), pep_schema: str = Form(DEFAULT_PEP_SCHEMA), - files: Optional[List[UploadFile]] = File( + files: List[UploadFile] = File( None # let the file upload be optional. dont send a file? We instantiate with blank ), agent: PEPDatabaseAgent = Depends(get_db),