diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_checkouts.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_checkouts.py index c8fd22ce581..9e032a487fb 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_checkouts.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_checkouts.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import NamedTuple -from pydantic import BaseModel, PositiveInt +from pydantic import BaseModel, ConfigDict, PositiveInt from ..licensed_items import LicensedItemID from ..products import ProductName @@ -22,6 +22,22 @@ class LicensedItemCheckoutRpcGet(BaseModel): started_at: datetime stopped_at: datetime | None num_of_seats: int + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + "licensed_item_checkout_id": "633ef980-6f3e-4b1a-989a-bd77bf9a5d6b", + "licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1", + "wallet_id": 6, + "user_id": 27845, + "product_name": "osparc", + "started_at": "2024-12-12 09:59:26.422140", + "stopped_at": "2024-12-12 09:59:26.423540", + "num_of_seats": 78, + } + ] + } + ) class LicensedItemCheckoutRpcGetPage(NamedTuple): diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/application.py b/services/api-server/src/simcore_service_api_server/api/dependencies/application.py index 1cf54c374ca..e7ccf5691f0 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/application.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/application.py @@ -15,6 +15,6 @@ def get_settings(request: Request) -> ApplicationSettings: assert get_app # nosec __all__: tuple[str, ...] = ( - "get_reverse_url_mapper", "get_app", + "get_reverse_url_mapper", ) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/resource_usage_tracker_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/resource_usage_tracker_rpc.py new file mode 100644 index 00000000000..e55fa59ca6d --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/resource_usage_tracker_rpc.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from servicelib.fastapi.dependencies import get_app + +from ...services_rpc.resource_usage_tracker import ResourceUsageTrackerClient + + +async def get_resource_usage_tracker_client( + app: Annotated[FastAPI, Depends(get_app)] +) -> ResourceUsageTrackerClient: + return ResourceUsageTrackerClient.get_from_app_state(app=app) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py index df47e0a5be9..19887e1e1c8 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py @@ -1,4 +1,4 @@ -from typing import Annotated, cast +from typing import Annotated from fastapi import Depends, FastAPI from servicelib.fastapi.dependencies import get_app @@ -9,5 +9,4 @@ async def get_wb_api_rpc_client( app: Annotated[FastAPI, Depends(get_app)] ) -> WbApiRpcClient: - assert app.state.wb_api_rpc_client # nosec - return cast(WbApiRpcClient, app.state.wb_api_rpc_client) + return WbApiRpcClient.get_from_app_state(app=app) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/files.py b/services/api-server/src/simcore_service_api_server/api/routes/files.py index 2187e68ad06..5bfa3ab030d 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/files.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/files.py @@ -116,7 +116,7 @@ async def list_files( file_meta: File = to_file_api_model(stored_file_meta) - except (ValidationError, ValueError, AttributeError) as err: # noqa: PERF203 + except (ValidationError, ValueError, AttributeError) as err: _logger.warning( "Skipping corrupted entry in storage '%s' (%s)" "TIP: check this entry in file_meta_data table.", @@ -186,7 +186,7 @@ async def upload_file( file_meta: File = await File.create_from_uploaded( file, file_size=file_size, - created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(), + created_at=datetime.datetime.now(datetime.UTC).isoformat(), ) _logger.debug( "Assigned id: %s of %s bytes (content-length), real size %s bytes", @@ -242,7 +242,7 @@ async def get_upload_links( assert request # nosec file_meta: File = await File.create_from_client_file( client_file, - datetime.datetime.now(datetime.timezone.utc).isoformat(), + datetime.datetime.now(datetime.UTC).isoformat(), ) _, upload_links = await get_upload_links_from_s3( user_id=user_id, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/health.py b/services/api-server/src/simcore_service_api_server/api/routes/health.py index 0b7bfbcbc9f..a2f445c7a57 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/health.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/health.py @@ -29,7 +29,7 @@ async def check_service_health( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="unhealthy" ) - return f"{__name__}@{datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}" + return f"{__name__}@{datetime.datetime.now(tz=datetime.UTC).isoformat()}" @router.get( diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index d02151080ad..58c2a695f90 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -1,12 +1,21 @@ from typing import Annotated, Any -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, HTTPException, status +from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker_licensed_items_checkouts import ( + LicensedItemCheckoutID, +) +from pydantic import PositiveInt +from simcore_service_api_server.api.dependencies.resource_usage_tracker_rpc import ( + get_resource_usage_tracker_client, +) -from ...api.dependencies.authentication import get_product_name +from ...api.dependencies.authentication import get_current_user_id, get_product_name from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.pagination import Page, PaginationParams -from ...models.schemas.model_adapter import LicensedItemGet +from ...models.schemas.model_adapter import LicensedItemCheckoutGet, LicensedItemGet +from ...services_rpc.resource_usage_tracker import ResourceUsageTrackerClient from ...services_rpc.wb_api_server import WbApiRpcClient router = APIRouter() @@ -32,3 +41,36 @@ async def get_licensed_items( return await web_api_rpc.get_licensed_items( product_name=product_name, page_params=page_params ) + + +@router.post( + "/{licensed_item_id}/checked-out-items/{licensed_item_checkout_id}/release", + response_model=LicensedItemCheckoutGet, + status_code=status.HTTP_200_OK, + responses=_LICENSE_ITEMS_STATUS_CODES, + description="Release previously checked out licensed item", + include_in_schema=False, +) +async def release_licensed_item( + web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + rut_rpc: Annotated[ + ResourceUsageTrackerClient, Depends(get_resource_usage_tracker_client) + ], + product_name: Annotated[str, Depends(get_product_name)], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + licensed_item_id: LicensedItemID, + licensed_item_checkout_id: LicensedItemCheckoutID, +): + _licensed_item_checkout = await rut_rpc.get_licensed_item_checkout( + product_name=product_name, licensed_item_checkout_id=licensed_item_checkout_id + ) + if _licensed_item_checkout.licensed_item_id != licensed_item_id: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"{licensed_item_id} is not the license_item_id associated with the checked out item {licensed_item_checkout_id}", + ) + return await web_api_rpc.release_licensed_item_for_wallet( + product_name=product_name, + user_id=user_id, + licensed_item_checkout_id=licensed_item_checkout_id, + ) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py index ca34726eae3..e992d94704a 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py @@ -2,10 +2,21 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, status +from models_library.licensed_items import LicensedItemID +from pydantic import PositiveInt +from ...api.dependencies.authentication import get_current_user_id, get_product_name +from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES +from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet -from ...models.schemas.model_adapter import WalletGetWithAvailableCreditsLegacy +from ...models.schemas.licensed_items import LicensedItemCheckoutData +from ...models.schemas.model_adapter import ( + LicensedItemCheckoutGet, + LicensedItemGet, + WalletGetWithAvailableCreditsLegacy, +) +from ...services_rpc.wb_api_server import WbApiRpcClient from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION @@ -49,3 +60,52 @@ async def get_wallet( webserver_api: Annotated[AuthSession, Depends(get_webserver_session)], ): return await webserver_api.get_wallet(wallet_id=wallet_id) + + +@router.get( + "/{wallet_id}/licensed-items", + response_model=Page[LicensedItemGet], + status_code=status.HTTP_200_OK, + responses=WALLET_STATUS_CODES, + description="Get all available licensed items for a given wallet", + include_in_schema=False, +) +async def get_available_licensed_items_for_wallet( + wallet_id: int, + page_params: Annotated[PaginationParams, Depends()], + web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + product_name: Annotated[str, Depends(get_product_name)], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], +): + return await web_api_rpc.get_available_licensed_items_for_wallet( + product_name=product_name, + wallet_id=wallet_id, + user_id=user_id, + page_params=page_params, + ) + + +@router.post( + "/{wallet_id}/licensed-items/{licensed_item_id}/checkout", + response_model=LicensedItemCheckoutGet, + status_code=status.HTTP_200_OK, + responses=WALLET_STATUS_CODES, + description="Checkout licensed item", + include_in_schema=False, +) +async def checkout_licensed_item( + wallet_id: int, + licensed_item_id: LicensedItemID, + web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + product_name: Annotated[str, Depends(get_product_name)], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + checkout_data: LicensedItemCheckoutData, +): + return await web_api_rpc.checkout_licensed_item_for_wallet( + product_name=product_name, + user_id=user_id, + wallet_id=wallet_id, + licensed_item_id=licensed_item_id, + num_of_seats=checkout_data.number_of_seats, + service_run_id=checkout_data.service_run_id, + ) diff --git a/services/api-server/src/simcore_service_api_server/db/tables.py b/services/api-server/src/simcore_service_api_server/db/tables.py index e07cfdb3792..8444fc7c4e5 100644 --- a/services/api-server/src/simcore_service_api_server/db/tables.py +++ b/services/api-server/src/simcore_service_api_server/db/tables.py @@ -4,14 +4,14 @@ from simcore_postgres_database.models.users import UserRole, UserStatus, users __all__: tuple[str, ...] = ( + "GroupType", + "UserRole", + "UserStatus", "api_keys", "groups", - "GroupType", "metadata", "user_to_groups", - "UserRole", "users", - "UserStatus", ) # nopycln: file # noqa: ERA001 diff --git a/services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py b/services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py index 0a23d0400f7..11c3e65a28e 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py @@ -105,3 +105,18 @@ class PricingPlanNotFoundError(BaseBackEndError): class ProjectAlreadyStartedError(BaseBackEndError): msg_template = "Project already started" status_code = status.HTTP_200_OK + + +class InsufficientNumberOfSeatsError(BaseBackEndError): + msg_template = "Not enough available seats. Current available seats {num_of_seats} for license item {licensed_item_id}" + status_code = status.HTTP_409_CONFLICT + + +class CanNotCheckoutServiceIsNotRunningError(BaseBackEndError): + msg_template = "Can not checkout license item {licensed_item_id} as dynamic service is not running. Current service id: {service_run_id}" + status_code = status.HTTP_422_UNPROCESSABLE_ENTITY + + +class LicensedItemCheckoutNotFoundError(BaseBackEndError): + msg_template = "Licensed item checkout {licensed_item_checkout_id} not found." + status_code = status.HTTP_404_NOT_FOUND diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_utils.py index cb2c382bf3a..da741fdb8b4 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_utils.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, TypeAlias +from collections.abc import Awaitable, Callable +from typing import Any, TypeAlias from fastapi.encoders import jsonable_encoder from fastapi.requests import Request diff --git a/services/api-server/src/simcore_service_api_server/models/pagination.py b/services/api-server/src/simcore_service_api_server/models/pagination.py index e2578437653..c8f883f05e4 100644 --- a/services/api-server/src/simcore_service_api_server/models/pagination.py +++ b/services/api-server/src/simcore_service_api_server/models/pagination.py @@ -88,8 +88,8 @@ def _check_total(cls, v, info: ValidationInfo): __all__: tuple[str, ...] = ( - "PaginationParams", "MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE", "OnePage", "Page", + "PaginationParams", ) diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/files.py b/services/api-server/src/simcore_service_api_server/models/schemas/files.py index 1a188e87740..78651edfef1 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/files.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/files.py @@ -45,7 +45,7 @@ class File(BaseModel): # WARNING: from pydantic import File as FileParam # NOTE: see https://ant.apache.org/manual/Tasks/checksum.html - id: UUID = Field(..., description="Resource identifier") # noqa: A003 + id: UUID = Field(..., description="Resource identifier") filename: str = Field(..., description="Name of the file with extension") content_type: str | None = Field( diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/jobs.py b/services/api-server/src/simcore_service_api_server/models/schemas/jobs.py index cdd01eacd35..b0616170501 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/jobs.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/jobs.py @@ -189,7 +189,7 @@ class JobMetadata(BaseModel): class Job(BaseModel): - id: JobID # noqa: A003 + id: JobID name: RelativeResourceName inputs_checksum: str = Field(..., description="Input's checksum") @@ -248,7 +248,7 @@ def create_now( id=global_uuid, runner_name=parent_name, inputs_checksum=inputs_checksum, - created_at=datetime.datetime.now(tz=datetime.timezone.utc), + created_at=datetime.datetime.now(tz=datetime.UTC), url=None, runner_url=None, outputs_url=None, diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/licensed_items.py b/services/api-server/src/simcore_service_api_server/models/schemas/licensed_items.py new file mode 100644 index 00000000000..4080d4089ed --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/models/schemas/licensed_items.py @@ -0,0 +1,7 @@ +from models_library.services_types import ServiceRunID +from pydantic import BaseModel, PositiveInt + + +class LicensedItemCheckoutData(BaseModel): + number_of_seats: PositiveInt + service_run_id: ServiceRunID diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 06da65580f8..de929e07f36 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -10,6 +10,9 @@ from models_library.api_schemas_webserver.licensed_items import ( LicensedItemGet as _LicensedItemGet, ) +from models_library.api_schemas_webserver.licensed_items_checkouts import ( + LicensedItemCheckoutRpcGet as _LicensedItemCheckoutRpcGet, +) from models_library.api_schemas_webserver.product import ( GetCreditPrice as _GetCreditPrice, ) @@ -22,12 +25,17 @@ from models_library.basic_types import IDStr, NonNegativeDecimal from models_library.groups import GroupID from models_library.licensed_items import LicensedItemID, LicensedResourceType +from models_library.products import ProductName from models_library.resource_tracker import ( PricingPlanClassification, PricingPlanId, PricingUnitId, UnitExtraInfo, ) +from models_library.resource_tracker_licensed_items_checkouts import ( + LicensedItemCheckoutID, +) +from models_library.users import UserID from models_library.wallets import WalletID, WalletStatus from pydantic import ( BaseModel, @@ -130,6 +138,7 @@ class ServicePricingPlanGetLegacy(BaseModel): class LicensedItemGet(BaseModel): licensed_item_id: LicensedItemID name: Annotated[str, Field(alias="display_name")] + license_key: str | None licensed_resource_type: LicensedResourceType pricing_plan_id: PricingPlanId created_at: datetime @@ -141,7 +150,20 @@ class LicensedItemGet(BaseModel): assert set(LicensedItemGet.model_fields.keys()) == set( _LicensedItemGet.model_fields.keys() - - { - "license_key" - } # NOTE: @bisgaard-itis please expose https://github.com/ITISFoundation/osparc-simcore/issues/6875 +) + + +class LicensedItemCheckoutGet(BaseModel): + licensed_item_checkout_id: LicensedItemCheckoutID + licensed_item_id: LicensedItemID + wallet_id: WalletID + user_id: UserID + product_name: ProductName + started_at: datetime + stopped_at: datetime | None + num_of_seats: int + + +assert set(LicensedItemCheckoutGet.model_fields.keys()) == set( + _LicensedItemCheckoutRpcGet.model_fields.keys() ) diff --git a/services/api-server/src/simcore_service_api_server/services_http/catalog.py b/services/api-server/src/simcore_service_api_server/services_http/catalog.py index c380025c672..f20264087e1 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/catalog.py +++ b/services/api-server/src/simcore_service_api_server/services_http/catalog.py @@ -125,7 +125,7 @@ async def list_solvers( if predicate is None or predicate(solver): solvers.append(solver) - except ValidationError as err: # noqa: PERF203 + except ValidationError as err: # NOTE: For the moment, this is necessary because there are no guarantees # at the image registry. Therefore we exclude and warn # invalid items instead of returning error diff --git a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py index 07201402876..d2daaf82434 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py @@ -4,11 +4,11 @@ from servicelib.rabbitmq import RabbitMQClient, wait_till_rabbitmq_responsive from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from settings_library.rabbit import RabbitSettings -from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client -from simcore_service_api_server.services_rpc import wb_api_server +from ..api.dependencies.rabbitmq import get_rabbitmq_rpc_client from ..core.health_checker import ApiServerHealthChecker from ..services_http.log_streaming import LogDistributor +from ..services_rpc import resource_usage_tracker, wb_api_server _logger = logging.getLogger(__name__) @@ -39,6 +39,7 @@ async def _on_startup() -> None: app.state.settings.API_SERVER_HEALTH_CHECK_TASK_PERIOD_SECONDS ) wb_api_server.setup(app, get_rabbitmq_rpc_client(app)) + resource_usage_tracker.setup(app, get_rabbitmq_rpc_client(app)) async def _on_shutdown() -> None: if app.state.health_checker: diff --git a/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py index f95061c0ded..44c5ca993aa 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py +++ b/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py @@ -6,7 +6,7 @@ import urllib.parse import uuid from collections.abc import Callable -from datetime import datetime, timezone +from datetime import UTC, datetime from functools import lru_cache import arrow @@ -45,7 +45,7 @@ def format_datetime(snapshot: datetime) -> str: def now_str() -> str: # NOTE: backend MUST use UTC - return format_datetime(datetime.now(timezone.utc)) + return format_datetime(datetime.now(UTC)) # CONVERTERS -------------- diff --git a/services/api-server/src/simcore_service_api_server/services_http/storage.py b/services/api-server/src/simcore_service_api_server/services_http/storage.py index aa8b724ce98..9616b1541bd 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/storage.py +++ b/services/api-server/src/simcore_service_api_server/services_http/storage.py @@ -232,8 +232,8 @@ def setup( __all__: tuple[str, ...] = ( - "setup", "StorageApi", "StorageFileMetaData", + "setup", "to_file_api_model", ) diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/resource_usage_tracker.py b/services/api-server/src/simcore_service_api_server/services_rpc/resource_usage_tracker.py new file mode 100644 index 00000000000..371263a9880 --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/services_rpc/resource_usage_tracker.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from functools import partial + +from fastapi import FastAPI +from models_library.resource_tracker_licensed_items_checkouts import ( + LicensedItemCheckoutID, +) +from servicelib.fastapi.app_state import SingletonInAppStateMixin +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + LicensedItemCheckoutNotFoundError as _LicensedItemCheckoutNotFoundError, +) +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.licensed_items_checkouts import ( + get_licensed_item_checkout as _get_licensed_item_checkout, +) + +from ..exceptions.backend_errors import LicensedItemCheckoutNotFoundError +from ..exceptions.service_errors_utils import service_exception_mapper +from ..models.schemas.model_adapter import LicensedItemCheckoutGet + +_exception_mapper = partial( + service_exception_mapper, service_name="ResourceUsageTracker" +) + + +@dataclass +class ResourceUsageTrackerClient(SingletonInAppStateMixin): + app_state_name = "resource_usage_tracker_rpc_client" + _client: RabbitMQRPCClient + + @_exception_mapper( + rpc_exception_map={ + _LicensedItemCheckoutNotFoundError: LicensedItemCheckoutNotFoundError + } + ) + async def get_licensed_item_checkout( + self, *, product_name: str, licensed_item_checkout_id: LicensedItemCheckoutID + ) -> LicensedItemCheckoutGet: + _licensed_item_checkout = await _get_licensed_item_checkout( + rabbitmq_rpc_client=self._client, + product_name=product_name, + licensed_item_checkout_id=licensed_item_checkout_id, + ) + return LicensedItemCheckoutGet( + licensed_item_checkout_id=_licensed_item_checkout.licensed_item_checkout_id, + licensed_item_id=_licensed_item_checkout.licensed_item_id, + wallet_id=_licensed_item_checkout.wallet_id, + user_id=_licensed_item_checkout.user_id, + product_name=_licensed_item_checkout.product_name, + started_at=_licensed_item_checkout.started_at, + stopped_at=_licensed_item_checkout.stopped_at, + num_of_seats=_licensed_item_checkout.num_of_seats, + ) + + +def setup(app: FastAPI, rabbitmq_rpc_client: RabbitMQRPCClient): + resource_usage_tracker_rpc_client = ResourceUsageTrackerClient( + _client=rabbitmq_rpc_client + ) + resource_usage_tracker_rpc_client.set_to_app_state(app=app) diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 207712e3e47..78b2de93272 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -3,26 +3,84 @@ from typing import cast from fastapi import FastAPI -from fastapi_pagination import Page, create_page +from fastapi_pagination import create_page +from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker_licensed_items_checkouts import ( + LicensedItemCheckoutID, +) +from models_library.services_types import ServiceRunID +from models_library.users import UserID +from models_library.wallets import WalletID +from servicelib.fastapi.app_state import SingletonInAppStateMixin from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + CanNotCheckoutNotEnoughAvailableSeatsError, +) +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + CanNotCheckoutServiceIsNotRunningError as _CanNotCheckoutServiceIsNotRunningError, +) +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + LicensedItemCheckoutNotFoundError as _LicensedItemCheckoutNotFoundError, +) +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + NotEnoughAvailableSeatsError, +) +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + checkout_licensed_item_for_wallet as _checkout_licensed_item_for_wallet, +) +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + get_available_licensed_items_for_wallet as _get_available_licensed_items_for_wallet, +) from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( get_licensed_items as _get_licensed_items, ) +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + release_licensed_item_for_wallet as _release_licensed_item_for_wallet, +) +from ..exceptions.backend_errors import ( + CanNotCheckoutServiceIsNotRunningError, + InsufficientNumberOfSeatsError, + LicensedItemCheckoutNotFoundError, +) from ..exceptions.service_errors_utils import service_exception_mapper -from ..models.pagination import PaginationParams -from ..models.schemas.model_adapter import LicensedItemGet +from ..models.pagination import Page, PaginationParams +from ..models.schemas.model_adapter import LicensedItemCheckoutGet, LicensedItemGet _exception_mapper = partial(service_exception_mapper, service_name="WebApiServer") +def _create_licensed_items_get_page( + *, licensed_items_page: LicensedItemGetPage, page_params: PaginationParams +) -> Page[LicensedItemGet]: + page = create_page( + [ + LicensedItemGet( + licensed_item_id=elm.licensed_item_id, + name=elm.name, + license_key=elm.license_key, + licensed_resource_type=elm.licensed_resource_type, + pricing_plan_id=elm.pricing_plan_id, + created_at=elm.created_at, + modified_at=elm.modified_at, + ) + for elm in licensed_items_page.items + ], + total=licensed_items_page.total, + params=page_params, + ) + return cast(Page[LicensedItemGet], page) + + @dataclass -class WbApiRpcClient: +class WbApiRpcClient(SingletonInAppStateMixin): + app_state_name = "wb_api_rpc_client" _client: RabbitMQRPCClient @_exception_mapper(rpc_exception_map={}) async def get_licensed_items( - self, product_name: str, page_params: PaginationParams + self, *, product_name: str, page_params: PaginationParams ) -> Page[LicensedItemGet]: licensed_items_page = await _get_licensed_items( rabbitmq_rpc_client=self._client, @@ -30,23 +88,98 @@ async def get_licensed_items( offset=page_params.offset, limit=page_params.limit, ) - page = create_page( - [ - LicensedItemGet( - licensed_item_id=elm.licensed_item_id, - name=elm.name, - licensed_resource_type=elm.licensed_resource_type, - pricing_plan_id=elm.pricing_plan_id, - created_at=elm.created_at, - modified_at=elm.modified_at, - ) - for elm in licensed_items_page.items - ], - total=licensed_items_page.total, - params=page_params, + return _create_licensed_items_get_page( + licensed_items_page=licensed_items_page, page_params=page_params + ) + + @_exception_mapper(rpc_exception_map={}) + async def get_available_licensed_items_for_wallet( + self, + *, + product_name: str, + wallet_id: WalletID, + user_id: UserID, + page_params: PaginationParams, + ) -> Page[LicensedItemGet]: + licensed_items_page = await _get_available_licensed_items_for_wallet( + rabbitmq_rpc_client=self._client, + product_name=product_name, + wallet_id=wallet_id, + user_id=user_id, + offset=page_params.offset, + limit=page_params.limit, + ) + return _create_licensed_items_get_page( + licensed_items_page=licensed_items_page, page_params=page_params + ) + + @_exception_mapper( + rpc_exception_map={ + NotEnoughAvailableSeatsError: InsufficientNumberOfSeatsError, + CanNotCheckoutNotEnoughAvailableSeatsError: InsufficientNumberOfSeatsError, + _CanNotCheckoutServiceIsNotRunningError: CanNotCheckoutServiceIsNotRunningError, + } + ) + async def checkout_licensed_item_for_wallet( + self, + *, + product_name: str, + user_id: UserID, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunID, + ) -> LicensedItemCheckoutGet: + licensed_item_checkout_get = await _checkout_licensed_item_for_wallet( + self._client, + product_name=product_name, + user_id=user_id, + wallet_id=wallet_id, + licensed_item_id=licensed_item_id, + num_of_seats=num_of_seats, + service_run_id=service_run_id, + ) + return LicensedItemCheckoutGet( + licensed_item_checkout_id=licensed_item_checkout_get.licensed_item_checkout_id, + licensed_item_id=licensed_item_checkout_get.licensed_item_id, + wallet_id=licensed_item_checkout_get.wallet_id, + user_id=licensed_item_checkout_get.user_id, + product_name=licensed_item_checkout_get.product_name, + started_at=licensed_item_checkout_get.started_at, + stopped_at=licensed_item_checkout_get.stopped_at, + num_of_seats=licensed_item_checkout_get.num_of_seats, + ) + + @_exception_mapper( + rpc_exception_map={ + _LicensedItemCheckoutNotFoundError: LicensedItemCheckoutNotFoundError + } + ) + async def release_licensed_item_for_wallet( + self, + *, + product_name: str, + user_id: UserID, + licensed_item_checkout_id: LicensedItemCheckoutID, + ) -> LicensedItemCheckoutGet: + licensed_item_checkout_get = await _release_licensed_item_for_wallet( + self._client, + product_name=product_name, + user_id=user_id, + licensed_item_checkout_id=licensed_item_checkout_id, + ) + return LicensedItemCheckoutGet( + licensed_item_checkout_id=licensed_item_checkout_get.licensed_item_checkout_id, + licensed_item_id=licensed_item_checkout_get.licensed_item_id, + wallet_id=licensed_item_checkout_get.wallet_id, + user_id=licensed_item_checkout_get.user_id, + product_name=licensed_item_checkout_get.product_name, + started_at=licensed_item_checkout_get.started_at, + stopped_at=licensed_item_checkout_get.stopped_at, + num_of_seats=licensed_item_checkout_get.num_of_seats, ) - return cast(Page[LicensedItemGet], page) def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): - app.state.wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) + wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) + wb_api_rpc_client.set_to_app_state(app=app) diff --git a/services/api-server/tests/conftest.py b/services/api-server/tests/conftest.py index 57533c02438..16d33d3afc0 100644 --- a/services/api-server/tests/conftest.py +++ b/services/api-server/tests/conftest.py @@ -5,11 +5,11 @@ import sys from pathlib import Path -from pydantic import TypeAdapter import pytest import simcore_service_api_server from dotenv import dotenv_values from models_library.projects import ProjectID +from pydantic import TypeAdapter from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict from simcore_service_api_server.models.schemas.jobs import JobID diff --git a/services/api-server/tests/unit/_with_db/conftest.py b/services/api-server/tests/unit/_with_db/conftest.py index 22b2f6b4c84..98493ca0411 100644 --- a/services/api-server/tests/unit/_with_db/conftest.py +++ b/services/api-server/tests/unit/_with_db/conftest.py @@ -65,7 +65,7 @@ def docker_compose_file( # configs subprocess.run( f'docker compose --file "{src_path}" config > "{dst_path}"', - shell=True, # noqa: S602 + shell=True, check=True, env=environ, ) diff --git a/services/api-server/tests/unit/test_api_files.py b/services/api-server/tests/unit/test_api_files.py index bbbef802188..6b522e737ea 100644 --- a/services/api-server/tests/unit/test_api_files.py +++ b/services/api-server/tests/unit/test_api_files.py @@ -58,7 +58,7 @@ def file(cls) -> File: id=File.create_id( cls._file_size, cls._file_name, - datetime.datetime.now(datetime.timezone.utc).isoformat(), + datetime.datetime.now(datetime.UTC).isoformat(), ), filename=cls._file_name, e_tag="", diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index e0db32dc3b7..883de9b593c 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -3,78 +3,120 @@ # pylint: disable=unused-variable # pylint: disable=too-many-arguments import asyncio +from functools import partial +from typing import cast +from uuid import UUID import pytest +from faker import Faker from fastapi import FastAPI, status from httpx import AsyncClient, BasicAuth +from models_library.api_schemas_resource_usage_tracker.licensed_items_checkouts import ( + LicensedItemCheckoutGet, +) from models_library.api_schemas_webserver.licensed_items import ( LicensedItemGet as _LicensedItemGet, ) from models_library.api_schemas_webserver.licensed_items import ( LicensedItemGetPage as _LicensedItemGetPage, ) +from models_library.api_schemas_webserver.licensed_items_checkouts import ( + LicensedItemCheckoutRpcGet, +) +from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker_licensed_items_checkouts import ( + LicensedItemCheckoutID, +) +from models_library.services_types import ServiceRunID +from models_library.users import UserID +from models_library.wallets import WalletID from pydantic import TypeAdapter from pytest_mock import MockerFixture from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( + CanNotCheckoutNotEnoughAvailableSeatsError, + CanNotCheckoutServiceIsNotRunningError, + LicensedItemCheckoutNotFoundError, + NotEnoughAvailableSeatsError, +) from simcore_service_api_server._meta import API_VTAG +from simcore_service_api_server.api.dependencies.resource_usage_tracker_rpc import ( + get_resource_usage_tracker_client, +) from simcore_service_api_server.api.dependencies.webserver_rpc import ( get_wb_api_rpc_client, ) from simcore_service_api_server.models.pagination import Page +from simcore_service_api_server.models.schemas.licensed_items import ( + LicensedItemCheckoutData, +) from simcore_service_api_server.models.schemas.model_adapter import LicensedItemGet +from simcore_service_api_server.services_rpc.resource_usage_tracker import ( + ResourceUsageTrackerClient, +) from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient -@pytest.fixture -async def mock_wb_api_server_rcp( - app: FastAPI, mocker: MockerFixture, exception_to_raise: Exception | None -) -> MockerFixture: - async def _get_backend_licensed_items( - rabbitmq_rpc_client: RabbitMQRPCClient, - *, - product_name: str, - offset: int, - limit: int, - ) -> _LicensedItemGetPage: - if exception_to_raise is not None: - raise exception_to_raise - extra = _LicensedItemGet.model_config.get("json_schema_extra") - assert isinstance(extra, dict) - examples = extra.get("examples") - assert isinstance(examples, list) - return _LicensedItemGetPage( - items=[_LicensedItemGet.model_validate(ex) for ex in examples], - total=len(examples), - ) +async def _get_backend_licensed_items( + exception_to_raise: Exception | None, + rabbitmq_rpc_client: RabbitMQRPCClient, + product_name: str, + offset: int, + limit: int, +) -> _LicensedItemGetPage: + if exception_to_raise is not None: + raise exception_to_raise + extra = _LicensedItemGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + return _LicensedItemGetPage( + items=[_LicensedItemGet.model_validate(ex) for ex in examples], + total=len(examples), + ) + + +class DummyRpcClient: + pass - class DummyRpcClient: - pass + +@pytest.fixture +async def mock_wb_api_server_rcp(app: FastAPI, mocker: MockerFixture) -> MockerFixture: app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient( _client=DummyRpcClient() ) - mocker.patch( - "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", - _get_backend_licensed_items, - ) + return mocker + +@pytest.fixture +async def mock_rut_rpc(app: FastAPI, mocker: MockerFixture) -> MockerFixture: + app.dependency_overrides[ + get_resource_usage_tracker_client + ] = lambda: ResourceUsageTrackerClient(_client=DummyRpcClient()) return mocker -@pytest.mark.parametrize("exception_to_raise", [None]) async def test_get_licensed_items( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", + partial(_get_backend_licensed_items, None), + ) resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_200_OK TypeAdapter(Page[LicensedItemGet]).validate_json(resp.text) -@pytest.mark.parametrize("exception_to_raise", [asyncio.TimeoutError()]) async def test_get_licensed_items_timeout( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", + partial(_get_backend_licensed_items, exception_to_raise=TimeoutError()), + ) resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_504_GATEWAY_TIMEOUT @@ -84,7 +126,194 @@ async def test_get_licensed_items_timeout( [asyncio.CancelledError(), RuntimeError(), RemoteMethodNotRegisteredError()], ) async def test_get_licensed_items_502( - mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth + mock_wb_api_server_rcp: MockerFixture, + client: AsyncClient, + auth: BasicAuth, + exception_to_raise: Exception, ): + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", + partial(_get_backend_licensed_items, exception_to_raise), + ) resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_502_BAD_GATEWAY + + +@pytest.mark.parametrize( + "exception_to_raise,expected_api_server_status_code", + [ + (NotImplementedError(), status.HTTP_501_NOT_IMPLEMENTED), + ], +) +async def test_get_licensed_items_for_wallet( + mock_wb_api_server_rcp: MockerFixture, + client: AsyncClient, + auth: BasicAuth, + exception_to_raise: Exception | None, + expected_api_server_status_code: int, + faker: Faker, +): + _wallet_id = faker.pyint(min_value=1) + + async def side_effect( + rabbitmq_rpc_client: RabbitMQRPCClient, + product_name: str, + wallet_id: WalletID, + user_id: UserID, + offset: int, + limit: int, + ) -> _LicensedItemGetPage: + assert _wallet_id == wallet_id + if exception_to_raise is not None: + raise exception_to_raise + extra = _LicensedItemGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + return _LicensedItemGetPage( + items=[_LicensedItemGet.model_validate(ex) for ex in examples], + total=len(examples), + ) + + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_available_licensed_items_for_wallet", + side_effect, + ) + resp = await client.get( + f"{API_VTAG}/wallets/{_wallet_id}/licensed-items", auth=auth + ) + assert resp.status_code == expected_api_server_status_code + + +@pytest.mark.parametrize( + "exception_to_raise,expected_api_server_status_code", + [ + (None, status.HTTP_200_OK), + (NotEnoughAvailableSeatsError(), status.HTTP_409_CONFLICT), + (CanNotCheckoutNotEnoughAvailableSeatsError(), status.HTTP_409_CONFLICT), + ( + CanNotCheckoutServiceIsNotRunningError(), + status.HTTP_422_UNPROCESSABLE_ENTITY, + ), + ], +) +async def test_checkout_licensed_item( + mock_wb_api_server_rcp: MockerFixture, + client: AsyncClient, + auth: BasicAuth, + exception_to_raise: Exception | None, + expected_api_server_status_code: int, + faker: Faker, +): + _wallet_id = faker.pyint(min_value=1) + _licensed_item_id = faker.uuid4() + + async def side_effect( + rabbitmq_rpc_client: RabbitMQRPCClient, + product_name: str, + user_id: UserID, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunID, + ) -> LicensedItemCheckoutRpcGet: + if exception_to_raise is not None: + raise exception_to_raise + extra = LicensedItemCheckoutRpcGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + assert len(examples) > 0 + example = examples[0] + assert isinstance(example, dict) + return LicensedItemCheckoutRpcGet.model_validate(example) + + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._checkout_licensed_item_for_wallet", + side_effect, + ) + body = LicensedItemCheckoutData( + number_of_seats=faker.pyint(min_value=1), + service_run_id=cast(ServiceRunID, "myservice"), + ) + resp = await client.post( + f"{API_VTAG}/wallets/{_wallet_id}/licensed-items/{_licensed_item_id}/checkout", + auth=auth, + content=body.model_dump_json(), + ) + assert resp.status_code == expected_api_server_status_code + + +@pytest.mark.parametrize( + "wb_api_exception_to_raise,rut_exception_to_raise,expected_api_server_status_code,valid_license_checkout_id", + [ + (LicensedItemCheckoutNotFoundError, None, status.HTTP_404_NOT_FOUND, True), + (None, LicensedItemCheckoutNotFoundError, status.HTTP_404_NOT_FOUND, True), + (None, None, status.HTTP_200_OK, True), + (None, None, status.HTTP_422_UNPROCESSABLE_ENTITY, False), + ], +) +async def test_release_checked_out_licensed_item( + mock_wb_api_server_rcp: MockerFixture, + mock_rut_rpc: MockerFixture, + client: AsyncClient, + auth: BasicAuth, + wb_api_exception_to_raise: Exception | None, + rut_exception_to_raise: Exception | None, + expected_api_server_status_code: int, + valid_license_checkout_id: bool, + faker: Faker, +): + _licensed_item_id = cast(UUID, faker.uuid4()) + _licensed_item_checkout_id = cast(UUID, faker.uuid4()) + + async def get_licensed_item_checkout( + rabbitmq_rpc_client: RabbitMQRPCClient, + product_name: str, + licensed_item_checkout_id: LicensedItemCheckoutID, + ) -> LicensedItemCheckoutGet: + if rut_exception_to_raise is not None: + raise rut_exception_to_raise + extra = LicensedItemCheckoutGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + assert len(examples) > 0 + example = examples[0] + assert isinstance(example, dict) + licensed_item_checkout_get = LicensedItemCheckoutGet.model_validate(example) + if valid_license_checkout_id: + licensed_item_checkout_get.licensed_item_id = _licensed_item_id + return licensed_item_checkout_get + + async def release_licensed_item_for_wallet( + rabbitmq_rpc_client: RabbitMQRPCClient, + product_name: str, + user_id: int, + licensed_item_checkout_id: LicensedItemCheckoutID, + ) -> LicensedItemCheckoutRpcGet: + if wb_api_exception_to_raise is not None: + raise wb_api_exception_to_raise + extra = LicensedItemCheckoutRpcGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + assert len(examples) > 0 + example = examples[0] + assert isinstance(example, dict) + return LicensedItemCheckoutRpcGet.model_validate(example) + + mock_rut_rpc.patch( + "simcore_service_api_server.services_rpc.resource_usage_tracker._get_licensed_item_checkout", + get_licensed_item_checkout, + ) + mock_wb_api_server_rcp.patch( + "simcore_service_api_server.services_rpc.wb_api_server._release_licensed_item_for_wallet", + release_licensed_item_for_wallet, + ) + + resp = await client.post( + f"{API_VTAG}/licensed-items/{_licensed_item_id}/checked-out-items/{_licensed_item_checkout_id}/release", + auth=auth, + ) + assert resp.status_code == expected_api_server_status_code diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_checkouts.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_checkouts.py index c17ff34655b..e37c4269045 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_checkouts.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_checkouts.py @@ -15,6 +15,7 @@ from servicelib.rabbitmq import RPCRouter from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( LICENSES_ERRORS, + LicensedItemCheckoutNotFoundError, ) from ...services import licensed_items_checkouts @@ -22,7 +23,7 @@ router = RPCRouter() -@router.expose(reraise_if_error_type=LICENSES_ERRORS) +@router.expose(reraise_if_error_type=(LicensedItemCheckoutNotFoundError,)) async def get_licensed_item_checkout( app: FastAPI, *, diff --git a/services/web/server/src/simcore_service_webserver/licenses/_rpc.py b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py index 261eb51c3aa..85cc3a99642 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_rpc.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py @@ -17,6 +17,10 @@ from servicelib.rabbitmq import RPCRouter from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker.errors import ( LICENSES_ERRORS, + CanNotCheckoutNotEnoughAvailableSeatsError, + CanNotCheckoutServiceIsNotRunningError, + LicensedItemCheckoutNotFoundError, + NotEnoughAvailableSeatsError, ) from ..rabbitmq import get_rabbitmq_rpc_server @@ -58,7 +62,13 @@ async def get_available_licensed_items_for_wallet( raise NotImplementedError -@router.expose(reraise_if_error_type=LICENSES_ERRORS) +@router.expose( + reraise_if_error_type=( + NotEnoughAvailableSeatsError, + CanNotCheckoutNotEnoughAvailableSeatsError, + CanNotCheckoutServiceIsNotRunningError, + ) +) async def checkout_licensed_item_for_wallet( app: web.Application, *, @@ -92,7 +102,7 @@ async def checkout_licensed_item_for_wallet( ) -@router.expose(reraise_if_error_type=LICENSES_ERRORS) +@router.expose(reraise_if_error_type=(LicensedItemCheckoutNotFoundError,)) async def release_licensed_item_for_wallet( app: web.Application, *,