Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
80c1223
@matusdrobuliak66 add license key to api-server model
bisgaard-itis Dec 18, 2024
c82f397
refactoringh
bisgaard-itis Dec 18, 2024
8a79348
expose get_available_licensed_items_for_wallet in api-server
bisgaard-itis Dec 18, 2024
cf218a1
expose checkout endpoint
bisgaard-itis Dec 18, 2024
d6525ef
start exposing final endpoint
bisgaard-itis Dec 18, 2024
237bd93
expose final endpoint
bisgaard-itis Dec 19, 2024
8240f47
refactor licensed items tests
bisgaard-itis Dec 19, 2024
f794286
add some tests for exceptions
bisgaard-itis Dec 19, 2024
7d4ace4
Merge branch 'master' into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Dec 30, 2024
6c3682c
update to latest master changes
bisgaard-itis Dec 30, 2024
09003df
merge master into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 7, 2025
4758760
resolve issues caused by merging master
bisgaard-itis Jan 7, 2025
7caaa0e
merge master into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 7, 2025
1d22b01
make pylint happy
bisgaard-itis Jan 7, 2025
8e4e18d
merge master into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 7, 2025
e21c8aa
add test for checking out licensed item
bisgaard-itis Jan 7, 2025
9ede5af
add error handling to checkout endpoint
bisgaard-itis Jan 7, 2025
6c4f1a6
minor correction
bisgaard-itis Jan 7, 2025
964bb67
ruff
bisgaard-itis Jan 7, 2025
97f8d7d
add rpc client for resource usage tracker
bisgaard-itis Jan 7, 2025
0c0312a
add endpoint for releasing a checked out item
bisgaard-itis Jan 7, 2025
7beac73
add exception handling to release endpoint
bisgaard-itis Jan 7, 2025
ce7fc12
make test pass
bisgaard-itis Jan 7, 2025
baed7ba
add more parameters to test
bisgaard-itis Jan 7, 2025
ae332a6
Merge branch 'master' into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 7, 2025
35c930e
fix wrong import
bisgaard-itis Jan 7, 2025
c2ccce1
merge master into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 9, 2025
e37ccf2
@pcrespov int -> PositiveInt
bisgaard-itis Jan 9, 2025
3d3b3f4
@sanderegg @pcrespov handle cast
bisgaard-itis Jan 9, 2025
f69a4b8
@pcrespov use SingletonInAppStateMixin
bisgaard-itis Jan 9, 2025
239fb66
@GitHK TimeoutError -> asyncio.TimeoutError
bisgaard-itis Jan 9, 2025
893cdc0
Merge branch 'master' into 6948-expose-licensing-endpoints-in-api-server
bisgaard-itis Jan 9, 2025
e1b54ea
assert type
bisgaard-itis Jan 9, 2025
642c474
nosec
bisgaard-itis Jan 9, 2025
5e0fba5
resolve type issue
bisgaard-itis Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading