Skip to content

Commit

Permalink
Add summary endpoint (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
seallard authored Feb 23, 2024
1 parent fecc11a commit 2462a87
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 58 deletions.
1 change: 1 addition & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def analysis() -> Analysis:
type=TYPES[0],
workflow_manager=WorkflowManager.SLURM,
is_visible=True,
order_id=1,
)
session: Session = get_session()
session.add(analysis)
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/endpoints/test_get_summaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask.testing import FlaskClient
from http import HTTPStatus

from trailblazer.store.models import Analysis


def test_get_summaries(client: FlaskClient, analysis: Analysis):
# GIVEN an order with an analysis
order_id: int = analysis.order_id

# WHEN requesting a summary for the analyses in the order
response = client.get(f"/api/v1/summary?orderIds={order_id}")

# THEN it gives a success response
assert response.status_code == HTTPStatus.OK
2 changes: 1 addition & 1 deletion trailblazer/cli/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from trailblazer.io.controller import ReadFile
from trailblazer.models import Config
from trailblazer.server.wiring import setup_dependency_injection
from trailblazer.services.analysis_service import AnalysisService
from trailblazer.services.analysis_service.analysis_service import AnalysisService
from trailblazer.services.job_service import JobService
from trailblazer.store.database import get_session, initialize_database
from trailblazer.store.models import Analysis, User
Expand Down
2 changes: 1 addition & 1 deletion trailblazer/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dependency_injector import containers, providers

from trailblazer.clients.slurm_cli_client.slurm_cli_client import SlurmCLIClient
from trailblazer.services.analysis_service import AnalysisService
from trailblazer.services.analysis_service.analysis_service import AnalysisService
from trailblazer.services.job_service import JobService
from trailblazer.services.slurm.slurm_cli_service.slurm_cli_service import SlurmCLIService
from trailblazer.store.store import Store
Expand Down
10 changes: 10 additions & 0 deletions trailblazer/dto/summaries_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing_extensions import Annotated
from pydantic import BaseModel, BeforeValidator, Field, ValidationInfo


def parse_order_ids(v: str) -> list[str]:
return v[0].split(",") if v else []


class SummariesRequest(BaseModel):
order_ids: Annotated[list[int], BeforeValidator(parse_order_ids)] = Field(alias="orderIds")
14 changes: 14 additions & 0 deletions trailblazer/dto/summaries_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel


class Summary(BaseModel):
order_id: int
total: int
delivered: int
running: int
cancelled: int
failed: int


class SummariesResponse(BaseModel):
summaries: list[Summary]
28 changes: 19 additions & 9 deletions trailblazer/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,16 @@
)
from trailblazer.dto.analyses_response import UpdateAnalysesResponse
from trailblazer.dto.create_analysis_request import CreateAnalysisRequest
from trailblazer.dto.summaries_request import SummariesRequest
from trailblazer.dto.summaries_response import SummariesResponse
from trailblazer.dto.update_analyses import UpdateAnalyses
from trailblazer.exc import MissingAnalysis
from trailblazer.server.ext import store
from trailblazer.server.utils import (
parse_analyses_request,
parse_analysis_update_request,
parse_get_failed_jobs_request,
parse_job_create_request,
stringify_timestamps,
)
from trailblazer.services.analysis_service import AnalysisService
from trailblazer.services.analysis_service.analysis_service import AnalysisService
from trailblazer.services.job_service import JobService
from trailblazer.store.models import Info

Expand Down Expand Up @@ -105,8 +104,8 @@ def get_analysis(
@inject
def add_job(analysis_id: int, job_service: JobService = Provide[Container.job_service]):
try:
job_request: CreateJobRequest = parse_job_create_request(request)
response: JobResponse = job_service.add_job(analysis_id=analysis_id, data=job_request)
data = CreateJobRequest.model_validate(request.json)
response: JobResponse = job_service.add_job(analysis_id=analysis_id, data=data)
return jsonify(response.model_dump()), HTTPStatus.CREATED
except MissingAnalysis as error:
return jsonify(error=str(error)), HTTPStatus.NOT_FOUND
Expand All @@ -120,7 +119,7 @@ def update_analysis(
analysis_id: int, analysis_service: AnalysisService = Provide[Container.analysis_service]
):
try:
request_data: AnalysisUpdateRequest = parse_analysis_update_request(request)
request_data = AnalysisUpdateRequest.model_validate(request.json)
response: AnalysisResponse = analysis_service.update_analysis(
analysis_id=analysis_id, update=request_data
)
Expand All @@ -131,6 +130,17 @@ def update_analysis(
return jsonify(error=str(error)), HTTPStatus.BAD_REQUEST


@blueprint.route("/summary", methods=["GET"])
@inject
def get_summaries(analysis_service: AnalysisService = Provide[Container.analysis_service]):
try:
request_data = SummariesRequest.model_validate(request.args)
response: SummariesResponse = analysis_service.get_summaries(request_data)
return jsonify(response.model_dump()), HTTPStatus.OK
except ValidationError as error:
return jsonify(error=str(error)), HTTPStatus.BAD_REQUEST


@blueprint.route("/info")
def info():
"""Display metadata about database."""
Expand All @@ -148,8 +158,8 @@ def me():
@inject
def get_failed_jobs(job_service: JobService = Provide[Container.job_service]):
try:
query: FailedJobsRequest = parse_get_failed_jobs_request(request)
response: FailedJobsResponse = job_service.get_failed_jobs(query)
request_data = FailedJobsRequest.model_validate(request.args)
response: FailedJobsResponse = job_service.get_failed_jobs(request_data)
return jsonify(response.model_dump()), HTTPStatus.OK
except ValidationError as error:
return jsonify(error=str(error)), HTTPStatus.BAD_REQUEST
Expand Down
16 changes: 1 addition & 15 deletions trailblazer/server/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import datetime

from flask import Request

from trailblazer.dto import AnalysesRequest, AnalysisUpdateRequest, FailedJobsRequest
from trailblazer.dto.create_job_request import CreateJobRequest
from trailblazer.dto import AnalysesRequest


def parse_analyses_request(request: Request) -> AnalysesRequest:
Expand All @@ -21,21 +19,9 @@ def key_has_list_of_values(key: str) -> bool:
return key.endswith("[]")


def parse_analysis_update_request(request: Request) -> AnalysisUpdateRequest:
return AnalysisUpdateRequest.model_validate(request.json)


def parse_get_failed_jobs_request(request: Request) -> FailedJobsRequest:
return FailedJobsRequest.model_validate(request.args)


def stringify_timestamps(data: dict) -> dict[str, str]:
"""Convert datetime into string before dumping in order to avoid information loss"""
for key, val in data.items():
if isinstance(val, datetime.datetime):
data[key] = str(val)
return data


def parse_job_create_request(request: Request) -> CreateJobRequest:
return CreateJobRequest.model_validate(request.json)
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
CreateAnalysisRequest,
)
from trailblazer.dto.analyses_response import UpdateAnalysesResponse
from trailblazer.dto.summaries_request import SummariesRequest
from trailblazer.dto.summaries_response import SummariesResponse, Summary
from trailblazer.dto.update_analyses import UpdateAnalyses
from trailblazer.exc import MissingAnalysis
from trailblazer.services.utils import create_update_analyses_response, create_analysis_response
from trailblazer.services.analysis_service.utils import (
create_analysis_response,
create_summary,
create_update_analyses_response,
)
from trailblazer.store.models import Analysis, Job
from trailblazer.store.store import Store

Expand Down Expand Up @@ -58,3 +64,11 @@ def create_analyses_response(

def update_ongoing_analyses(self) -> None:
self.store.update_ongoing_analyses()

def get_summaries(self, request_data: SummariesRequest) -> SummariesResponse:
summaries: list[Summary] = []
for order_id in request_data.order_ids:
analyses: list[Analysis] = self.store.get_analyses_by_order_id(order_id)
summary: Summary = create_summary(analyses=analyses, order_id=order_id)
summaries.append(summary)
return SummariesResponse(summaries=summaries)
41 changes: 41 additions & 0 deletions trailblazer/services/analysis_service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from trailblazer.constants import TrailblazerStatus
from trailblazer.dto.analyses_response import UpdateAnalysesResponse
from trailblazer.dto.analysis_response import AnalysisResponse
from trailblazer.dto.summaries_response import Summary
from trailblazer.store.models import Analysis


def get_status_count(analyses: list[Analysis], status: TrailblazerStatus) -> int:
return len([a for a in analyses if a.status == status])


def create_summary(analyses: list[Analysis], order_id: int) -> Summary:
total: int = len(analyses)
delivered: int = get_status_count(analyses=analyses, status=TrailblazerStatus.COMPLETED)
running: int = get_status_count(analyses=analyses, status=TrailblazerStatus.RUNNING)
cancelled: int = get_status_count(analyses=analyses, status=TrailblazerStatus.CANCELLED)
failed = get_status_count(analyses=analyses, status=TrailblazerStatus.FAILED)
return Summary(
order_id=order_id,
total=total,
delivered=delivered,
running=running,
cancelled=cancelled,
failed=failed,
)


def create_analysis_response(analysis: Analysis) -> AnalysisResponse:
analysis_data: dict = analysis.to_dict()
analysis_data["jobs"] = [job.to_dict() for job in analysis.analysis_jobs]
analysis_data["upload_jobs"] = [job.to_dict() for job in analysis.upload_jobs]
analysis_data["user"] = analysis.user.to_dict() if analysis.user else None
return AnalysisResponse.model_validate(analysis_data)


def create_update_analyses_response(analyses: list[Analysis]) -> UpdateAnalysesResponse:
response_data: list[dict] = []
for analysis in analyses:
analysis_data = analysis.to_dict()
response_data.append(analysis_data)
return UpdateAnalysesResponse(analyses=response_data)
1 change: 1 addition & 0 deletions trailblazer/services/job_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from trailblazer.services.job_service.job_service import JobService
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import logging

from trailblazer.dto import CreateJobRequest, FailedJobsRequest, FailedJobsResponse, JobResponse
from trailblazer.services.job_service.utils import create_failed_jobs_response, create_job_response
from trailblazer.services.slurm.dtos import SlurmJobInfo
from trailblazer.services.slurm.slurm_service import SlurmService
from trailblazer.services.utils import create_job_response, create_failed_jobs_response
from trailblazer.store.models import Job
from trailblazer.store.store import Store
from trailblazer.utils.datetime import get_date_number_of_days_ago
Expand Down
13 changes: 13 additions & 0 deletions trailblazer/services/job_service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from trailblazer.dto.failed_jobs_response import FailedJobsResponse
from trailblazer.dto.job_response import JobResponse
from trailblazer.store.models import Job


def create_job_response(job: Job) -> JobResponse:
return JobResponse(
slurm_id=job.slurm_id, analysis_id=job.analysis_id, status=job.status, id=job.id
)


def create_failed_jobs_response(failed_job_statistics: list[dict]) -> FailedJobsResponse:
return FailedJobsResponse(jobs=failed_job_statistics)
30 changes: 0 additions & 30 deletions trailblazer/services/utils.py

This file was deleted.

7 changes: 7 additions & 0 deletions trailblazer/store/crud/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,10 @@ def get_job_by_id(self, job_id: int) -> Job | None:
jobs=self.get_query(Job),
job_id=job_id,
).first()

def get_analyses_by_order_id(self, order_id: int) -> list[Analysis]:
return apply_analysis_filter(
analyses=self.get_query(Analysis),
filter_functions=[AnalysisFilter.FILTER_BY_ORDER_ID],
order_id=order_id,
).all()

0 comments on commit 2462a87

Please sign in to comment.