Skip to content

Commit 525778e

Browse files
authored
Merge branch 'master' into feature/standalone-projects
2 parents e1b0be4 + 15d54a6 commit 525778e

File tree

25 files changed

+679
-236
lines changed

25 files changed

+679
-236
lines changed

api/specs/web-server/_licensed_items.py

Lines changed: 4 additions & 3 deletions
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()],
Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
from datetime import datetime
2-
from typing import NamedTuple
2+
from typing import Any, NamedTuple, cast
33

4-
from models_library.licensed_items import LicensedItemID, LicensedResourceType
4+
from models_library.licensed_items import (
5+
VIP_DETAILS_EXAMPLE,
6+
LicensedItemID,
7+
LicensedResourceType,
8+
)
59
from models_library.resource_tracker import PricingPlanId
6-
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
714

815
from ._base import OutputSchema
916

17+
# RPC
1018

11-
class LicensedItemGet(OutputSchema):
19+
20+
class LicensedItemRpcGet(BaseModel):
1221
licensed_item_id: LicensedItemID
13-
name: str
14-
license_key: str | None
22+
display_name: str
1523
licensed_resource_type: LicensedResourceType
24+
licensed_resource_data: dict[str, Any]
1625
pricing_plan_id: PricingPlanId
1726
created_at: datetime
1827
modified_at: datetime
@@ -21,9 +30,46 @@ class LicensedItemGet(OutputSchema):
2130
"examples": [
2231
{
2332
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
24-
"name": "best-model",
25-
"license_key": "license-specific-key",
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+
49+
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+
]
60+
pricing_plan_id: PricingPlanId
61+
62+
created_at: datetime
63+
modified_at: datetime
64+
65+
model_config = ConfigDict(
66+
json_schema_extra={
67+
"examples": [
68+
{
69+
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
70+
"display_name": "best-model",
2671
"licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}",
72+
"licensed_resource_data": cast(JsonDict, VIP_DETAILS_EXAMPLE),
2773
"pricing_plan_id": "15",
2874
"created_at": "2024-12-12 09:59:26.422140",
2975
"modified_at": "2024-12-12 09:59:26.422140",
@@ -33,6 +79,6 @@ class LicensedItemGet(OutputSchema):
3379
)
3480

3581

36-
class LicensedItemGetPage(NamedTuple):
37-
items: list[LicensedItemGet]
82+
class LicensedItemRestGetPage(NamedTuple):
83+
items: list[LicensedItemRestGet]
3884
total: PositiveInt
Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import TypeAlias
3+
from typing import Any, TypeAlias
44
from uuid import UUID
55

6-
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic import BaseModel, ConfigDict
77

88
from .products import ProductName
99
from .resource_tracker import PricingPlanId
@@ -16,30 +16,59 @@ 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-
name: str
27-
license_key: str | None
53+
display_name: str
54+
55+
licensed_resource_name: str
2856
licensed_resource_type: LicensedResourceType
29-
pricing_plan_id: PricingPlanId
30-
product_name: ProductName
31-
created: datetime = Field(
32-
...,
33-
description="Timestamp on creation",
34-
)
35-
modified: datetime = Field(
36-
...,
37-
description="Timestamp of last modification",
38-
)
39-
# ----
57+
licensed_resource_data: dict[str, Any] | None
58+
59+
pricing_plan_id: PricingPlanId | None
60+
product_name: ProductName | None
61+
62+
# states
63+
created: datetime
64+
modified: datetime
65+
trashed: datetime | None
66+
4067
model_config = ConfigDict(from_attributes=True)
4168

4269

4370
class LicensedItemUpdateDB(BaseModel):
44-
name: str | None = None
71+
display_name: str | None = None
72+
licensed_resource_name: str | None = None
4573
pricing_plan_id: PricingPlanId | None = None
74+
trash: bool | None = None

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

Lines changed: 16 additions & 0 deletions
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
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""add data to licensed_items
2+
3+
Revision ID: 4f31760a63ba
4+
Revises: 1bc517536e0a
5+
Create Date: 2025-01-29 16:51:16.453069+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "4f31760a63ba"
14+
down_revision = "1bc517536e0a"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
21+
with op.batch_alter_table("licensed_items") as batch_op:
22+
batch_op.alter_column(
23+
"name",
24+
new_column_name="licensed_resource_name",
25+
existing_type=sa.String(),
26+
nullable=False,
27+
)
28+
batch_op.alter_column(
29+
"pricing_plan_id",
30+
existing_type=sa.Integer(),
31+
nullable=True,
32+
)
33+
batch_op.alter_column(
34+
"product_name",
35+
existing_type=sa.String(),
36+
nullable=True,
37+
)
38+
39+
# ### commands auto generated by Alembic - please adjust! ###
40+
op.add_column(
41+
"licensed_items",
42+
sa.Column(
43+
"licensed_resource_data",
44+
postgresql.JSONB(astext_type=sa.Text()),
45+
nullable=True,
46+
),
47+
)
48+
op.add_column(
49+
"licensed_items",
50+
sa.Column(
51+
"trashed",
52+
sa.DateTime(timezone=True),
53+
nullable=True,
54+
comment="The date and time when the licensed_item was marked as trashed. Null if the licensed_item has not been trashed [default].",
55+
),
56+
)
57+
# ### end Alembic commands ###
58+
59+
60+
def downgrade():
61+
# ### commands auto generated by Alembic - please adjust! ###
62+
op.drop_column("licensed_items", "trashed")
63+
op.drop_column("licensed_items", "licensed_resource_data")
64+
# ### end Alembic commands ###
65+
66+
# Delete rows with null values in pricing_plan_id and product_name
67+
op.execute(
68+
sa.DDL(
69+
"""
70+
DELETE FROM licensed_items
71+
WHERE pricing_plan_id IS NULL OR product_name IS NULL;
72+
"""
73+
)
74+
)
75+
print(
76+
"Warning: Rows with null values in pricing_plan_id or product_name have been deleted."
77+
)
78+
79+
with op.batch_alter_table("licensed_items") as batch_op:
80+
81+
batch_op.alter_column(
82+
"product_name",
83+
existing_type=sa.String(),
84+
nullable=False,
85+
)
86+
batch_op.alter_column(
87+
"pricing_plan_id",
88+
existing_type=sa.Integer(),
89+
nullable=False,
90+
)
91+
batch_op.alter_column(
92+
"licensed_resource_name",
93+
new_column_name="name",
94+
existing_type=sa.String(),
95+
nullable=False,
96+
)
Lines changed: 34 additions & 0 deletions
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 ###

0 commit comments

Comments
 (0)