diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 2baf47dde3d1b739650a733a20f150aaeecedb49..966548006c8583d33c9c4f6e26eb86fd0446ec93 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 1767b9b15cc15457e31321f4c997e97139b6aed2..106e4a67850d4f72a6646e2f1df6b4a70806167d 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 e9982a004c3e25f2fd4d2f06bf718b1c528d2985..9b5e6d6242b394144183b8c6c64e7f0e1bbc3958 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 48103c7c0635fc666ab16c7692ad2ebb4e93abe5..3aea52f904fc1540b79f3a1e4dbbfb0f458a9038 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