Skip to content

Commit 1853c29

Browse files
authored
🎨 Enhances Iicence feature: new vendor phantoms and licensed-resource-data id 🚨 (#7179)
1 parent a8a8046 commit 1853c29

File tree

13 files changed

+132
-39
lines changed

13 files changed

+132
-39
lines changed

.env-devel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ FUNCTION_SERVICES_AUTHORS='{"UN": {"name": "Unknown", "email": "unknown@osparc.i
135135

136136
WEBSERVER_LICENSES={}
137137
LICENSES_ITIS_VIP_SYNCER_ENABLED=false
138-
LICENSES_ITIS_VIP_API_URL=https://some-api/{category}
138+
LICENSES_ITIS_VIP_API_URL=https://replace-with-itis-api/{category}
139139
LICENSES_ITIS_VIP_CATEGORIES='{"HumanWholeBody": "Humans", "HumanBodyRegion": "Humans (Region)", "AnimalWholeBody": "Animal"}'
140+
LICENSES_SPEAG_PHANTOMS_API_URL=https://replace-with-speag-api/{category}
141+
LICENSES_SPEAG_PHANTOMS_CATEGORIES='{"ComputationalPhantom": "Phantom of the Opera"}'
140142

141143
# Can use 'docker run -it itisfoundation/invitations:latest simcore-service-invitations generate-dotenv --auto-password'
142144
INVITATIONS_DEFAULT_PRODUCT=osparc

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class LicensedItemRpcGetPage(NamedTuple):
5353

5454

5555
class _ItisVipRestData(OutputSchema):
56+
id: int
5657
description: str
5758
thumbnail: str
5859
features: FeaturesDict # NOTE: here there is a bit of coupling with domain model
@@ -62,8 +63,9 @@ class _ItisVipRestData(OutputSchema):
6263
class _ItisVipResourceRestData(OutputSchema):
6364
category_id: IDStr
6465
category_display: str
66+
category_icon: HttpUrl | None = None # NOTE: Placeholder until provide @odeimaiz
6567
source: _ItisVipRestData
66-
terms_of_use_url: HttpUrl | None = None
68+
terms_of_use_url: HttpUrl | None = None # NOTE: Placeholder until provided @mguidon
6769

6870

6971
class LicensedItemRestGet(OutputSchema):
@@ -85,7 +87,6 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
8587
{
8688
"licensedItemId": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
8789
"displayName": "my best model",
88-
"licensedResourceName": "best-model",
8990
"licensedResourceType": f"{LicensedResourceType.VIP_MODEL}",
9091
"licensedResourceData": cast(
9192
JsonDict,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class FeaturesDict(TypedDict):
5353
"doi": "10.1000/xyz123",
5454
"license_key": "ABC123XYZ",
5555
"license_version": "1.0",
56-
"protection": "Encrypted",
56+
"protection": "Code",
5757
"available_from_url": "https://example.com/download",
5858
"additional_field": "trimmed if rest",
5959
}
Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
from models_library.api_schemas_webserver.licensed_items import LicensedItemRestGet
22
from models_library.licenses import LicensedItem
3+
from pydantic import ConfigDict
34

45

56
def test_licensed_item_from_domain_model():
67
for example in LicensedItem.model_json_schema()["examples"]:
78
item = LicensedItem.model_validate(example)
89

9-
payload = LicensedItemRestGet.from_domain_model(item)
10+
got = LicensedItemRestGet.from_domain_model(item)
1011

11-
assert item.display_name == payload.display_name
12+
assert item.display_name == got.display_name
1213

1314
# nullable doi
1415
assert (
15-
payload.licensed_resource_data.source.doi
16+
got.licensed_resource_data.source.doi
1617
== item.licensed_resource_data["source"]["doi"]
1718
)
1819

1920
# date is required
20-
assert payload.licensed_resource_data.source.features["date"]
21+
assert got.licensed_resource_data.source.features["date"]
22+
23+
#
24+
assert (
25+
got.licensed_resource_data.source.id
26+
== item.licensed_resource_data["source"]["id"]
27+
)
28+
29+
30+
def test_strict_check_of_examples():
31+
class TestLicensedItemRestGet(LicensedItemRestGet):
32+
model_config = ConfigDict(extra="forbid")
33+
34+
for example in LicensedItemRestGet.model_json_schema()["examples"]:
35+
TestLicensedItemRestGet.model_validate(example)

services/docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,9 @@ services:
708708
LICENSES_ITIS_VIP_SYNCER_ENABLED : ${LICENSES_ITIS_VIP_SYNCER_ENABLED}
709709
LICENSES_ITIS_VIP_API_URL: ${LICENSES_ITIS_VIP_API_URL}
710710
LICENSES_ITIS_VIP_CATEGORIES: ${LICENSES_ITIS_VIP_CATEGORIES}
711+
LICENSES_SPEAG_PHANTOMS_API_URL: ${LICENSES_SPEAG_PHANTOMS_API_URL}
712+
LICENSES_SPEAG_PHANTOMS_CATEGORIES: ${LICENSES_SPEAG_PHANTOMS_CATEGORIES}
713+
711714

712715
WEBSERVER_LOGIN: ${WEBSERVER_LOGIN}
713716
LOGIN_ACCOUNT_DELETION_RETENTION_DAYS: ${LOGIN_ACCOUNT_DELETION_RETENTION_DAYS}

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.52.0
1+
0.53.0

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.52.0
2+
current_version = 0.53.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: simcore-service-webserver
44
description: Main service with an interface (http-API & websockets) to the web front-end
5-
version: 0.52.0
5+
version: 0.53.0
66
servers:
77
- url: ''
88
description: webserver
@@ -15518,6 +15518,11 @@ components:
1551815518
categoryDisplay:
1551915519
type: string
1552015520
title: Categorydisplay
15521+
categoryIcon:
15522+
anyOf:
15523+
- type: string
15524+
- type: 'null'
15525+
title: Categoryicon
1552115526
source:
1552215527
$ref: '#/components/schemas/_ItisVipRestData'
1552315528
termsOfUseUrl:
@@ -15533,6 +15538,9 @@ components:
1553315538
title: _ItisVipResourceRestData
1553415539
_ItisVipRestData:
1553515540
properties:
15541+
id:
15542+
type: integer
15543+
title: Id
1553615544
description:
1553715545
type: string
1553815546
title: Description
@@ -15548,6 +15556,7 @@ components:
1554815556
title: Doi
1554915557
type: object
1555015558
required:
15559+
- id
1555115560
- description
1555215561
- thumbnail
1555315562
- features

services/web/server/src/simcore_service_webserver/licenses/_itis_vip_models.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import re
2-
from typing import Annotated, Any, Literal, NamedTuple, TypeAlias
2+
from typing import Annotated, Any, Literal, NamedTuple, TypeAlias, cast
33

44
from models_library.basic_types import IDStr
5-
from models_library.licenses import FeaturesDict
5+
from models_library.licenses import VIP_DETAILS_EXAMPLE, FeaturesDict
66
from pydantic import (
77
BaseModel,
88
BeforeValidator,
9+
ConfigDict,
910
Field,
1011
HttpUrl,
1112
StringConstraints,
1213
TypeAdapter,
1314
)
15+
from pydantic.config import JsonDict
1416

1517
_max_str_adapter: TypeAdapter[str] = TypeAdapter(
1618
Annotated[str, StringConstraints(strip_whitespace=True, max_length=1_000)]
@@ -53,6 +55,31 @@ class ItisVipData(BaseModel):
5355
protection: Annotated[Literal["Code", "PayPal"], Field(alias="Protection")]
5456
available_from_url: Annotated[HttpUrl | None, Field(alias="AvailableFromURL")]
5557

58+
@staticmethod
59+
def _update_json_schema_extra(schema: JsonDict) -> None:
60+
schema.update(
61+
{
62+
"examples": [
63+
# complete
64+
cast(JsonDict, VIP_DETAILS_EXAMPLE),
65+
# minimal
66+
{
67+
"id": 1,
68+
"description": "A detailed description of the VIP model",
69+
"thumbnail": "https://example.com/thumbnail.jpg",
70+
"features": {"date": "2013-02-01"},
71+
"doi": "null",
72+
"license_key": "ABC123XYZ",
73+
"license_version": "1.0",
74+
"protection": "Code",
75+
"available_from_url": "null",
76+
},
77+
]
78+
}
79+
)
80+
81+
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
82+
5683

5784
class ItisVipResourceData(BaseModel):
5885
category_id: IDStr

services/web/server/src/simcore_service_webserver/licenses/_itis_vip_settings.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ def _validate_url_contains_category(url: str) -> str:
1313
return url
1414

1515

16+
def _to_categories(
17+
api_url: str, category_map: dict[CategoryID, CategoryDisplay]
18+
) -> list[CategoryTuple]:
19+
return [
20+
CategoryTuple(
21+
url=HttpUrl(api_url.format(category=category_id)),
22+
id=category_id,
23+
display=category_display,
24+
)
25+
for category_id, category_display in category_map.items()
26+
]
27+
28+
1629
class ItisVipSettings(BaseCustomSettings):
1730
LICENSES_ITIS_VIP_API_URL: Annotated[
1831
str, AfterValidator(_validate_url_contains_category)
@@ -26,13 +39,20 @@ def get_urls(self) -> list[HttpUrl]:
2639
]
2740

2841
def to_categories(self) -> list[CategoryTuple]:
29-
return [
30-
CategoryTuple(
31-
url=HttpUrl(
32-
self.LICENSES_ITIS_VIP_API_URL.format(category=category_id)
33-
),
34-
id=category_id,
35-
display=category_display,
36-
)
37-
for category_id, category_display in self.LICENSES_ITIS_VIP_CATEGORIES.items()
38-
]
42+
return _to_categories(
43+
self.LICENSES_ITIS_VIP_API_URL,
44+
self.LICENSES_ITIS_VIP_CATEGORIES,
45+
)
46+
47+
48+
class SpeagPhantomsSettings(BaseCustomSettings):
49+
LICENSES_SPEAG_PHANTOMS_API_URL: Annotated[
50+
str, AfterValidator(_validate_url_contains_category)
51+
]
52+
LICENSES_SPEAG_PHANTOMS_CATEGORIES: dict[CategoryID, CategoryDisplay]
53+
54+
def to_categories(self) -> list[CategoryTuple]:
55+
return _to_categories(
56+
self.LICENSES_SPEAG_PHANTOMS_API_URL,
57+
self.LICENSES_SPEAG_PHANTOMS_CATEGORIES,
58+
)

services/web/server/src/simcore_service_webserver/licenses/_itis_vip_syncer_service.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
_itis_vip_service,
1414
_licensed_items_service,
1515
)
16-
from simcore_service_webserver.licenses._itis_vip_settings import ItisVipSettings
1716

1817
from ..redis import get_redis_lock_manager_client_sdk, setup_redis
1918
from ._itis_vip_models import CategoryTuple, ItisVipData, ItisVipResourceData
@@ -86,15 +85,10 @@ async def sync_resources_with_licensed_items(
8685

8786

8887
def setup_itis_vip_syncer(
89-
app: web.Application, settings: ItisVipSettings, resync_after: datetime.timedelta
88+
app: web.Application,
89+
categories: list[CategoryTuple],
90+
resync_after: datetime.timedelta,
9091
):
91-
categories = settings.to_categories()
92-
if not categories:
93-
_logger.warning(
94-
"Skipping setup_itis_vip_syncer. %s did not provide any category", settings
95-
)
96-
return
97-
9892
setup_redis(app)
9993

10094
async def _lifespan(app_: web.Application):

services/web/server/src/simcore_service_webserver/licenses/plugin.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,21 @@ def setup_licenses(app: web.Application):
4141
app.on_startup.append(_rpc.register_rpc_routes_on_startup)
4242

4343
if settings.LICENSES_ITIS_VIP_SYNCER_ENABLED and settings.LICENSES_ITIS_VIP:
44-
_itis_vip_syncer_service.setup_itis_vip_syncer(
45-
app,
46-
settings=settings.LICENSES_ITIS_VIP,
47-
resync_after=settings.LICENSES_ITIS_VIP_SYNCER_PERIODICITY,
48-
)
44+
categories = []
45+
if settings.LICENSES_ITIS_VIP:
46+
categories += settings.LICENSES_ITIS_VIP.to_categories()
47+
48+
if settings.LICENSES_SPEAG_PHANTOMS:
49+
categories += settings.LICENSES_SPEAG_PHANTOMS.to_categories()
50+
51+
if categories:
52+
_itis_vip_syncer_service.setup_itis_vip_syncer(
53+
app,
54+
categories=categories,
55+
resync_after=settings.LICENSES_ITIS_VIP_SYNCER_PERIODICITY,
56+
)
57+
else:
58+
_logger.warning(
59+
"Skipping setup_itis_vip_syncer. Did not provide any category in settings %s",
60+
settings.model_dump_json(indent=1),
61+
)

services/web/server/src/simcore_service_webserver/licenses/settings.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY
77
from settings_library.base import BaseCustomSettings
88

9-
from ._itis_vip_settings import ItisVipSettings
9+
from ._itis_vip_settings import ItisVipSettings, SpeagPhantomsSettings
1010

1111

1212
class LicensesSettings(BaseCustomSettings):
1313
# ITIS - VIP
1414
LICENSES_ITIS_VIP: Annotated[
1515
ItisVipSettings | None,
1616
Field(
17-
description="Settings for VIP license models",
17+
description="Settings for VIP licensed models",
1818
json_schema_extra={"auto_default_from_env": True},
1919
),
2020
]
@@ -23,6 +23,15 @@ class LicensesSettings(BaseCustomSettings):
2323
days=1
2424
)
2525

26+
# SPEAG - PHANTOMS
27+
LICENSES_SPEAG_PHANTOMS: Annotated[
28+
SpeagPhantomsSettings | None,
29+
Field(
30+
description="Settings for SPEAG licensed phantoms",
31+
json_schema_extra={"auto_default_from_env": True},
32+
),
33+
]
34+
2635
# other licensed resources come here ...
2736

2837

0 commit comments

Comments
 (0)