Skip to content

Commit e1cd3e5

Browse files
daily work
1 parent b3fb639 commit e1cd3e5

File tree

20 files changed

+616
-153
lines changed

20 files changed

+616
-153
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: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,85 @@
11
from datetime import datetime
22
from typing import NamedTuple
33

4-
from models_library.licensed_items import LicensedItemID, LicensedResourceType
4+
from models_library.licensed_items import (
5+
LicensedItemID,
6+
LicensedResourceType,
7+
VipDetails,
8+
)
59
from models_library.resource_tracker import PricingPlanId
6-
from pydantic import ConfigDict, PositiveInt
10+
from pydantic import BaseModel, ConfigDict, PositiveInt
11+
from pydantic.alias_generators import to_camel
712

813
from ._base import OutputSchema
914

15+
# RPC
1016

11-
class LicensedItemGet(OutputSchema):
17+
18+
class LicensedItemRpcGet(BaseModel):
19+
licensed_item_id: LicensedItemID
20+
display_name: str
21+
licensed_resource_type: LicensedResourceType
22+
pricing_plan_id: PricingPlanId
23+
licensed_resource_type_details: VipDetails
24+
created_at: datetime
25+
modified_at: datetime
26+
model_config = ConfigDict(
27+
json_schema_extra={
28+
"examples": [
29+
{
30+
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
31+
"display_name": "best-model",
32+
"licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}",
33+
"pricing_plan_id": "15",
34+
"licensed_resource_type_details": VipDetails.model_config[
35+
"json_schema_extra"
36+
]["examples"][0],
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+
class CustomBaseModel(BaseModel):
52+
model_config = ConfigDict(
53+
alias_generator=to_camel, populate_by_name=True, extra="allow"
54+
)
55+
56+
def model_dump_camel(self):
57+
data = self.model_dump(by_alias=True)
58+
if hasattr(self, "__pydantic_extra__") and self.__pydantic_extra__:
59+
extra_camel = {to_camel(k): v for k, v in self.__pydantic_extra__.items()}
60+
data.update(extra_camel)
61+
return data
62+
63+
64+
class LicensedItemRestGet(OutputSchema):
1265
licensed_item_id: LicensedItemID
13-
name: str
14-
license_key: str | None
66+
display_name: str
1567
licensed_resource_type: LicensedResourceType
1668
pricing_plan_id: PricingPlanId
69+
licensed_resource_type_details: VipDetails
1770
created_at: datetime
1871
modified_at: datetime
1972
model_config = ConfigDict(
2073
json_schema_extra={
2174
"examples": [
2275
{
2376
"licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
24-
"name": "best-model",
25-
"license_key": "license-specific-key",
77+
"display_name": "best-model",
2678
"licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}",
2779
"pricing_plan_id": "15",
80+
"licensed_resource_type_details": VipDetails.model_config[
81+
"json_schema_extra"
82+
]["examples"][0],
2883
"created_at": "2024-12-12 09:59:26.422140",
2984
"modified_at": "2024-12-12 09:59:26.422140",
3085
}
@@ -33,6 +88,6 @@ class LicensedItemGet(OutputSchema):
3388
)
3489

3590

36-
class LicensedItemGetPage(NamedTuple):
37-
items: list[LicensedItemGet]
91+
class LicensedItemRestGetPage(NamedTuple):
92+
items: list[LicensedItemRestGet]
3893
total: PositiveInt

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

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from typing import TypeAlias
44
from uuid import UUID
55

6-
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic import AliasGenerator, BaseModel, ConfigDict, Field
7+
from pydantic.alias_generators import to_camel
78

89
from .products import ProductName
910
from .resource_tracker import PricingPlanId
@@ -16,18 +17,92 @@ class LicensedResourceType(StrAutoEnum):
1617
VIP_MODEL = auto()
1718

1819

20+
class VipFeatures(BaseModel):
21+
name: str
22+
version: str
23+
sex: str
24+
age: str
25+
weight: str
26+
height: str
27+
data: str
28+
ethnicity: str
29+
functionality: str
30+
31+
model_config = ConfigDict(
32+
alias_generator=AliasGenerator(
33+
serialization_alias=to_camel,
34+
),
35+
populate_by_name=True,
36+
extra="allow",
37+
json_schema_extra={
38+
"examples": [
39+
{
40+
"name": "Duke",
41+
"version": "V2.0",
42+
"sex": "Male",
43+
"age": "34 years",
44+
"weight": "70.2 Kg",
45+
"height": "1.77 m",
46+
"data": "2015-03-01",
47+
"ethnicity": "Caucasian",
48+
"functionality": "Static",
49+
"additional_field": "allowed",
50+
}
51+
]
52+
},
53+
)
54+
55+
56+
class VipDetails(BaseModel):
57+
id: int
58+
description: str
59+
thumbnail: str
60+
features: VipFeatures
61+
doi: str
62+
license_key: str | None
63+
license_version: str | None
64+
protection: str
65+
available_from_url: str
66+
67+
model_config = ConfigDict(
68+
alias_generator=AliasGenerator(
69+
serialization_alias=to_camel,
70+
),
71+
populate_by_name=True,
72+
extra="allow",
73+
json_schema_extra={
74+
"examples": [
75+
{
76+
"id": 1,
77+
"description": "custom description",
78+
"thumbnail": "custom description",
79+
"features": VipFeatures.model_config["json_schema_extra"][
80+
"examples"
81+
][0],
82+
"doi": "custom value",
83+
"license_key": "custom value",
84+
"license_version": "custom value",
85+
"protection": "custom value",
86+
"available_from_url": "custom value",
87+
"additional_field": "allowed",
88+
}
89+
]
90+
},
91+
)
92+
93+
1994
#
2095
# DB
2196
#
2297

2398

2499
class LicensedItemDB(BaseModel):
25100
licensed_item_id: LicensedItemID
26-
name: str
27-
license_key: str | None
101+
display_name: str
28102
licensed_resource_type: LicensedResourceType
29103
pricing_plan_id: PricingPlanId
30104
product_name: ProductName
105+
licensed_resource_type_details: VipDetails
31106
created: datetime = Field(
32107
...,
33108
description="Timestamp on creation",
@@ -41,5 +116,5 @@ class LicensedItemDB(BaseModel):
41116

42117

43118
class LicensedItemUpdateDB(BaseModel):
44-
name: str | None = None
119+
display_name: str | None = None
45120
pricing_plan_id: PricingPlanId | None = None
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""modify licensed items DB
2+
3+
Revision ID: 7bde20ddf18e
4+
Revises: 1bc517536e0a
5+
Create Date: 2025-01-29 12:34:18.162155+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 = "7bde20ddf18e"
14+
down_revision = "1bc517536e0a"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column(
22+
"licensed_items", sa.Column("display_name", sa.String(), nullable=False)
23+
)
24+
op.add_column(
25+
"licensed_items",
26+
sa.Column(
27+
"licensed_resource_type_details",
28+
postgresql.JSONB(astext_type=sa.Text()),
29+
server_default=sa.text("'{}'::jsonb"),
30+
nullable=False,
31+
),
32+
)
33+
op.drop_column("licensed_items", "name")
34+
op.drop_column("licensed_items", "license_key")
35+
# ### end Alembic commands ###
36+
37+
38+
def downgrade():
39+
# ### commands auto generated by Alembic - please adjust! ###
40+
op.add_column(
41+
"licensed_items",
42+
sa.Column("license_key", sa.VARCHAR(), autoincrement=False, nullable=True),
43+
)
44+
op.add_column(
45+
"licensed_items",
46+
sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False),
47+
)
48+
op.drop_column("licensed_items", "licensed_resource_type_details")
49+
op.drop_column("licensed_items", "display_name")
50+
# ### end Alembic commands ###

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import enum
55

66
import sqlalchemy as sa
7-
from sqlalchemy.dialects.postgresql import UUID
7+
from sqlalchemy.dialects.postgresql import JSONB, UUID
88

99
from ._common import RefActions, column_created_datetime, column_modified_datetime
1010
from .base import metadata
@@ -25,7 +25,7 @@ class LicensedResourceType(str, enum.Enum):
2525
server_default=sa.text("gen_random_uuid()"),
2626
),
2727
sa.Column(
28-
"name",
28+
"display_name",
2929
sa.String,
3030
nullable=False,
3131
),
@@ -59,10 +59,10 @@ class LicensedResourceType(str, enum.Enum):
5959
doc="Product name",
6060
),
6161
sa.Column(
62-
"license_key",
63-
sa.String,
64-
nullable=True,
65-
doc="Purpose: Acts as a mapping key to the internal license server. Usage: The Sim4Life base applications use this key to check out a seat from the internal license server.",
62+
"licensed_resource_type_details",
63+
JSONB,
64+
nullable=False,
65+
server_default=sa.text("'{}'::jsonb"),
6666
),
6767
column_created_datetime(timezone=True),
6868
column_modified_datetime(timezone=True),
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# pylint: disable=no-name-in-module
2+
# pylint: disable=no-value-for-parameter
3+
# pylint: disable=redefined-outer-name
4+
# pylint: disable=unused-argument
5+
6+
7+
from collections.abc import Callable
8+
9+
import pytest
10+
from aiopg.sa.exc import ResourceClosedError
11+
from faker import Faker
12+
from pytest_simcore.helpers.faker_factories import random_product
13+
from simcore_postgres_database.webserver_models import products
14+
from sqlalchemy.dialects.postgresql import insert as pg_insert
15+
16+
17+
@pytest.fixture
18+
def products_regex() -> dict:
19+
return {
20+
"s4l": r"(^s4l[\.-])|(^sim4life\.)",
21+
"osparc": r"^osparc.",
22+
"tis": r"(^ti.[\.-])|(^ti-solution\.)",
23+
}
24+
25+
26+
@pytest.fixture
27+
def products_names(products_regex: dict) -> list[str]:
28+
return list(products_regex)
29+
30+
31+
@pytest.fixture
32+
def make_products_table(products_regex: dict, faker: Faker) -> Callable:
33+
async def _make(conn) -> None:
34+
for n, (name, regex) in enumerate(products_regex.items()):
35+
36+
result = await conn.execute(
37+
pg_insert(products)
38+
.values(
39+
**random_product(
40+
name=name,
41+
display_name=f"Product {name.capitalize()}",
42+
short_name=name[:3].lower(),
43+
host_regex=regex,
44+
priority=n,
45+
)
46+
)
47+
.on_conflict_do_update(
48+
index_elements=[products.c.name],
49+
set_={
50+
"display_name": f"Product {name.capitalize()}",
51+
"short_name": name[:3].lower(),
52+
"host_regex": regex,
53+
"priority": n,
54+
},
55+
)
56+
)
57+
58+
assert result.closed
59+
assert not result.returns_rows
60+
with pytest.raises(ResourceClosedError):
61+
await result.scalar()
62+
63+
return _make

0 commit comments

Comments
 (0)