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

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent faaf530 commit b02f9f8
Showing 13 changed files with 792 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -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
@@ -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__)
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 -------------------------------------------------
@@ -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 -------------------------------------------------------------

Original file line number Diff line number Diff line change
@@ -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:
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.