Skip to content

Commit 15d54a6

Browse files
🎨 Changing Rest Model for licensed items listing (🗃️) (#7139)
Co-authored-by: Pedro Crespo-Valero <[email protected]>
1 parent 68b2497 commit 15d54a6

File tree

19 files changed

+395
-181
lines changed

19 files changed

+395
-181
lines changed

api/specs/web-server/_licensed_items.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010

1111
from _common import as_query
1212
from fastapi import APIRouter, Depends, status
13-
from models_library.api_schemas_webserver.licensed_items import LicensedItemGet
13+
from models_library.api_schemas_webserver.licensed_items import LicensedItemRestGet
1414
from models_library.generics import Envelope
1515
from models_library.rest_error import EnvelopedError
16+
from models_library.rest_pagination import Page
1617
from simcore_service_webserver._meta import API_VTAG
1718
from simcore_service_webserver.licenses._common.exceptions_handlers import (
1819
_TO_HTTP_ERROR_MAP,
@@ -37,7 +38,7 @@
3738

3839
@router.get(
3940
"/catalog/licensed-items",
40-
response_model=Envelope[list[LicensedItemGet]],
41+
response_model=Page[LicensedItemRestGet],
4142
)
4243
async def list_licensed_items(
4344
_query: Annotated[as_query(LicensedItemsListQueryParams), Depends()],
@@ -47,7 +48,7 @@ async def list_licensed_items(
4748

4849
@router.get(
4950
"/catalog/licensed-items/{licensed_item_id}",
50-
response_model=Envelope[LicensedItemGet],
51+
response_model=Envelope[LicensedItemRestGet],
5152
)
5253
async def get_licensed_item(
5354
_path: Annotated[LicensedItemsPathParams, Depends()],
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,62 @@
11
from datetime import datetime
2-
from typing import NamedTuple, Self
2+
from typing import Any, NamedTuple, cast
33

4-
from common_library.dict_tools import remap_keys
54
from models_library.licensed_items import (
6-
LicensedItemDB,
5+
VIP_DETAILS_EXAMPLE,
76
LicensedItemID,
87
LicensedResourceType,
98
)
109
from models_library.resource_tracker import PricingPlanId
11-
from pydantic import ConfigDict, PositiveInt
10+
from models_library.utils.common_validators import to_camel_recursive
11+
from pydantic import AfterValidator, BaseModel, ConfigDict, PositiveInt
12+
from pydantic.config import JsonDict
13+
from typing_extensions import Annotated
1214

1315
from ._base import OutputSchema
1416

17+
# RPC
1518

16-
class LicensedItemGet(OutputSchema):
17-
licensed_item_id: LicensedItemID
1819

19-
name: str
20-
license_key: str | None
20+
class LicensedItemRpcGet(BaseModel):
21+
licensed_item_id: LicensedItemID
22+
display_name: str
2123
licensed_resource_type: LicensedResourceType
24+
licensed_resource_data: dict[str, Any]
25+
pricing_plan_id: PricingPlanId
26+
created_at: datetime
27+
modified_at: datetime
28+
model_config = ConfigDict(
29+
json_schema_extra={
30+
"examples": [
31+
{
32+
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
33+
"display_name": "best-model",
34+
"licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}",
35+
"licensed_resource_data": cast(JsonDict, VIP_DETAILS_EXAMPLE),
36+
"pricing_plan_id": "15",
37+
"created_at": "2024-12-12 09:59:26.422140",
38+
"modified_at": "2024-12-12 09:59:26.422140",
39+
}
40+
]
41+
},
42+
)
43+
44+
45+
class LicensedItemRpcGetPage(NamedTuple):
46+
items: list[LicensedItemRpcGet]
47+
total: PositiveInt
48+
2249

50+
# Rest
51+
52+
53+
class LicensedItemRestGet(OutputSchema):
54+
licensed_item_id: LicensedItemID
55+
display_name: str
56+
licensed_resource_type: LicensedResourceType
57+
licensed_resource_data: Annotated[
58+
dict[str, Any], AfterValidator(to_camel_recursive)
59+
]
2360
pricing_plan_id: PricingPlanId
2461

2562
created_at: datetime
@@ -30,9 +67,9 @@ class LicensedItemGet(OutputSchema):
3067
"examples": [
3168
{
3269
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
33-
"name": "best-model",
34-
"license_key": "license-specific-key",
70+
"display_name": "best-model",
3571
"licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}",
72+
"licensed_resource_data": cast(JsonDict, VIP_DETAILS_EXAMPLE),
3673
"pricing_plan_id": "15",
3774
"created_at": "2024-12-12 09:59:26.422140",
3875
"modified_at": "2024-12-12 09:59:26.422140",
@@ -41,30 +78,7 @@ class LicensedItemGet(OutputSchema):
4178
}
4279
)
4380

44-
@classmethod
45-
def from_domain_model(cls, licensed_item_db: LicensedItemDB) -> Self:
46-
return cls.model_validate(
47-
remap_keys(
48-
licensed_item_db.model_dump(
49-
include={
50-
"licensed_item_id",
51-
"licensed_resource_name",
52-
"licensed_resource_type",
53-
"license_key",
54-
"pricing_plan_id",
55-
"created",
56-
"modified",
57-
}
58-
),
59-
{
60-
"licensed_resource_name": "name",
61-
"created": "created_at",
62-
"modified": "modified_at",
63-
},
64-
)
65-
)
66-
6781

68-
class LicensedItemGetPage(NamedTuple):
69-
items: list[LicensedItemGet]
82+
class LicensedItemRestGetPage(NamedTuple):
83+
items: list[LicensedItemRestGet]
7084
total: PositiveInt

packages/models-library/src/models_library/licensed_items.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,41 @@ class LicensedResourceType(StrAutoEnum):
1616
VIP_MODEL = auto()
1717

1818

19+
VIP_FEAUTES_EXAMPLE = {
20+
"name": "Duke",
21+
"version": "V2.0",
22+
"sex": "Male",
23+
"age": "34 years",
24+
"weight": "70.2 Kg",
25+
"height": "1.77 m",
26+
"data": "2015-03-01",
27+
"ethnicity": "Caucasian",
28+
"functionality": "Static",
29+
"additional_field": "allowed",
30+
}
31+
32+
VIP_DETAILS_EXAMPLE = {
33+
"id": 1,
34+
"description": "custom description",
35+
"thumbnail": "custom description",
36+
"features": VIP_FEAUTES_EXAMPLE,
37+
"doi": "custom value",
38+
"license_key": "custom value",
39+
"license_version": "custom value",
40+
"protection": "custom value",
41+
"available_from_url": "custom value",
42+
"additional_field": "allowed",
43+
}
44+
45+
1946
#
2047
# DB
2148
#
2249

2350

2451
class LicensedItemDB(BaseModel):
2552
licensed_item_id: LicensedItemID
26-
license_key: str | None
53+
display_name: str
2754

2855
licensed_resource_name: str
2956
licensed_resource_type: LicensedResourceType
@@ -41,7 +68,7 @@ class LicensedItemDB(BaseModel):
4168

4269

4370
class LicensedItemUpdateDB(BaseModel):
71+
display_name: str | None = None
4472
licensed_resource_name: str | None = None
4573
pricing_plan_id: PricingPlanId | None = None
46-
4774
trash: bool | None = None

packages/models-library/src/models_library/utils/common_validators.py

+16
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class MyModel(BaseModel):
2323
from common_library.json_serialization import json_loads
2424
from orjson import JSONDecodeError
2525
from pydantic import BaseModel
26+
from pydantic.alias_generators import to_camel
2627

2728

2829
def empty_str_to_none_pre_validator(value: Any):
@@ -120,3 +121,18 @@ def _validator(cls: type[BaseModel], values):
120121
return values
121122

122123
return _validator
124+
125+
126+
def to_camel_recursive(data: dict[str, Any]) -> dict[str, Any]:
127+
"""Recursively convert dictionary keys to camelCase"""
128+
if not isinstance(data, dict):
129+
return data # Return as-is if it's not a dictionary
130+
131+
new_dict = {}
132+
for key, value in data.items():
133+
new_key = to_camel(key) # Convert key to camelCase
134+
if isinstance(value, dict):
135+
new_dict[new_key] = to_camel_recursive(value) # Recursive call for dicts
136+
else:
137+
new_dict[new_key] = value
138+
return new_dict
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""modify licensed items DB
2+
3+
Revision ID: 7d1c6425a51d
4+
Revises: 4f31760a63ba
5+
Create Date: 2025-01-30 17:32:31.969343+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "7d1c6425a51d"
13+
down_revision = "4f31760a63ba"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
"licensed_items", sa.Column("display_name", sa.String(), nullable=False)
22+
)
23+
op.drop_column("licensed_items", "license_key")
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.add_column(
30+
"licensed_items",
31+
sa.Column("license_key", sa.VARCHAR(), autoincrement=False, nullable=True),
32+
)
33+
op.drop_column("licensed_items", "display_name")
34+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class LicensedResourceType(str, enum.Enum):
2929
primary_key=True,
3030
server_default=sa.text("gen_random_uuid()"),
3131
),
32+
sa.Column(
33+
"display_name",
34+
sa.String,
35+
nullable=False,
36+
doc="Display name for front-end",
37+
),
3238
sa.Column(
3339
"licensed_resource_name",
3440
sa.String,
@@ -70,13 +76,6 @@ class LicensedResourceType(str, enum.Enum):
7076
nullable=True,
7177
doc="Product name identifier. If None, then the item is not exposed",
7278
),
73-
sa.Column(
74-
"license_key",
75-
sa.String,
76-
nullable=True,
77-
doc="Purpose: Acts as a mapping key to the internal license server."
78-
"Usage: The Sim4Life base applications use this key to check out a seat from the internal license server.",
79-
),
8079
column_created_datetime(timezone=True),
8180
column_modified_datetime(timezone=True),
8281
column_trashed_datetime("licensed_item"),

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/licensed_items.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import logging
22

33
from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
4-
from models_library.api_schemas_webserver.licensed_items import (
5-
LicensedItemGet,
6-
LicensedItemGetPage,
7-
)
4+
from models_library.api_schemas_webserver.licensed_items import LicensedItemRpcGetPage
85
from models_library.api_schemas_webserver.licensed_items_checkouts import (
96
LicensedItemCheckoutRpcGet,
107
)
@@ -31,15 +28,15 @@ async def get_licensed_items(
3128
product_name: str,
3229
offset: int = 0,
3330
limit: int = 20,
34-
) -> LicensedItemGetPage:
35-
result: LicensedItemGetPage = await rabbitmq_rpc_client.request(
31+
) -> LicensedItemRpcGetPage:
32+
result: LicensedItemRpcGetPage = await rabbitmq_rpc_client.request(
3633
WEBSERVER_RPC_NAMESPACE,
3734
TypeAdapter(RPCMethodName).validate_python("get_licensed_items"),
3835
product_name=product_name,
3936
offset=offset,
4037
limit=limit,
4138
)
42-
assert isinstance(result, LicensedItemGetPage) # nosec
39+
assert isinstance(result, LicensedItemRpcGetPage) # nosec
4340
return result
4441

4542

@@ -52,8 +49,8 @@ async def get_available_licensed_items_for_wallet(
5249
user_id: UserID,
5350
offset: int = 0,
5451
limit: int = 20,
55-
) -> LicensedItemGetPage:
56-
result: LicensedItemGet = await rabbitmq_rpc_client.request(
52+
) -> LicensedItemRpcGetPage:
53+
result: LicensedItemRpcGetPage = await rabbitmq_rpc_client.request(
5754
WEBSERVER_RPC_NAMESPACE,
5855
TypeAdapter(RPCMethodName).validate_python(
5956
"get_available_licensed_items_for_wallet"
@@ -64,7 +61,7 @@ async def get_available_licensed_items_for_wallet(
6461
offset=offset,
6562
limit=limit,
6663
)
67-
assert isinstance(result, LicensedItemGetPage) # nosec
64+
assert isinstance(result, LicensedItemRpcGetPage) # nosec
6865
return result
6966

7067

services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
from datetime import datetime
44
from decimal import Decimal
5-
from typing import Annotated
5+
from typing import Annotated, Any
66

77
from models_library.api_schemas_api_server.pricing_plans import (
88
ServicePricingPlanGet as _ServicePricingPlanGet,
99
)
1010
from models_library.api_schemas_webserver.licensed_items import (
11-
LicensedItemGet as _LicensedItemGet,
11+
LicensedItemRpcGet as _LicensedItemGet,
1212
)
1313
from models_library.api_schemas_webserver.licensed_items_checkouts import (
1414
LicensedItemCheckoutRpcGet as _LicensedItemCheckoutRpcGet,
@@ -137,9 +137,9 @@ class ServicePricingPlanGetLegacy(BaseModel):
137137

138138
class LicensedItemGet(BaseModel):
139139
licensed_item_id: LicensedItemID
140-
name: Annotated[str, Field(alias="display_name")]
141-
license_key: str | None
140+
display_name: str
142141
licensed_resource_type: LicensedResourceType
142+
licensed_resource_data: dict[str, Any]
143143
pricing_plan_id: PricingPlanId
144144
created_at: datetime
145145
modified_at: datetime

services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fastapi import FastAPI
66
from fastapi_pagination import create_page
7-
from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage
7+
from models_library.api_schemas_webserver.licensed_items import LicensedItemRpcGetPage
88
from models_library.licensed_items import LicensedItemID
99
from models_library.resource_tracker_licensed_items_checkouts import (
1010
LicensedItemCheckoutID,
@@ -52,15 +52,15 @@
5252

5353

5454
def _create_licensed_items_get_page(
55-
*, licensed_items_page: LicensedItemGetPage, page_params: PaginationParams
55+
*, licensed_items_page: LicensedItemRpcGetPage, page_params: PaginationParams
5656
) -> Page[LicensedItemGet]:
5757
page = create_page(
5858
[
5959
LicensedItemGet(
6060
licensed_item_id=elm.licensed_item_id,
61-
name=elm.name,
62-
license_key=elm.license_key,
61+
display_name=elm.display_name,
6362
licensed_resource_type=elm.licensed_resource_type,
63+
licensed_resource_data=elm.licensed_resource_data,
6464
pricing_plan_id=elm.pricing_plan_id,
6565
created_at=elm.created_at,
6666
modified_at=elm.modified_at,

0 commit comments

Comments
 (0)