Skip to content

Commit

Permalink
✨ Expose licensing endpoints in api server (#7009)
Browse files Browse the repository at this point in the history
  • Loading branch information
bisgaard-itis authored Jan 9, 2025
1 parent f543f5a commit 7e31592
Show file tree
Hide file tree
Showing 28 changed files with 695 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ def _check_total(cls, v, info: ValidationInfo):


__all__: tuple[str, ...] = (
"PaginationParams",
"MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE",
"OnePage",
"Page",
"PaginationParams",
)
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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()
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 7e31592

Please sign in to comment.