Skip to content

Commit 7e31592

Browse files
✨ Expose licensing endpoints in api server (ITISFoundation#7009)
1 parent f543f5a commit 7e31592

File tree

28 files changed

+695
-87
lines changed

28 files changed

+695
-87
lines changed

packages/models-library/src/models_library/api_schemas_webserver/licensed_items_checkouts.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from typing import NamedTuple
33

4-
from pydantic import BaseModel, PositiveInt
4+
from pydantic import BaseModel, ConfigDict, PositiveInt
55

66
from ..licensed_items import LicensedItemID
77
from ..products import ProductName
@@ -22,6 +22,22 @@ class LicensedItemCheckoutRpcGet(BaseModel):
2222
started_at: datetime
2323
stopped_at: datetime | None
2424
num_of_seats: int
25+
model_config = ConfigDict(
26+
json_schema_extra={
27+
"examples": [
28+
{
29+
"licensed_item_checkout_id": "633ef980-6f3e-4b1a-989a-bd77bf9a5d6b",
30+
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
31+
"wallet_id": 6,
32+
"user_id": 27845,
33+
"product_name": "osparc",
34+
"started_at": "2024-12-12 09:59:26.422140",
35+
"stopped_at": "2024-12-12 09:59:26.423540",
36+
"num_of_seats": 78,
37+
}
38+
]
39+
}
40+
)
2541

2642

2743
class LicensedItemCheckoutRpcGetPage(NamedTuple):

services/api-server/src/simcore_service_api_server/api/dependencies/application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ def get_settings(request: Request) -> ApplicationSettings:
1515
assert get_app # nosec
1616

1717
__all__: tuple[str, ...] = (
18-
"get_reverse_url_mapper",
1918
"get_app",
19+
"get_reverse_url_mapper",
2020
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Annotated
2+
3+
from fastapi import Depends, FastAPI
4+
from servicelib.fastapi.dependencies import get_app
5+
6+
from ...services_rpc.resource_usage_tracker import ResourceUsageTrackerClient
7+
8+
9+
async def get_resource_usage_tracker_client(
10+
app: Annotated[FastAPI, Depends(get_app)]
11+
) -> ResourceUsageTrackerClient:
12+
return ResourceUsageTrackerClient.get_from_app_state(app=app)
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, cast
1+
from typing import Annotated
22

33
from fastapi import Depends, FastAPI
44
from servicelib.fastapi.dependencies import get_app
@@ -9,5 +9,4 @@
99
async def get_wb_api_rpc_client(
1010
app: Annotated[FastAPI, Depends(get_app)]
1111
) -> WbApiRpcClient:
12-
assert app.state.wb_api_rpc_client # nosec
13-
return cast(WbApiRpcClient, app.state.wb_api_rpc_client)
12+
return WbApiRpcClient.get_from_app_state(app=app)

services/api-server/src/simcore_service_api_server/api/routes/files.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async def list_files(
116116

117117
file_meta: File = to_file_api_model(stored_file_meta)
118118

119-
except (ValidationError, ValueError, AttributeError) as err: # noqa: PERF203
119+
except (ValidationError, ValueError, AttributeError) as err:
120120
_logger.warning(
121121
"Skipping corrupted entry in storage '%s' (%s)"
122122
"TIP: check this entry in file_meta_data table.",
@@ -186,7 +186,7 @@ async def upload_file(
186186
file_meta: File = await File.create_from_uploaded(
187187
file,
188188
file_size=file_size,
189-
created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
189+
created_at=datetime.datetime.now(datetime.UTC).isoformat(),
190190
)
191191
_logger.debug(
192192
"Assigned id: %s of %s bytes (content-length), real size %s bytes",
@@ -242,7 +242,7 @@ async def get_upload_links(
242242
assert request # nosec
243243
file_meta: File = await File.create_from_client_file(
244244
client_file,
245-
datetime.datetime.now(datetime.timezone.utc).isoformat(),
245+
datetime.datetime.now(datetime.UTC).isoformat(),
246246
)
247247
_, upload_links = await get_upload_links_from_s3(
248248
user_id=user_id,

services/api-server/src/simcore_service_api_server/api/routes/health.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def check_service_health(
2929
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="unhealthy"
3030
)
3131

32-
return f"{__name__}@{datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}"
32+
return f"{__name__}@{datetime.datetime.now(tz=datetime.UTC).isoformat()}"
3333

3434

3535
@router.get(

services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
from typing import Annotated, Any
22

3-
from fastapi import APIRouter, Depends, status
3+
from fastapi import APIRouter, Depends, HTTPException, status
4+
from models_library.licensed_items import LicensedItemID
5+
from models_library.resource_tracker_licensed_items_checkouts import (
6+
LicensedItemCheckoutID,
7+
)
8+
from pydantic import PositiveInt
9+
from simcore_service_api_server.api.dependencies.resource_usage_tracker_rpc import (
10+
get_resource_usage_tracker_client,
11+
)
412

5-
from ...api.dependencies.authentication import get_product_name
13+
from ...api.dependencies.authentication import get_current_user_id, get_product_name
614
from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client
715
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
816
from ...models.pagination import Page, PaginationParams
9-
from ...models.schemas.model_adapter import LicensedItemGet
17+
from ...models.schemas.model_adapter import LicensedItemCheckoutGet, LicensedItemGet
18+
from ...services_rpc.resource_usage_tracker import ResourceUsageTrackerClient
1019
from ...services_rpc.wb_api_server import WbApiRpcClient
1120

1221
router = APIRouter()
@@ -32,3 +41,36 @@ async def get_licensed_items(
3241
return await web_api_rpc.get_licensed_items(
3342
product_name=product_name, page_params=page_params
3443
)
44+
45+
46+
@router.post(
47+
"/{licensed_item_id}/checked-out-items/{licensed_item_checkout_id}/release",
48+
response_model=LicensedItemCheckoutGet,
49+
status_code=status.HTTP_200_OK,
50+
responses=_LICENSE_ITEMS_STATUS_CODES,
51+
description="Release previously checked out licensed item",
52+
include_in_schema=False,
53+
)
54+
async def release_licensed_item(
55+
web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)],
56+
rut_rpc: Annotated[
57+
ResourceUsageTrackerClient, Depends(get_resource_usage_tracker_client)
58+
],
59+
product_name: Annotated[str, Depends(get_product_name)],
60+
user_id: Annotated[PositiveInt, Depends(get_current_user_id)],
61+
licensed_item_id: LicensedItemID,
62+
licensed_item_checkout_id: LicensedItemCheckoutID,
63+
):
64+
_licensed_item_checkout = await rut_rpc.get_licensed_item_checkout(
65+
product_name=product_name, licensed_item_checkout_id=licensed_item_checkout_id
66+
)
67+
if _licensed_item_checkout.licensed_item_id != licensed_item_id:
68+
raise HTTPException(
69+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
70+
detail=f"{licensed_item_id} is not the license_item_id associated with the checked out item {licensed_item_checkout_id}",
71+
)
72+
return await web_api_rpc.release_licensed_item_for_wallet(
73+
product_name=product_name,
74+
user_id=user_id,
75+
licensed_item_checkout_id=licensed_item_checkout_id,
76+
)

services/api-server/src/simcore_service_api_server/api/routes/wallets.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,21 @@
22
from typing import Annotated, Any
33

44
from fastapi import APIRouter, Depends, status
5+
from models_library.licensed_items import LicensedItemID
6+
from pydantic import PositiveInt
57

8+
from ...api.dependencies.authentication import get_current_user_id, get_product_name
9+
from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client
610
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
11+
from ...models.pagination import Page, PaginationParams
712
from ...models.schemas.errors import ErrorGet
8-
from ...models.schemas.model_adapter import WalletGetWithAvailableCreditsLegacy
13+
from ...models.schemas.licensed_items import LicensedItemCheckoutData
14+
from ...models.schemas.model_adapter import (
15+
LicensedItemCheckoutGet,
16+
LicensedItemGet,
17+
WalletGetWithAvailableCreditsLegacy,
18+
)
19+
from ...services_rpc.wb_api_server import WbApiRpcClient
920
from ..dependencies.webserver_http import AuthSession, get_webserver_session
1021
from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION
1122

@@ -49,3 +60,52 @@ async def get_wallet(
4960
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
5061
):
5162
return await webserver_api.get_wallet(wallet_id=wallet_id)
63+
64+
65+
@router.get(
66+
"/{wallet_id}/licensed-items",
67+
response_model=Page[LicensedItemGet],
68+
status_code=status.HTTP_200_OK,
69+
responses=WALLET_STATUS_CODES,
70+
description="Get all available licensed items for a given wallet",
71+
include_in_schema=False,
72+
)
73+
async def get_available_licensed_items_for_wallet(
74+
wallet_id: int,
75+
page_params: Annotated[PaginationParams, Depends()],
76+
web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)],
77+
product_name: Annotated[str, Depends(get_product_name)],
78+
user_id: Annotated[PositiveInt, Depends(get_current_user_id)],
79+
):
80+
return await web_api_rpc.get_available_licensed_items_for_wallet(
81+
product_name=product_name,
82+
wallet_id=wallet_id,
83+
user_id=user_id,
84+
page_params=page_params,
85+
)
86+
87+
88+
@router.post(
89+
"/{wallet_id}/licensed-items/{licensed_item_id}/checkout",
90+
response_model=LicensedItemCheckoutGet,
91+
status_code=status.HTTP_200_OK,
92+
responses=WALLET_STATUS_CODES,
93+
description="Checkout licensed item",
94+
include_in_schema=False,
95+
)
96+
async def checkout_licensed_item(
97+
wallet_id: int,
98+
licensed_item_id: LicensedItemID,
99+
web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)],
100+
product_name: Annotated[str, Depends(get_product_name)],
101+
user_id: Annotated[PositiveInt, Depends(get_current_user_id)],
102+
checkout_data: LicensedItemCheckoutData,
103+
):
104+
return await web_api_rpc.checkout_licensed_item_for_wallet(
105+
product_name=product_name,
106+
user_id=user_id,
107+
wallet_id=wallet_id,
108+
licensed_item_id=licensed_item_id,
109+
num_of_seats=checkout_data.number_of_seats,
110+
service_run_id=checkout_data.service_run_id,
111+
)

services/api-server/src/simcore_service_api_server/db/tables.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from simcore_postgres_database.models.users import UserRole, UserStatus, users
55

66
__all__: tuple[str, ...] = (
7+
"GroupType",
8+
"UserRole",
9+
"UserStatus",
710
"api_keys",
811
"groups",
9-
"GroupType",
1012
"metadata",
1113
"user_to_groups",
12-
"UserRole",
1314
"users",
14-
"UserStatus",
1515
)
1616

1717
# nopycln: file # noqa: ERA001

services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,18 @@ class PricingPlanNotFoundError(BaseBackEndError):
105105
class ProjectAlreadyStartedError(BaseBackEndError):
106106
msg_template = "Project already started"
107107
status_code = status.HTTP_200_OK
108+
109+
110+
class InsufficientNumberOfSeatsError(BaseBackEndError):
111+
msg_template = "Not enough available seats. Current available seats {num_of_seats} for license item {licensed_item_id}"
112+
status_code = status.HTTP_409_CONFLICT
113+
114+
115+
class CanNotCheckoutServiceIsNotRunningError(BaseBackEndError):
116+
msg_template = "Can not checkout license item {licensed_item_id} as dynamic service is not running. Current service id: {service_run_id}"
117+
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
118+
119+
120+
class LicensedItemCheckoutNotFoundError(BaseBackEndError):
121+
msg_template = "Licensed item checkout {licensed_item_checkout_id} not found."
122+
status_code = status.HTTP_404_NOT_FOUND

0 commit comments

Comments
 (0)