Skip to content

Commit

Permalink
✨ Add getters for pricing plan and unit (ITISFoundation#4882)
Browse files Browse the repository at this point in the history
  • Loading branch information
bisgaard-itis authored Oct 18, 2023
1 parent faaf530 commit b02f9f8
Show file tree
Hide file tree
Showing 13 changed files with 792 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from fastapi import APIRouter, Depends, HTTPException, status
from httpx import HTTPStatusError
from models_library.api_schemas_webserver.resource_usage import ServicePricingPlanGet
from pydantic import ValidationError
from pydantic.errors import PydanticValueError
from servicelib.error_codes import create_error_code
Expand All @@ -16,6 +17,7 @@
from ..dependencies.application import get_product_name, get_reverse_url_mapper
from ..dependencies.authentication import get_current_user_id
from ..dependencies.services import get_api_client
from ..dependencies.webserver import AuthSession, get_webserver_session
from ._common import API_SERVER_DEV_FEATURES_ENABLED

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -255,3 +257,20 @@ async def list_solver_ports(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ports for solver {solver_key}:{version} not found",
) from err


@router.get(
"/{solver_key:path}/releases/{version}/pricing_plan",
response_model=ServicePricingPlanGet,
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
)
async def get_solver_pricing_plan(
solver_key: SolverKeyId,
version: VersionStr,
user_id: Annotated[int, Depends(get_current_user_id)],
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
product_name: Annotated[str, Depends(get_product_name)],
):
assert user_id
assert product_name
return await webserver_api.get_service_pricing_plan(solver_key, version)
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
from fastapi.responses import RedirectResponse
from fastapi_pagination.api import create_page
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
from models_library.api_schemas_webserver.resource_usage import PricingUnitGet
from models_library.api_schemas_webserver.wallets import WalletGet
from models_library.clusters import ClusterID
from models_library.projects_nodes_io import BaseFileLink
from pydantic.types import PositiveInt
from servicelib.logging_utils import log_context

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


def _raise_if_job_not_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_422_UNPROCESSABLE_ENTITY,
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)
Expand Down Expand Up @@ -559,3 +574,28 @@ async def get_job_wallet(
f"Cannot find job={job_name}",
status_code=status.HTTP_404_NOT_FOUND,
)


@router.get(
"/{solver_key:path}/releases/{version}/jobs/{job_id:uuid}/pricing_unit",
response_model=PricingUnitGet | None,
responses={**_COMMON_ERROR_RESPONSES},
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
)
async def get_job_pricing_unit(
solver_key: SolverKeyId,
version: VersionStr,
job_id: JobID,
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
):
job_name = _compose_job_resource_name(solver_key, version, job_id)
with log_context(_logger, logging.DEBUG, "Get pricing unit"):
_logger.debug("job: %s", job_name)
project: ProjectGet = await webserver_api.get_project(project_id=job_id)
_raise_if_job_not_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])
return await webserver_api.get_project_node_pricing_unit(
project_id=job_id, node_id=node_id
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
ProjectMetadataGet,
ProjectMetadataUpdate,
)
from models_library.api_schemas_webserver.resource_usage import (
PricingUnitGet,
ServicePricingPlanGet,
)
from models_library.api_schemas_webserver.wallets import WalletGet
from models_library.generics import Envelope
from models_library.projects import ProjectID
Expand All @@ -24,6 +28,7 @@
from pydantic.errors import PydanticErrorMixin
from servicelib.aiohttp.long_running_tasks.server import TaskStatus
from servicelib.error_codes import create_error_code
from simcore_service_api_server.models.schemas.solvers import SolverKeyId
from starlette import status
from tenacity import TryAgain
from tenacity._asyncio import AsyncRetrying
Expand All @@ -32,6 +37,7 @@
from tenacity.wait import wait_fixed

from ..core.settings import WebServerSettings
from ..models.basic_types import VersionStr
from ..models.pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
from ..models.schemas.jobs import MetaValueType
from ..models.types import AnyJson
Expand Down Expand Up @@ -87,6 +93,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
Expand Down Expand Up @@ -284,16 +295,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
Expand Down Expand Up @@ -369,14 +379,17 @@ async def update_project_metadata(
assert data # nosec
return data

async def get_project_wallet(self, project_id: ProjectID) -> WalletGet | None:
async def get_project_node_pricing_unit(
self, project_id: UUID, node_id: UUID
) -> PricingUnitGet | None:
with _handle_webserver_api_errors():
response = await self.client.get(
f"/projects/{project_id}/wallet",
f"/projects/{project_id}/nodes/{node_id}/pricing-unit",
cookies=self.session_cookies,
)

response.raise_for_status()
data = Envelope[WalletGet].parse_raw(response.text).data
data = Envelope[PricingUnitGet].parse_raw(response.text).data
return data

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

async def get_project_wallet(self, project_id: ProjectID) -> WalletGet | None:
with _handle_webserver_api_errors():
response = await self.client.get(
f"/projects/{project_id}/wallet",
cookies=self.session_cookies,
)
response.raise_for_status()
data = Envelope[WalletGet].parse_raw(response.text).data
return data

# SERVICES -------------------------------------------------

async def get_service_pricing_plan(
self, solver_key: SolverKeyId, version: VersionStr
) -> ServicePricingPlanGet | None:
service_key = urllib.parse.quote_plus(solver_key)

with _handle_webserver_api_errors():
response = await self.client.get(
f"/catalog/services/{service_key}/{version}/pricing-plan",
cookies=self.session_cookies,
)
response.raise_for_status()
data = Envelope[ServicePricingPlanGet].parse_raw(response.text).data
return data


# MODULES APP SETUP -------------------------------------------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def regex_pattern(self) -> str:
pattern = r"[+-]?\d+(?:\.\d+)?"
elif self.type_ == "str":
if self.format_ == "uuid":
pattern = r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}"
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}"
else:
pattern = r".*" # should match any string
if pattern is None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[
{
"name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50620",
"description": "<Request('GET', 'http://webserver:8080/v0/projects/87643648-3a38-44e2-9cfe-d86ab3d50620')>",
"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
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
[
{
"name": "GET /projects/87643648-3a38-44e2-9cfe-d86ab3d50629",
"description": "<Request('GET', 'http://webserver:8080/v0/projects/87643648-3a38-44e2-9cfe-d86ab3d50629')>",
"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": "[email protected]",
"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
}
]
Loading

0 comments on commit b02f9f8

Please sign in to comment.