diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91c4f945841636e9f1d36fad1157cdeb5c3abcac..24ec1953af2efca309526c6880cd28b17259f492 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,11 +21,11 @@ repos: files: app args: [--check] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.252' + rev: 'v0.0.257' hooks: - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.1.1 hooks: - id: mypy files: app diff --git a/app/api/endpoints/workflow_version.py b/app/api/endpoints/workflow_version.py index 0d12516a6330b9182182d920c66bca1795e3d547..d13d2e52abaf3598da1a41921a1ff7f69bd4d6af 100644 --- a/app/api/endpoints/workflow_version.py +++ b/app/api/endpoints/workflow_version.py @@ -1,4 +1,4 @@ -from typing import Any, Awaitable, Callable +from typing import Annotated, Any, Awaitable, Callable from clowmdb.models import User, Workflow, WorkflowVersion from fastapi import APIRouter, Depends, HTTPException, Path, Query, status @@ -12,18 +12,32 @@ from app.schemas.workflow_version import WorkflowVersionFull, WorkflowVersionSta router = APIRouter(prefix="/{wid}/versions", tags=["WorkflowVersion"]) workflow_authorization = AuthorizationDependency(resource="workflow") +CurrentUser = Annotated[User, Depends(get_current_user)] +CurrentWorkflow = Annotated[Workflow, Depends(get_current_workflow)] +DBSession = Annotated[AsyncSession, Depends(get_db)] +Authorization = Annotated[Callable[[str], Awaitable[Any]], Depends(workflow_authorization)] +GitCommitHash = Annotated[ + str, + Path( + ..., + description="Git commit git_commit_hash of specific version.", + regex=r"^([0-9a-f]{40}|latest)$", + example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", + ), +] + @router.get("", status_code=status.HTTP_200_OK, summary="Get all versions of a workflow") async def list_workflow_version( - workflow: Workflow = Depends(get_current_workflow), - db: AsyncSession = Depends(get_db), + current_user: CurrentUser, + workflow: CurrentWorkflow, + db: DBSession, + authorization: Authorization, version_status: list[WorkflowVersion.Status] | None = Query( None, description=f"Which versions of the workflow to include in the response. Permission 'workflow:list_filter' required if you are not the developer of this workflow. Default {WorkflowVersion.Status.PUBLISHED.name} and {WorkflowVersion.Status.DEPRECATED.name}", # noqa: E501 ), - current_user: User = Depends(get_current_user), - authorization: Callable[[str], Awaitable[Any]] = Depends(workflow_authorization), ) -> list[WorkflowVersionFull]: """ List all versions of a Workflow.\n @@ -71,7 +85,10 @@ async def list_workflow_version( summary="Get a workflow version", ) async def get_workflow_version( - workflow: Workflow = Depends(get_current_workflow), + workflow: CurrentWorkflow, + db: DBSession, + current_user: CurrentUser, + authorization: Authorization, git_commit_hash: str = Path( ..., description="Git commit git_commit_hash of specific version or 'latest'.", @@ -89,9 +106,6 @@ async def get_workflow_version( }, }, ), - db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), - authorization: Callable[[str], Awaitable[Any]] = Depends(workflow_authorization), ) -> WorkflowVersionFull: """ Get a specific version of a workflow.\n @@ -143,15 +157,10 @@ async def get_workflow_version( @router.patch("/{git_commit_hash}/status", status_code=status.HTTP_200_OK, summary="Update status of workflow version") async def update_workflow_version_status( version_status: WorkflowVersionStatus, - workflow: Workflow = Depends(get_current_workflow), - git_commit_hash: str = Path( - ..., - description="Git commit git_commit_hash of specific version.", - regex=r"^[0-9a-f]{40}$", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - ), - db: AsyncSession = Depends(get_db), - authorization: Callable[[str], Awaitable[Any]] = Depends(workflow_authorization), + workflow: CurrentWorkflow, + git_commit_hash: GitCommitHash, + db: DBSession, + authorization: Authorization, ) -> WorkflowVersionFull: """ Update the status of a workflow version.\n @@ -160,6 +169,7 @@ async def update_workflow_version_status( Parameters ---------- version_status : app.schemas.workflow_version.WorkflowVersionStatus + New Status of the workflow version. HTTP Body. workflow : clowmdb.models.Workflow Workflow with given ID. Dependency Injection. git_commit_hash: str @@ -172,7 +182,7 @@ async def update_workflow_version_status( Returns ------- version : clowmdb.models.WorkflowVersion - version of the workflow with updated status + Version of the workflow with updated status """ await authorization("update_status") version = await CRUDWorkflowVersion.get(db, git_commit_hash) @@ -186,3 +196,48 @@ async def update_workflow_version_status( return WorkflowVersionFull.from_db_version_with_repo( version, repo=build_repository(workflow.repository_url, version.git_commit_hash) ) + + +@router.post("/{git_commit_hash}/deprecate", status_code=status.HTTP_200_OK, summary="Deprecate a workflow version") +async def deprecate_workflow_version( + workflow: CurrentWorkflow, + git_commit_hash: GitCommitHash, + db: DBSession, + authorization: Authorization, + current_user: CurrentUser, +) -> WorkflowVersionFull: + """ + Deprecate a workflow version.\n + Permission "workflow:update" required if you are the developer of the workflow, + otherwise "workflow:read_status"" + \f + Parameters + ---------- + workflow : clowmdb.models.Workflow + Workflow with given ID. Dependency Injection. + git_commit_hash: str + Version ID + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. Dependency Injection. + authorization : Callable[[str], Awaitable[Any]] + Async function to ask the auth service for authorization. Dependency Injection. + current_user: clowmdb.models.User + Current user who will be the owner of the newly created bucket. Dependency Injection. + + Returns + ------- + version : clowmdb.models.WorkflowVersion + Version of the workflow with deprecated status + """ + await authorization("update_status" if current_user.uid != workflow.developer_id else "update") + version = await CRUDWorkflowVersion.get(db, git_commit_hash) + if version is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Workflow Version with git_commit_hash '{git_commit_hash}' not found", + ) + await CRUDWorkflowVersion.update_status(db, git_commit_hash, WorkflowVersion.Status.DEPRECATED) + version.status = WorkflowVersion.Status.DEPRECATED + return WorkflowVersionFull.from_db_version_with_repo( + version, repo=build_repository(workflow.repository_url, version.git_commit_hash) + ) diff --git a/app/tests/api/test_workflow_version.py b/app/tests/api/test_workflow_version.py index 5ffaa7cc5bed2a7daa7f7ad835b09545ea8da67a..e92257bc3585c8645da587941bb48ff7d1b02ac9 100644 --- a/app/tests/api/test_workflow_version.py +++ b/app/tests/api/test_workflow_version.py @@ -135,6 +135,40 @@ class TestWorkflowVersionRoutesUpdate(_TestWorkflowVersionRoutes): assert version["git_commit_hash"] == random_workflow.versions[0].git_commit_hash assert version["status"] == WorkflowVersion.Status.PUBLISHED + @pytest.mark.asyncio + async def test_deprecate_workflow_version( + self, client: AsyncClient, random_user: UserWithAuthHeader, random_workflow: WorkflowOut + ) -> None: + """ + Test deprecate a workflow version. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. pytest fixture. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. pytest fixture. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. pytest fixture. + """ + response = await client.post( + "/".join( + [ + self.base_path, + str(random_workflow.workflow_id), + "versions", + random_workflow.versions[0].git_commit_hash, + "deprecate", + ] + ), + headers=random_user.auth_headers, + ) + + assert response.status_code == status.HTTP_200_OK + version = response.json() + assert version["git_commit_hash"] == random_workflow.versions[0].git_commit_hash + assert version["status"] == WorkflowVersion.Status.DEPRECATED + @pytest.mark.asyncio async def test_update_non_existing_workflow_version_status( self, client: AsyncClient, random_user: UserWithAuthHeader, random_workflow: WorkflowOut @@ -158,3 +192,26 @@ class TestWorkflowVersionRoutesUpdate(_TestWorkflowVersionRoutes): ) assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.asyncio + async def test_deprecate_non_existing_workflow_version( + self, client: AsyncClient, random_user: UserWithAuthHeader, random_workflow: WorkflowOut + ) -> None: + """ + Test deprecate a non-existing workflow version. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. pytest fixture. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. pytest fixture. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. pytest fixture. + """ + response = await client.post( + "/".join([self.base_path, str(random_workflow.workflow_id), "versions", random_hex_string(), "deprecate"]), + headers=random_user.auth_headers, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/requirements.txt b/requirements.txt index 77cf649210e2a44e32d2a29f532f575fce0d305f..0e859b650103524be25b9ed3ee91e0c06e12249c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ clowmdb>=1.3.0,<1.4.0 # Webserver packages anyio>=3.6.0,<3.7.0 -fastapi>=0.92.0,<0.93.0 -pydantic>=1.9.0,<2.0.0 -uvicorn>=0.20.0,<0.21.0 +fastapi>=0.95.0,<0.96.0 +pydantic>=1.10.0,<2.0.0 +uvicorn>=0.21.0,<0.22.0 python-multipart # Database packages PyMySQL>=1.0.2,<1.1.0