From 555c265a9aa3373913b33602788560cce05f41da Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:02:43 +0100 Subject: [PATCH 1/9] Add summary endpoint --- trailblazer/cli/core.py | 2 +- trailblazer/containers.py | 2 +- trailblazer/dto/summaries_request.py | 5 ++++ trailblazer/dto/summaries_response.py | 14 +++++++++++ trailblazer/server/api.py | 16 ++++++++++++- trailblazer/server/utils.py | 10 ++++++++ .../services/analysis_service/__init__.py | 0 .../analysis_service.py | 11 +++++++++ .../services/analysis_service/utils.py | 24 +++++++++++++++++++ trailblazer/services/utils.py | 2 ++ trailblazer/store/crud/read.py | 7 ++++++ 11 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 trailblazer/dto/summaries_request.py create mode 100644 trailblazer/dto/summaries_response.py create mode 100644 trailblazer/services/analysis_service/__init__.py rename trailblazer/services/{ => analysis_service}/analysis_service.py (81%) create mode 100644 trailblazer/services/analysis_service/utils.py diff --git a/trailblazer/cli/core.py b/trailblazer/cli/core.py index 43001f46..550e81cb 100644 --- a/trailblazer/cli/core.py +++ b/trailblazer/cli/core.py @@ -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 diff --git a/trailblazer/containers.py b/trailblazer/containers.py index d6273813..f861701d 100644 --- a/trailblazer/containers.py +++ b/trailblazer/containers.py @@ -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 diff --git a/trailblazer/dto/summaries_request.py b/trailblazer/dto/summaries_request.py new file mode 100644 index 00000000..8ab3837a --- /dev/null +++ b/trailblazer/dto/summaries_request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class SummariesRequest(BaseModel): + order_ids: list[int] diff --git a/trailblazer/dto/summaries_response.py b/trailblazer/dto/summaries_response.py new file mode 100644 index 00000000..a796d992 --- /dev/null +++ b/trailblazer/dto/summaries_response.py @@ -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] diff --git a/trailblazer/server/api.py b/trailblazer/server/api.py index a61e056a..af294678 100644 --- a/trailblazer/server/api.py +++ b/trailblazer/server/api.py @@ -28,6 +28,8 @@ ) 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 @@ -36,9 +38,10 @@ parse_analysis_update_request, parse_get_failed_jobs_request, parse_job_create_request, + parse_summaries_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 @@ -131,6 +134,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 = parse_summaries_request(request) + 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.""" diff --git a/trailblazer/server/utils.py b/trailblazer/server/utils.py index 73b9a2a9..dd6aff7f 100644 --- a/trailblazer/server/utils.py +++ b/trailblazer/server/utils.py @@ -4,6 +4,7 @@ from trailblazer.dto import AnalysesRequest, AnalysisUpdateRequest, FailedJobsRequest from trailblazer.dto.create_job_request import CreateJobRequest +from trailblazer.dto.summaries_request import SummariesRequest def parse_analyses_request(request: Request) -> AnalysesRequest: @@ -39,3 +40,12 @@ def stringify_timestamps(data: dict) -> dict[str, str]: def parse_job_create_request(request: Request) -> CreateJobRequest: return CreateJobRequest.model_validate(request.json) + + +def parse_summaries_request(request: Request) -> SummariesRequest: + query_params = { + key[:-2]: request.args.getlist(key) + for key in request.args.keys() + if key_has_list_of_values(key) + } + return SummariesRequest.model_validate(query_params) diff --git a/trailblazer/services/analysis_service/__init__.py b/trailblazer/services/analysis_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/trailblazer/services/analysis_service.py b/trailblazer/services/analysis_service/analysis_service.py similarity index 81% rename from trailblazer/services/analysis_service.py rename to trailblazer/services/analysis_service/analysis_service.py index 24bd3c5d..42dcea96 100644 --- a/trailblazer/services/analysis_service.py +++ b/trailblazer/services/analysis_service/analysis_service.py @@ -6,8 +6,11 @@ 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.analysis_service.utils import create_summary from trailblazer.services.utils import create_update_analyses_response, create_analysis_response from trailblazer.store.models import Analysis, Job from trailblazer.store.store import Store @@ -58,3 +61,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) + summaries.append(summary) + return SummariesResponse(summaries=summaries) diff --git a/trailblazer/services/analysis_service/utils.py b/trailblazer/services/analysis_service/utils.py new file mode 100644 index 00000000..75b311dc --- /dev/null +++ b/trailblazer/services/analysis_service/utils.py @@ -0,0 +1,24 @@ +from trailblazer.constants import TrailblazerStatus +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]) -> Summary: + """Create a summary of the analyses.""" + 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=analyses[0].order_id, + total=total, + delivered=delivered, + running=running, + cancelled=cancelled, + failed=failed, + ) diff --git a/trailblazer/services/utils.py b/trailblazer/services/utils.py index 922abbd5..0df1f065 100644 --- a/trailblazer/services/utils.py +++ b/trailblazer/services/utils.py @@ -28,3 +28,5 @@ def create_update_analyses_response(analyses: list[Analysis]) -> UpdateAnalysesR analysis_data = analysis.to_dict() response_data.append(analysis_data) return UpdateAnalysesResponse(analyses=response_data) + + diff --git a/trailblazer/store/crud/read.py b/trailblazer/store/crud/read.py index 92ef9758..f530fe8a 100644 --- a/trailblazer/store/crud/read.py +++ b/trailblazer/store/crud/read.py @@ -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() From 166a55bac1ed9e92d2f8934293d88ab5875d4f04 Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:11:29 +0100 Subject: [PATCH 2/9] Restructure --- trailblazer/services/analysis_service/utils.py | 1 - trailblazer/services/job_service/__init__.py | 1 + trailblazer/services/{ => job_service}/job_service.py | 0 trailblazer/services/job_service/utils.py | 8 ++++++++ trailblazer/services/utils.py | 4 +--- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 trailblazer/services/job_service/__init__.py rename trailblazer/services/{ => job_service}/job_service.py (100%) create mode 100644 trailblazer/services/job_service/utils.py diff --git a/trailblazer/services/analysis_service/utils.py b/trailblazer/services/analysis_service/utils.py index 75b311dc..48ff2582 100644 --- a/trailblazer/services/analysis_service/utils.py +++ b/trailblazer/services/analysis_service/utils.py @@ -8,7 +8,6 @@ def get_status_count(analyses: list[Analysis], status: TrailblazerStatus) -> int def create_summary(analyses: list[Analysis]) -> Summary: - """Create a summary of the analyses.""" total: int = len(analyses) delivered: int = get_status_count(analyses=analyses, status=TrailblazerStatus.COMPLETED) running: int = get_status_count(analyses=analyses, status=TrailblazerStatus.RUNNING) diff --git a/trailblazer/services/job_service/__init__.py b/trailblazer/services/job_service/__init__.py new file mode 100644 index 00000000..c6dcf071 --- /dev/null +++ b/trailblazer/services/job_service/__init__.py @@ -0,0 +1 @@ +from trailblazer.services.job_service.job_service import JobService diff --git a/trailblazer/services/job_service.py b/trailblazer/services/job_service/job_service.py similarity index 100% rename from trailblazer/services/job_service.py rename to trailblazer/services/job_service/job_service.py diff --git a/trailblazer/services/job_service/utils.py b/trailblazer/services/job_service/utils.py new file mode 100644 index 00000000..c7de4591 --- /dev/null +++ b/trailblazer/services/job_service/utils.py @@ -0,0 +1,8 @@ +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 + ) diff --git a/trailblazer/services/utils.py b/trailblazer/services/utils.py index 0df1f065..930ff2f3 100644 --- a/trailblazer/services/utils.py +++ b/trailblazer/services/utils.py @@ -1,5 +1,5 @@ from trailblazer.dto import AnalysisResponse, FailedJobsResponse -from trailblazer.dto.analyses_response import AnalysesResponse, UpdateAnalysesResponse +from trailblazer.dto.analyses_response import UpdateAnalysesResponse from trailblazer.dto.job_response import JobResponse from trailblazer.store.models import Analysis, Job @@ -28,5 +28,3 @@ def create_update_analyses_response(analyses: list[Analysis]) -> UpdateAnalysesR analysis_data = analysis.to_dict() response_data.append(analysis_data) return UpdateAnalysesResponse(analyses=response_data) - - From 98b5fe1c160992bf5fd6113916d80bbd7b7519aa Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:14:01 +0100 Subject: [PATCH 3/9] Restructure --- .../analysis_service/analysis_service.py | 3 +- .../services/analysis_service/utils.py | 17 +++++++++++ .../services/job_service/job_service.py | 2 +- trailblazer/services/job_service/utils.py | 5 ++++ trailblazer/services/utils.py | 30 ------------------- 5 files changed, 24 insertions(+), 33 deletions(-) delete mode 100644 trailblazer/services/utils.py diff --git a/trailblazer/services/analysis_service/analysis_service.py b/trailblazer/services/analysis_service/analysis_service.py index 42dcea96..12464b4c 100644 --- a/trailblazer/services/analysis_service/analysis_service.py +++ b/trailblazer/services/analysis_service/analysis_service.py @@ -10,8 +10,7 @@ from trailblazer.dto.summaries_response import SummariesResponse, Summary from trailblazer.dto.update_analyses import UpdateAnalyses from trailblazer.exc import MissingAnalysis -from trailblazer.services.analysis_service.utils import create_summary -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 diff --git a/trailblazer/services/analysis_service/utils.py b/trailblazer/services/analysis_service/utils.py index 48ff2582..528b69e6 100644 --- a/trailblazer/services/analysis_service/utils.py +++ b/trailblazer/services/analysis_service/utils.py @@ -1,4 +1,6 @@ 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 @@ -21,3 +23,18 @@ def create_summary(analyses: list[Analysis]) -> Summary: 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) diff --git a/trailblazer/services/job_service/job_service.py b/trailblazer/services/job_service/job_service.py index 6cd6c23c..b90777e5 100644 --- a/trailblazer/services/job_service/job_service.py +++ b/trailblazer/services/job_service/job_service.py @@ -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 diff --git a/trailblazer/services/job_service/utils.py b/trailblazer/services/job_service/utils.py index c7de4591..f6af681b 100644 --- a/trailblazer/services/job_service/utils.py +++ b/trailblazer/services/job_service/utils.py @@ -1,3 +1,4 @@ +from trailblazer.dto.failed_jobs_response import FailedJobsResponse from trailblazer.dto.job_response import JobResponse from trailblazer.store.models import Job @@ -6,3 +7,7 @@ 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) diff --git a/trailblazer/services/utils.py b/trailblazer/services/utils.py deleted file mode 100644 index 930ff2f3..00000000 --- a/trailblazer/services/utils.py +++ /dev/null @@ -1,30 +0,0 @@ -from trailblazer.dto import AnalysisResponse, FailedJobsResponse -from trailblazer.dto.analyses_response import UpdateAnalysesResponse -from trailblazer.dto.job_response import JobResponse -from trailblazer.store.models import Analysis, Job - - -def create_failed_jobs_response(failed_job_statistics: list[dict]) -> FailedJobsResponse: - return FailedJobsResponse(jobs=failed_job_statistics) - - -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_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_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) From 40e5a3bce9abd6d87fb878dc49843440db2be346 Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:15:04 +0100 Subject: [PATCH 4/9] Restructure --- trailblazer/services/analysis_service/analysis_service.py | 6 +++++- trailblazer/services/analysis_service/utils.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/trailblazer/services/analysis_service/analysis_service.py b/trailblazer/services/analysis_service/analysis_service.py index 12464b4c..a2b0c20f 100644 --- a/trailblazer/services/analysis_service/analysis_service.py +++ b/trailblazer/services/analysis_service/analysis_service.py @@ -10,7 +10,11 @@ from trailblazer.dto.summaries_response import SummariesResponse, Summary from trailblazer.dto.update_analyses import UpdateAnalyses from trailblazer.exc import MissingAnalysis -from trailblazer.services.analysis_service.utils import create_analysis_response, create_summary, create_update_analyses_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 diff --git a/trailblazer/services/analysis_service/utils.py b/trailblazer/services/analysis_service/utils.py index 528b69e6..725cbb41 100644 --- a/trailblazer/services/analysis_service/utils.py +++ b/trailblazer/services/analysis_service/utils.py @@ -24,6 +24,7 @@ def create_summary(analyses: list[Analysis]) -> Summary: 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] From c8964d785c03af91d33f4529d4dd751ec43340ff Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:19:32 +0100 Subject: [PATCH 5/9] Add test --- tests/integration/conftest.py | 1 + .../integration/endpoints/test_get_summaries.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/integration/endpoints/test_get_summaries.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b0365f80..d2d9aa4b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -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) diff --git a/tests/integration/endpoints/test_get_summaries.py b/tests/integration/endpoints/test_get_summaries.py new file mode 100644 index 00000000..945f90f8 --- /dev/null +++ b/tests/integration/endpoints/test_get_summaries.py @@ -0,0 +1,16 @@ +from flask.testing import FlaskClient +from http import HTTPStatus + +from trailblazer.store.models import Analysis + + +def test_get_existing_analysis(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?order_ids[]={order_id}") + + # THEN it gives a success response + assert response.status_code == HTTPStatus.OK + From 31fe86d5c1575837e22b886c1bc9e0af88eeefc6 Mon Sep 17 00:00:00 2001 From: seallard Date: Wed, 21 Feb 2024 10:50:47 +0100 Subject: [PATCH 6/9] Formatting --- tests/integration/endpoints/test_get_summaries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/endpoints/test_get_summaries.py b/tests/integration/endpoints/test_get_summaries.py index 945f90f8..806d8b72 100644 --- a/tests/integration/endpoints/test_get_summaries.py +++ b/tests/integration/endpoints/test_get_summaries.py @@ -13,4 +13,3 @@ def test_get_existing_analysis(client: FlaskClient, analysis: Analysis): # THEN it gives a success response assert response.status_code == HTTPStatus.OK - From 0a06c6aba54a211c08e1763cc60a2ac96e54f982 Mon Sep 17 00:00:00 2001 From: seallard Date: Fri, 23 Feb 2024 11:42:10 +0100 Subject: [PATCH 7/9] Fix parsing --- tests/integration/endpoints/test_get_summaries.py | 4 ++-- trailblazer/dto/summaries_request.py | 9 +++++++-- trailblazer/server/api.py | 5 +++-- trailblazer/server/utils.py | 9 --------- .../services/analysis_service/analysis_service.py | 2 +- trailblazer/services/analysis_service/utils.py | 4 ++-- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/tests/integration/endpoints/test_get_summaries.py b/tests/integration/endpoints/test_get_summaries.py index 806d8b72..6f0e9342 100644 --- a/tests/integration/endpoints/test_get_summaries.py +++ b/tests/integration/endpoints/test_get_summaries.py @@ -4,12 +4,12 @@ from trailblazer.store.models import Analysis -def test_get_existing_analysis(client: FlaskClient, analysis: 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?order_ids[]={order_id}") + response = client.get(f"/api/v1/summary?orderIds={order_id}") # THEN it gives a success response assert response.status_code == HTTPStatus.OK diff --git a/trailblazer/dto/summaries_request.py b/trailblazer/dto/summaries_request.py index 8ab3837a..1a2f84e5 100644 --- a/trailblazer/dto/summaries_request.py +++ b/trailblazer/dto/summaries_request.py @@ -1,5 +1,10 @@ -from pydantic import BaseModel +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: list[int] + order_ids: Annotated[list[int], BeforeValidator(parse_order_ids)] = Field(alias="orderIds") diff --git a/trailblazer/server/api.py b/trailblazer/server/api.py index af294678..c84393e5 100644 --- a/trailblazer/server/api.py +++ b/trailblazer/server/api.py @@ -38,7 +38,6 @@ parse_analysis_update_request, parse_get_failed_jobs_request, parse_job_create_request, - parse_summaries_request, stringify_timestamps, ) from trailblazer.services.analysis_service.analysis_service import AnalysisService @@ -138,11 +137,13 @@ def update_analysis( @inject def get_summaries(analysis_service: AnalysisService = Provide[Container.analysis_service]): try: - request_data: SummariesRequest = parse_summaries_request(request) + 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 + except Exception as error: + return jsonify(error=str(error)), HTTPStatus.INTERNAL_SERVER_ERROR @blueprint.route("/info") diff --git a/trailblazer/server/utils.py b/trailblazer/server/utils.py index dd6aff7f..38448a12 100644 --- a/trailblazer/server/utils.py +++ b/trailblazer/server/utils.py @@ -40,12 +40,3 @@ def stringify_timestamps(data: dict) -> dict[str, str]: def parse_job_create_request(request: Request) -> CreateJobRequest: return CreateJobRequest.model_validate(request.json) - - -def parse_summaries_request(request: Request) -> SummariesRequest: - query_params = { - key[:-2]: request.args.getlist(key) - for key in request.args.keys() - if key_has_list_of_values(key) - } - return SummariesRequest.model_validate(query_params) diff --git a/trailblazer/services/analysis_service/analysis_service.py b/trailblazer/services/analysis_service/analysis_service.py index a2b0c20f..dbb58503 100644 --- a/trailblazer/services/analysis_service/analysis_service.py +++ b/trailblazer/services/analysis_service/analysis_service.py @@ -69,6 +69,6 @@ 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) + summary: Summary = create_summary(analyses=analyses, order_id=order_id) summaries.append(summary) return SummariesResponse(summaries=summaries) diff --git a/trailblazer/services/analysis_service/utils.py b/trailblazer/services/analysis_service/utils.py index 725cbb41..5ee17f11 100644 --- a/trailblazer/services/analysis_service/utils.py +++ b/trailblazer/services/analysis_service/utils.py @@ -9,14 +9,14 @@ 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]) -> Summary: +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=analyses[0].order_id, + order_id=order_id, total=total, delivered=delivered, running=running, From d85dbed35f2eff43fa61f5b5b2b9c082cbe71f21 Mon Sep 17 00:00:00 2001 From: seallard Date: Fri, 23 Feb 2024 11:47:41 +0100 Subject: [PATCH 8/9] Simplify parsing --- trailblazer/server/api.py | 15 +++++---------- trailblazer/server/utils.py | 12 ------------ 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/trailblazer/server/api.py b/trailblazer/server/api.py index c84393e5..07680a7a 100644 --- a/trailblazer/server/api.py +++ b/trailblazer/server/api.py @@ -35,9 +35,6 @@ 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.analysis_service import AnalysisService @@ -107,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 @@ -122,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 ) @@ -142,8 +139,6 @@ def get_summaries(analysis_service: AnalysisService = Provide[Container.analysis return jsonify(response.model_dump()), HTTPStatus.OK except ValidationError as error: return jsonify(error=str(error)), HTTPStatus.BAD_REQUEST - except Exception as error: - return jsonify(error=str(error)), HTTPStatus.INTERNAL_SERVER_ERROR @blueprint.route("/info") @@ -163,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 diff --git a/trailblazer/server/utils.py b/trailblazer/server/utils.py index 38448a12..b55fb7dc 100644 --- a/trailblazer/server/utils.py +++ b/trailblazer/server/utils.py @@ -22,21 +22,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) From 768815584c0a6593f8a3d3552a86edd476f00b72 Mon Sep 17 00:00:00 2001 From: seallard Date: Fri, 23 Feb 2024 11:48:06 +0100 Subject: [PATCH 9/9] Remove unused imports --- trailblazer/server/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/trailblazer/server/utils.py b/trailblazer/server/utils.py index b55fb7dc..727632b4 100644 --- a/trailblazer/server/utils.py +++ b/trailblazer/server/utils.py @@ -1,10 +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.summaries_request import SummariesRequest +from trailblazer.dto import AnalysesRequest def parse_analyses_request(request: Request) -> AnalysesRequest: