diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py index 408be42686c..dcd6b276011 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py @@ -64,6 +64,19 @@ def _compose_job_resource_name(solver_key, solver_version, job_id) -> str: ) +def _assert_project_associated_with_solver( + solver_key: SolverKeyId, version: VersionStr, project: ProjectGet +) -> None: + expected_job_name: str = _compose_job_resource_name( + solver_key, version, project.uuid + ) + if expected_job_name != project.name: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail=f"job {project.uuid} is not associated with solver {solver_key} and version {version}", + ) + + # JOBS --------------- # # - Similar to docker container's API design (container = job and image = solver) @@ -578,6 +591,7 @@ async def get_job_pricing_unit( _logger.debug("Getting wallet for job '%s'", job_name) project: ProjectGet = await webserver_api.get_project(project_id=job_id) + _assert_project_associated_with_solver(solver_key, version, project) node_ids = list(project.workbench.keys()) assert len(node_ids) == 1 # nosec node_id: UUID = UUID(node_ids[0]) diff --git a/services/api-server/src/simcore_service_api_server/services/webserver.py b/services/api-server/src/simcore_service_api_server/services/webserver.py index e1923465513..2a4a225b439 100644 --- a/services/api-server/src/simcore_service_api_server/services/webserver.py +++ b/services/api-server/src/simcore_service_api_server/services/webserver.py @@ -88,6 +88,11 @@ def _handle_webserver_api_errors(): msg = error.get("errors") or resp.reason_phrase or f"{exc}" raise HTTPException(resp.status_code, detail=msg) from exc + except ProjectNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) + ) from exc + class WebserverApi(BaseServiceClientApi): """Access to web-server API @@ -285,16 +290,15 @@ async def clone_project(self, project_id: UUID) -> ProjectGet: return ProjectGet.parse_obj(result) async def get_project(self, project_id: UUID) -> ProjectGet: - response = await self.client.get( - f"/projects/{project_id}", - cookies=self.session_cookies, - ) - - data = self._get_data_or_raise( - response, - {status.HTTP_404_NOT_FOUND: ProjectNotFoundError(project_id=project_id)}, - ) - return ProjectGet.parse_obj(data) + with _handle_webserver_api_errors(): + response = await self.client.get( + f"/projects/{project_id}", + cookies=self.session_cookies, + ) + response.raise_for_status() + data = Envelope[ProjectGet].parse_raw(response.text).data + assert data is not None + return data async def get_projects_w_solver_page( self, solver_name: str, limit: int, offset: int @@ -395,17 +399,16 @@ async def get_wallet(self, wallet_id: int) -> WalletGet: async def get_project_node_pricing_unit( self, project_id: UUID, node_id: UUID - ) -> PricingUnitGet: - response = await self.client.get( - f"/projects/{project_id}/nodes/{node_id}/pricing-unit", - cookies=self.session_cookies, - ) + ) -> PricingUnitGet | None: + with _handle_webserver_api_errors(): + response = await self.client.get( + f"/projects/{project_id}/nodes/{node_id}/pricing-unit", + cookies=self.session_cookies, + ) - data = self._get_data_or_raise( - response, - {status.HTTP_404_NOT_FOUND: ProjectNotFoundError(project_id=project_id)}, - ) - return PricingUnitGet.parse_obj(data) + response.raise_for_status() + data = Envelope[PricingUnitGet].parse_raw(response.text).data + return data # MODULES APP SETUP ------------------------------------------------------------- diff --git a/services/api-server/tests/mocks/get_job_pricing_unit_invalid_job.json b/services/api-server/tests/mocks/get_job_pricing_unit_invalid_job.json new file mode 100644 index 00000000000..e6f79d165cd --- /dev/null +++ b/services/api-server/tests/mocks/get_job_pricing_unit_invalid_job.json @@ -0,0 +1,55 @@ +[ + { + "name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50620", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in_": "path", + "name": "project_id", + "required": true, + "schema_": { + "title": "Project Id", + "type_": "str", + "pattern": null, + "format_": "uuid", + "exclusiveMinimum": null, + "minimum": null, + "anyOf": null, + "allOf": null, + "oneOf": null + }, + "response_value": "projects" + } + ] + }, + "query": null, + "request_payload": null, + "response_body": { + "data": null, + "error": { + "logs": [ + { + "message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found", + "level": "ERROR", + "logger": "user" + } + ], + "errors": [ + { + "code": "HTTPNotFound", + "message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found", + "resource": null, + "field": null + } + ], + "status": 404, + "message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found" + } + }, + "status_code": 404 + } +] diff --git a/services/api-server/tests/mocks/get_job_pricing_unit_invalid_solver.json b/services/api-server/tests/mocks/get_job_pricing_unit_invalid_solver.json new file mode 100644 index 00000000000..c1ccb3755c7 --- /dev/null +++ b/services/api-server/tests/mocks/get_job_pricing_unit_invalid_solver.json @@ -0,0 +1,105 @@ +[ + { + "name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50629", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in_": "path", + "name": "project_id", + "required": true, + "schema_": { + "title": "Project Id", + "type_": "str", + "pattern": null, + "format_": "uuid", + "exclusiveMinimum": null, + "minimum": null, + "anyOf": null, + "allOf": null, + "oneOf": null + }, + "response_value": "projects" + } + ] + }, + "query": null, + "request_payload": null, + "response_body": { + "data": { + "uuid": "87643648-3a38-44e2-9cfe-d86ab3d50629", + "name": "solvers/simcore%2Fservices%2Fcomp%2Fisolve/releases/2.1.24/jobs/87643648-3a38-44e2-9cfe-d86ab3d50629", + "description": "Study associated to solver job:\n{\n \"id\": \"87643648-3a38-44e2-9cfe-d86ab3d50629\",\n \"name\": \"solvers/simcore%2Fservices%2Fcomp%2Fisolve/releases/2.1.24/jobs/87643648-3a38-44e2-9cfe-d86ab3d50629\",\n \"inputs_checksum\": \"015ba4cd5cf00c511a8217deb65c242e3b15dc6ae4b1ecf94982d693887d9e8a\",\n \"created_at\": \"2023-10-10T20:15:22.071797+00:00\"\n}", + "thumbnail": "https://via.placeholder.com/170x120.png", + "creationDate": "2023-10-10T20:15:22.096Z", + "lastChangeDate": "2023-10-10T20:15:22.096Z", + "workbench": { + "4b03863d-107a-5c77-a3ca-c5ba1d7048c0": { + "key": "simcore/services/comp/isolve", + "version": "2.1.24", + "label": "isolve edge", + "progress": 0.0, + "inputs": { + "x": 4.33, + "n": 55, + "title": "Temperature", + "enabled": true, + "input_file": { + "store": 0, + "path": "api/0a3b2c56-dbcd-4871-b93b-d454b7883f9f/input.txt", + "label": "input.txt" + } + }, + "inputsUnits": {}, + "inputNodes": [], + "outputs": {}, + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "3": { + "read": true, + "write": true, + "delete": true + } + }, + "tags": [], + "classifiers": [], + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "ui": { + "workbench": { + "4b03863d-107a-5c77-a3ca-c5ba1d7048c0": { + "position": { + "x": 633, + "y": 229 + } + } + }, + "slideshow": {}, + "currentNodeId": "4b03863d-107a-5c77-a3ca-c5ba1d7048c0", + "annotations": {} + }, + "quality": {}, + "dev": {} + } + }, + "status_code": 200 + } +] diff --git a/services/api-server/tests/mocks/get_job_pricing_unit_success.json b/services/api-server/tests/mocks/get_job_pricing_unit_success.json new file mode 100644 index 00000000000..6f1efe838fc --- /dev/null +++ b/services/api-server/tests/mocks/get_job_pricing_unit_success.json @@ -0,0 +1,162 @@ +[ + { + "name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50629", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in_": "path", + "name": "project_id", + "required": true, + "schema_": { + "title": "Project Id", + "type_": "str", + "pattern": null, + "format_": "uuid", + "exclusiveMinimum": null, + "minimum": null, + "anyOf": null, + "allOf": null, + "oneOf": null + }, + "response_value": "projects" + } + ] + }, + "query": null, + "request_payload": null, + "response_body": { + "data": { + "uuid": "87643648-3a38-44e2-9cfe-d86ab3d50629", + "name": "solvers/simcore%2Fservices%2Fcomp%2Fisolve/releases/2.1.24/jobs/87643648-3a38-44e2-9cfe-d86ab3d50629", + "description": "Study associated to solver job:\n{\n \"id\": \"87643648-3a38-44e2-9cfe-d86ab3d50629\",\n \"name\": \"solvers/simcore%2Fservices%2Fcomp%2Fisolve/releases/2.1.24/jobs/87643648-3a38-44e2-9cfe-d86ab3d50629\",\n \"inputs_checksum\": \"015ba4cd5cf00c511a8217deb65c242e3b15dc6ae4b1ecf94982d693887d9e8a\",\n \"created_at\": \"2023-10-10T20:15:22.071797+00:00\"\n}", + "thumbnail": "https://via.placeholder.com/170x120.png", + "creationDate": "2023-10-10T20:15:22.096Z", + "lastChangeDate": "2023-10-10T20:15:22.096Z", + "workbench": { + "4b03863d-107a-5c77-a3ca-c5ba1d7048c0": { + "key": "simcore/services/comp/isolve", + "version": "2.1.24", + "label": "isolve edge", + "progress": 0.0, + "inputs": { + "x": 4.33, + "n": 55, + "title": "Temperature", + "enabled": true, + "input_file": { + "store": 0, + "path": "api/0a3b2c56-dbcd-4871-b93b-d454b7883f9f/input.txt", + "label": "input.txt" + } + }, + "inputsUnits": {}, + "inputNodes": [], + "outputs": {}, + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "3": { + "read": true, + "write": true, + "delete": true + } + }, + "tags": [], + "classifiers": [], + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "ui": { + "workbench": { + "4b03863d-107a-5c77-a3ca-c5ba1d7048c0": { + "position": { + "x": 633, + "y": 229 + } + } + }, + "slideshow": {}, + "currentNodeId": "4b03863d-107a-5c77-a3ca-c5ba1d7048c0", + "annotations": {} + }, + "quality": {}, + "dev": {} + } + }, + "status_code": 200 + }, + { + "name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50629/nodes/4b03863d-107a-5c77-a3ca-c5ba1d7048c0/pricing-unit", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}/nodes/{node_id}/pricing-unit", + "path_parameters": [ + { + "in_": "path", + "name": "node_id", + "required": true, + "schema_": { + "title": "Node Id", + "type_": "str", + "pattern": null, + "format_": "uuid", + "exclusiveMinimum": null, + "minimum": null, + "anyOf": null, + "allOf": null, + "oneOf": null + }, + "response_value": "nodes" + }, + { + "in_": "path", + "name": "project_id", + "required": true, + "schema_": { + "title": "Project Id", + "type_": "str", + "pattern": null, + "format_": "uuid", + "exclusiveMinimum": null, + "minimum": null, + "anyOf": null, + "allOf": null, + "oneOf": null + }, + "response_value": "projects" + } + ] + }, + "query": null, + "request_payload": null, + "response_body": { + "data": { + "pricingUnitId": 1, + "unitName": "small", + "unitExtraInfo": {}, + "currentCostPerUnit": 50, + "default": true + } + }, + "status_code": 200 + } +]