Skip to content

Commit b02f9f8

Browse files
✨ Add getters for pricing plan and unit (#4882)
1 parent faaf530 commit b02f9f8

13 files changed

+792
-30
lines changed

services/api-server/src/simcore_service_api_server/api/routes/solvers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from fastapi import APIRouter, Depends, HTTPException, status
77
from httpx import HTTPStatusError
8+
from models_library.api_schemas_webserver.resource_usage import ServicePricingPlanGet
89
from pydantic import ValidationError
910
from pydantic.errors import PydanticValueError
1011
from servicelib.error_codes import create_error_code
@@ -16,6 +17,7 @@
1617
from ..dependencies.application import get_product_name, get_reverse_url_mapper
1718
from ..dependencies.authentication import get_current_user_id
1819
from ..dependencies.services import get_api_client
20+
from ..dependencies.webserver import AuthSession, get_webserver_session
1921
from ._common import API_SERVER_DEV_FEATURES_ENABLED
2022

2123
_logger = logging.getLogger(__name__)
@@ -255,3 +257,20 @@ async def list_solver_ports(
255257
status_code=status.HTTP_404_NOT_FOUND,
256258
detail=f"Ports for solver {solver_key}:{version} not found",
257259
) from err
260+
261+
262+
@router.get(
263+
"/{solver_key:path}/releases/{version}/pricing_plan",
264+
response_model=ServicePricingPlanGet,
265+
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
266+
)
267+
async def get_solver_pricing_plan(
268+
solver_key: SolverKeyId,
269+
version: VersionStr,
270+
user_id: Annotated[int, Depends(get_current_user_id)],
271+
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
272+
product_name: Annotated[str, Depends(get_product_name)],
273+
):
274+
assert user_id
275+
assert product_name
276+
return await webserver_api.get_service_pricing_plan(solver_key, version)

services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from fastapi.responses import RedirectResponse
1212
from fastapi_pagination.api import create_page
1313
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
14+
from models_library.api_schemas_webserver.resource_usage import PricingUnitGet
1415
from models_library.api_schemas_webserver.wallets import WalletGet
1516
from models_library.clusters import ClusterID
1617
from models_library.projects_nodes_io import BaseFileLink
1718
from pydantic.types import PositiveInt
19+
from servicelib.logging_utils import log_context
1820

1921
from ...db.repositories.groups_extra_properties import GroupsExtraPropertiesRepository
2022
from ...models.basic_types import VersionStr
@@ -63,6 +65,19 @@ def _compose_job_resource_name(solver_key, solver_version, job_id) -> str:
6365
)
6466

6567

68+
def _raise_if_job_not_associated_with_solver(
69+
solver_key: SolverKeyId, version: VersionStr, project: ProjectGet
70+
) -> None:
71+
expected_job_name: str = _compose_job_resource_name(
72+
solver_key, version, project.uuid
73+
)
74+
if expected_job_name != project.name:
75+
raise HTTPException(
76+
status.HTTP_422_UNPROCESSABLE_ENTITY,
77+
detail=f"job {project.uuid} is not associated with solver {solver_key} and version {version}",
78+
)
79+
80+
6681
# JOBS ---------------
6782
#
6883
# - Similar to docker container's API design (container = job and image = solver)
@@ -559,3 +574,28 @@ async def get_job_wallet(
559574
f"Cannot find job={job_name}",
560575
status_code=status.HTTP_404_NOT_FOUND,
561576
)
577+
578+
579+
@router.get(
580+
"/{solver_key:path}/releases/{version}/jobs/{job_id:uuid}/pricing_unit",
581+
response_model=PricingUnitGet | None,
582+
responses={**_COMMON_ERROR_RESPONSES},
583+
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
584+
)
585+
async def get_job_pricing_unit(
586+
solver_key: SolverKeyId,
587+
version: VersionStr,
588+
job_id: JobID,
589+
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
590+
):
591+
job_name = _compose_job_resource_name(solver_key, version, job_id)
592+
with log_context(_logger, logging.DEBUG, "Get pricing unit"):
593+
_logger.debug("job: %s", job_name)
594+
project: ProjectGet = await webserver_api.get_project(project_id=job_id)
595+
_raise_if_job_not_associated_with_solver(solver_key, version, project)
596+
node_ids = list(project.workbench.keys())
597+
assert len(node_ids) == 1 # nosec
598+
node_id: UUID = UUID(node_ids[0])
599+
return await webserver_api.get_project_node_pricing_unit(
600+
project_id=job_id, node_id=node_id
601+
)

services/api-server/src/simcore_service_api_server/services/webserver.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
ProjectMetadataGet,
1616
ProjectMetadataUpdate,
1717
)
18+
from models_library.api_schemas_webserver.resource_usage import (
19+
PricingUnitGet,
20+
ServicePricingPlanGet,
21+
)
1822
from models_library.api_schemas_webserver.wallets import WalletGet
1923
from models_library.generics import Envelope
2024
from models_library.projects import ProjectID
@@ -24,6 +28,7 @@
2428
from pydantic.errors import PydanticErrorMixin
2529
from servicelib.aiohttp.long_running_tasks.server import TaskStatus
2630
from servicelib.error_codes import create_error_code
31+
from simcore_service_api_server.models.schemas.solvers import SolverKeyId
2732
from starlette import status
2833
from tenacity import TryAgain
2934
from tenacity._asyncio import AsyncRetrying
@@ -32,6 +37,7 @@
3237
from tenacity.wait import wait_fixed
3338

3439
from ..core.settings import WebServerSettings
40+
from ..models.basic_types import VersionStr
3541
from ..models.pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
3642
from ..models.schemas.jobs import MetaValueType
3743
from ..models.types import AnyJson
@@ -87,6 +93,11 @@ def _handle_webserver_api_errors():
8793
msg = error.get("errors") or resp.reason_phrase or f"{exc}"
8894
raise HTTPException(resp.status_code, detail=msg) from exc
8995

96+
except ProjectNotFoundError as exc:
97+
raise HTTPException(
98+
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
99+
) from exc
100+
90101

91102
class WebserverApi(BaseServiceClientApi):
92103
"""Access to web-server API
@@ -284,16 +295,15 @@ async def clone_project(self, project_id: UUID) -> ProjectGet:
284295
return ProjectGet.parse_obj(result)
285296

286297
async def get_project(self, project_id: UUID) -> ProjectGet:
287-
response = await self.client.get(
288-
f"/projects/{project_id}",
289-
cookies=self.session_cookies,
290-
)
291-
292-
data = self._get_data_or_raise(
293-
response,
294-
{status.HTTP_404_NOT_FOUND: ProjectNotFoundError(project_id=project_id)},
295-
)
296-
return ProjectGet.parse_obj(data)
298+
with _handle_webserver_api_errors():
299+
response = await self.client.get(
300+
f"/projects/{project_id}",
301+
cookies=self.session_cookies,
302+
)
303+
response.raise_for_status()
304+
data = Envelope[ProjectGet].parse_raw(response.text).data
305+
assert data is not None
306+
return data
297307

298308
async def get_projects_w_solver_page(
299309
self, solver_name: str, limit: int, offset: int
@@ -369,14 +379,17 @@ async def update_project_metadata(
369379
assert data # nosec
370380
return data
371381

372-
async def get_project_wallet(self, project_id: ProjectID) -> WalletGet | None:
382+
async def get_project_node_pricing_unit(
383+
self, project_id: UUID, node_id: UUID
384+
) -> PricingUnitGet | None:
373385
with _handle_webserver_api_errors():
374386
response = await self.client.get(
375-
f"/projects/{project_id}/wallet",
387+
f"/projects/{project_id}/nodes/{node_id}/pricing-unit",
376388
cookies=self.session_cookies,
377389
)
390+
378391
response.raise_for_status()
379-
data = Envelope[WalletGet].parse_raw(response.text).data
392+
data = Envelope[PricingUnitGet].parse_raw(response.text).data
380393
return data
381394

382395
# WALLETS -------------------------------------------------
@@ -392,6 +405,32 @@ async def get_wallet(self, wallet_id: int) -> WalletGet:
392405
assert data # nosec
393406
return data
394407

408+
async def get_project_wallet(self, project_id: ProjectID) -> WalletGet | None:
409+
with _handle_webserver_api_errors():
410+
response = await self.client.get(
411+
f"/projects/{project_id}/wallet",
412+
cookies=self.session_cookies,
413+
)
414+
response.raise_for_status()
415+
data = Envelope[WalletGet].parse_raw(response.text).data
416+
return data
417+
418+
# SERVICES -------------------------------------------------
419+
420+
async def get_service_pricing_plan(
421+
self, solver_key: SolverKeyId, version: VersionStr
422+
) -> ServicePricingPlanGet | None:
423+
service_key = urllib.parse.quote_plus(solver_key)
424+
425+
with _handle_webserver_api_errors():
426+
response = await self.client.get(
427+
f"/catalog/services/{service_key}/{version}/pricing-plan",
428+
cookies=self.session_cookies,
429+
)
430+
response.raise_for_status()
431+
data = Envelope[ServicePricingPlanGet].parse_raw(response.text).data
432+
return data
433+
395434

396435
# MODULES APP SETUP -------------------------------------------------------------
397436

services/api-server/src/simcore_service_api_server/utils/http_calls_capture_processing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def regex_pattern(self) -> str:
9696
pattern = r"[+-]?\d+(?:\.\d+)?"
9797
elif self.type_ == "str":
9898
if self.format_ == "uuid":
99-
pattern = r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}"
99+
pattern = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-(3|4|5)[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
100100
else:
101101
pattern = r".*" # should match any string
102102
if pattern is None:
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[
2+
{
3+
"name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50620",
4+
"description": "<Request('GET', 'http://webserver:8080/v0/projects/87643648-3a38-44e2-9cfe-d86ab3d50620')>",
5+
"method": "GET",
6+
"host": "webserver",
7+
"path": {
8+
"path": "/v0/projects/{project_id}",
9+
"path_parameters": [
10+
{
11+
"in_": "path",
12+
"name": "project_id",
13+
"required": true,
14+
"schema_": {
15+
"title": "Project Id",
16+
"type_": "str",
17+
"pattern": null,
18+
"format_": "uuid",
19+
"exclusiveMinimum": null,
20+
"minimum": null,
21+
"anyOf": null,
22+
"allOf": null,
23+
"oneOf": null
24+
},
25+
"response_value": "projects"
26+
}
27+
]
28+
},
29+
"query": null,
30+
"request_payload": null,
31+
"response_body": {
32+
"data": null,
33+
"error": {
34+
"logs": [
35+
{
36+
"message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found",
37+
"level": "ERROR",
38+
"logger": "user"
39+
}
40+
],
41+
"errors": [
42+
{
43+
"code": "HTTPNotFound",
44+
"message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found",
45+
"resource": null,
46+
"field": null
47+
}
48+
],
49+
"status": 404,
50+
"message": "Project 87643648-3a38-44e2-9cfe-d86ab3d50620 not found"
51+
}
52+
},
53+
"status_code": 404
54+
}
55+
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
[
2+
{
3+
"name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50629",
4+
"description": "<Request('GET', 'http://webserver:8080/v0/projects/87643648-3a38-44e2-9cfe-d86ab3d50629')>",
5+
"method": "GET",
6+
"host": "webserver",
7+
"path": {
8+
"path": "/v0/projects/{project_id}",
9+
"path_parameters": [
10+
{
11+
"in_": "path",
12+
"name": "project_id",
13+
"required": true,
14+
"schema_": {
15+
"title": "Project Id",
16+
"type_": "str",
17+
"pattern": null,
18+
"format_": "uuid",
19+
"exclusiveMinimum": null,
20+
"minimum": null,
21+
"anyOf": null,
22+
"allOf": null,
23+
"oneOf": null
24+
},
25+
"response_value": "projects"
26+
}
27+
]
28+
},
29+
"query": null,
30+
"request_payload": null,
31+
"response_body": {
32+
"data": {
33+
"uuid": "87643648-3a38-44e2-9cfe-d86ab3d50629",
34+
"name": "solvers/simcore%2Fservices%2Fcomp%2Fisolve/releases/2.1.24/jobs/87643648-3a38-44e2-9cfe-d86ab3d50629",
35+
"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}",
36+
"thumbnail": "https://via.placeholder.com/170x120.png",
37+
"creationDate": "2023-10-10T20:15:22.096Z",
38+
"lastChangeDate": "2023-10-10T20:15:22.096Z",
39+
"workbench": {
40+
"4b03863d-107a-5c77-a3ca-c5ba1d7048c0": {
41+
"key": "simcore/services/comp/isolve",
42+
"version": "2.1.24",
43+
"label": "isolve edge",
44+
"progress": 0.0,
45+
"inputs": {
46+
"x": 4.33,
47+
"n": 55,
48+
"title": "Temperature",
49+
"enabled": true,
50+
"input_file": {
51+
"store": 0,
52+
"path": "api/0a3b2c56-dbcd-4871-b93b-d454b7883f9f/input.txt",
53+
"label": "input.txt"
54+
}
55+
},
56+
"inputsUnits": {},
57+
"inputNodes": [],
58+
"outputs": {},
59+
"state": {
60+
"modified": true,
61+
"dependencies": [],
62+
"currentStatus": "NOT_STARTED",
63+
"progress": null
64+
}
65+
}
66+
},
67+
"prjOwner": "[email protected]",
68+
"accessRights": {
69+
"3": {
70+
"read": true,
71+
"write": true,
72+
"delete": true
73+
}
74+
},
75+
"tags": [],
76+
"classifiers": [],
77+
"state": {
78+
"locked": {
79+
"value": false,
80+
"status": "CLOSED"
81+
},
82+
"state": {
83+
"value": "NOT_STARTED"
84+
}
85+
},
86+
"ui": {
87+
"workbench": {
88+
"4b03863d-107a-5c77-a3ca-c5ba1d7048c0": {
89+
"position": {
90+
"x": 633,
91+
"y": 229
92+
}
93+
}
94+
},
95+
"slideshow": {},
96+
"currentNodeId": "4b03863d-107a-5c77-a3ca-c5ba1d7048c0",
97+
"annotations": {}
98+
},
99+
"quality": {},
100+
"dev": {}
101+
}
102+
},
103+
"status_code": 200
104+
}
105+
]

0 commit comments

Comments
 (0)