From 68b6beda8caa66dc8a0ace9ec158b720b651daa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Tue, 28 Mar 2023 15:56:59 +0200 Subject: [PATCH] Add endpoint to fetch statistics for workflow #24 --- app/api/endpoints/workflow.py | 34 ++++++++++++++++++++++++++++--- app/crud/crud_workflow.py | 36 +++++++++++++++++++++++++++++---- app/schemas/workflow.py | 6 ++++++ app/tests/crud/test_workflow.py | 10 ++++----- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 2baf47d..9665480 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -1,14 +1,14 @@ from typing import Annotated, Any, Awaitable, Callable from clowmdb.models import Workflow, WorkflowVersion -from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Query, UploadFile, status +from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Query, Response, UploadFile, status from app.api.dependencies import AuthorizationDependency, CurrentUser, CurrentWorkflow, DBSession, HTTPClient, S3Service from app.api.utils import check_repo, upload_icon from app.core.config import settings from app.crud import CRUDWorkflow, CRUDWorkflowVersion from app.git_repository import build_repository -from app.schemas.workflow import WorkflowIn, WorkflowOut +from app.schemas.workflow import WorkflowIn, WorkflowOut, WorkflowStatistic from app.schemas.workflow_version import WorkflowVersionFull, WorkflowVersionUpdate router = APIRouter(prefix="/workflows", tags=["Workflow"]) @@ -72,7 +72,7 @@ async def list_workflows( rbac_operation = "list_filter" await authorization(rbac_operation) - workflows: list[Workflow] = await CRUDWorkflow.list( + workflows: list[Workflow] = await CRUDWorkflow.list_workflows( db, name_substring=name_substring, developer_id=developer_id, @@ -205,6 +205,34 @@ async def get_workflow( return WorkflowOut.from_db_workflow(workflow, versions) +@router.get("/{wid}/statistics", status_code=status.HTTP_200_OK, summary="Get statistics for a workflow") +async def get_workflow_statistics( + workflow: CurrentWorkflow, db: DBSession, authorization: Authorization, response: Response +) -> list[WorkflowStatistic]: + """ + Get the number of started workflow per day. + \f + Parameters + ---------- + workflow : clowmdb.models.Workflow + Workflow with given ID. Dependency Injection. + 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. + response : fastapi.Response + Temporal Response object. Dependency Injection. + + Returns + ------- + statistics : list[app.schema.Workflow.WorkflowStatistic] + """ + await authorization("read") + # Instruct client to cache response for 1 hour + response.headers["Cache-Control"] = "max-age=3600" + return await CRUDWorkflow.statistics(db, workflow.workflow_id) + + @router.delete("/{wid}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a workflow") async def delete_workflow( background_tasks: BackgroundTasks, diff --git a/app/crud/crud_workflow.py b/app/crud/crud_workflow.py index 1767b9b..106e4a6 100644 --- a/app/crud/crud_workflow.py +++ b/app/crud/crud_workflow.py @@ -1,17 +1,17 @@ from uuid import UUID -from clowmdb.models import Workflow, WorkflowVersion -from sqlalchemy import delete, or_, select +from clowmdb.models import Workflow, WorkflowExecution, WorkflowVersion +from sqlalchemy import Date, cast, delete, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload from app.crud.crud_workflow_version import CRUDWorkflowVersion -from app.schemas.workflow import WorkflowIn +from app.schemas.workflow import WorkflowIn, WorkflowStatistic class CRUDWorkflow: @staticmethod - async def list( + async def list_workflows( db: AsyncSession, name_substring: str | None = None, developer_id: str | None = None, @@ -66,6 +66,34 @@ class CRUDWorkflow: await db.execute(stmt) await db.commit() + @staticmethod + async def statistics(db: AsyncSession, workflow_id: bytes | UUID) -> list[WorkflowStatistic]: + """ + Calculate the number of workflows started per day for a specific workflow + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + workflow_id : bytes | uuid.UUID + UID of a workflow. + + Returns + ------- + stat : list[app.schemas.Workflow.WorkflowStatistic] + List of datapoints + """ + wid = workflow_id.bytes if isinstance(workflow_id, UUID) else workflow_id + stmt = ( + select(cast(WorkflowExecution.start_time, Date).label("day"), func.count()) + .select_from(WorkflowExecution) + .join(WorkflowVersion) + .where(WorkflowVersion._workflow_id == wid) + .group_by("day") + .order_by("day") + ) + return [WorkflowStatistic(day=row.day, count=row.count) for row in await db.execute(stmt)] + @staticmethod async def get(db: AsyncSession, workflow_id: UUID | bytes) -> Workflow | None: """ diff --git a/app/schemas/workflow.py b/app/schemas/workflow.py index e9982a0..9b5e6d6 100644 --- a/app/schemas/workflow.py +++ b/app/schemas/workflow.py @@ -1,3 +1,4 @@ +from datetime import date from uuid import UUID from clowmdb.models import Workflow as WorkflowDB @@ -67,3 +68,8 @@ class WorkflowOut(_BaseWorkflow): versions=temp_versions, developer_id=db_workflow.developer_id, ) + + +class WorkflowStatistic(BaseModel): + day: date = Field(..., description="Day of the datapoint", example=date(day=1, month=1, year=2023)) + count: int = Field(..., description="Number of started workflows on that day", example=1) diff --git a/app/tests/crud/test_workflow.py b/app/tests/crud/test_workflow.py index 48103c7..3aea52f 100644 --- a/app/tests/crud/test_workflow.py +++ b/app/tests/crud/test_workflow.py @@ -24,7 +24,7 @@ class TestWorkflowCRUDList: random_workflow : app.schemas.workflow.WorkflowOut Random bucket for testing. pytest fixture. """ - workflows = await CRUDWorkflow.list(db) + workflows = await CRUDWorkflow.list_workflows(db) assert len(workflows) == 1 assert workflows[0].workflow_id == random_workflow.workflow_id @@ -44,7 +44,7 @@ class TestWorkflowCRUDList: random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. pytest fixture. """ - workflows = await CRUDWorkflow.list(db, developer_id=random_user.user.uid) + workflows = await CRUDWorkflow.list_workflows(db, developer_id=random_user.user.uid) assert len(workflows) == 1 assert workflows[0].workflow_id == random_workflow.workflow_id @@ -60,7 +60,7 @@ class TestWorkflowCRUDList: random_workflow : app.schemas.workflow.WorkflowOut Random bucket for testing. pytest fixture. """ - workflows = await CRUDWorkflow.list(db, version_status=[WorkflowVersion.Status.CREATED]) + workflows = await CRUDWorkflow.list_workflows(db, version_status=[WorkflowVersion.Status.CREATED]) assert len(workflows) == 1 assert workflows[0].workflow_id == random_workflow.workflow_id @@ -81,7 +81,7 @@ class TestWorkflowCRUDList: substring_indices = sorted(random.choices(range(len(random_workflow.name)), k=2)) random_substring = random_workflow.name[substring_indices[0] : substring_indices[1]] - workflows = await CRUDWorkflow.list(db, name_substring=random_substring) + workflows = await CRUDWorkflow.list_workflows(db, name_substring=random_substring) assert len(workflows) > 0 assert random_workflow.workflow_id in map(lambda w: w.workflow_id, workflows) @@ -97,7 +97,7 @@ class TestWorkflowCRUDList: random_workflow : app.schemas.workflow.WorkflowOut Random bucket for testing. pytest fixture. """ - workflows = await CRUDWorkflow.list(db, name_substring=2 * random_workflow.name) + workflows = await CRUDWorkflow.list_workflows(db, name_substring=2 * random_workflow.name) assert sum(1 for w in workflows if w.workflow_id == random_workflow.workflow_id) == 0 -- GitLab