Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,10 @@ FUNCTION_SERVICES_AUTHORS='{"UN": {"name": "Unknown", "email": "[email protected]

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

# Can use 'docker run -it itisfoundation/invitations:latest simcore-service-invitations generate-dotenv --auto-password'
INVITATIONS_DEFAULT_PRODUCT=osparc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class LicensedItemRpcGetPage(NamedTuple):


class _ItisVipRestData(OutputSchema):
id: int
description: str
thumbnail: str
features: FeaturesDict # NOTE: here there is a bit of coupling with domain model
Expand All @@ -62,8 +63,9 @@ class _ItisVipRestData(OutputSchema):
class _ItisVipResourceRestData(OutputSchema):
category_id: IDStr
category_display: str
category_icon: HttpUrl | None = None # NOTE: Placeholder until provide @odeimaiz
source: _ItisVipRestData
terms_of_use_url: HttpUrl | None = None
terms_of_use_url: HttpUrl | None = None # NOTE: Placeholder until provided @mguidon


class LicensedItemRestGet(OutputSchema):
Expand All @@ -85,7 +87,6 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
{
"licensedItemId": "0362b88b-91f8-4b41-867c-35544ad1f7a1",
"displayName": "my best model",
"licensedResourceName": "best-model",
"licensedResourceType": f"{LicensedResourceType.VIP_MODEL}",
"licensedResourceData": cast(
JsonDict,
Expand Down
2 changes: 1 addition & 1 deletion packages/models-library/src/models_library/licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class FeaturesDict(TypedDict):
"doi": "10.1000/xyz123",
"license_key": "ABC123XYZ",
"license_version": "1.0",
"protection": "Encrypted",
"protection": "Code",
"available_from_url": "https://example.com/download",
"additional_field": "trimmed if rest",
}
Expand Down
23 changes: 19 additions & 4 deletions packages/models-library/tests/test_licenses.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
from models_library.api_schemas_webserver.licensed_items import LicensedItemRestGet
from models_library.licenses import LicensedItem
from pydantic import ConfigDict


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

payload = LicensedItemRestGet.from_domain_model(item)
got = LicensedItemRestGet.from_domain_model(item)

assert item.display_name == payload.display_name
assert item.display_name == got.display_name

# nullable doi
assert (
payload.licensed_resource_data.source.doi
got.licensed_resource_data.source.doi
== item.licensed_resource_data["source"]["doi"]
)

# date is required
assert payload.licensed_resource_data.source.features["date"]
assert got.licensed_resource_data.source.features["date"]

#
assert (
got.licensed_resource_data.source.id
== item.licensed_resource_data["source"]["id"]
)


def test_strict_check_of_examples():
class TestLicensedItemRestGet(LicensedItemRestGet):
model_config = ConfigDict(extra="forbid")

for example in LicensedItemRestGet.model_json_schema()["examples"]:
TestLicensedItemRestGet.model_validate(example)
3 changes: 3 additions & 0 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,9 @@ services:
LICENSES_ITIS_VIP_SYNCER_ENABLED : ${LICENSES_ITIS_VIP_SYNCER_ENABLED}
LICENSES_ITIS_VIP_API_URL: ${LICENSES_ITIS_VIP_API_URL}
LICENSES_ITIS_VIP_CATEGORIES: ${LICENSES_ITIS_VIP_CATEGORIES}
LICENSES_SPEAG_PHANTOMS_API_URL: ${LICENSES_SPEAG_PHANTOMS_API_URL}
LICENSES_SPEAG_PHANTOMS_CATEGORIES: ${LICENSES_SPEAG_PHANTOMS_CATEGORIES}


WEBSERVER_LOGIN: ${WEBSERVER_LOGIN}
LOGIN_ACCOUNT_DELETION_RETENTION_DAYS: ${LOGIN_ACCOUNT_DELETION_RETENTION_DAYS}
Expand Down
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.52.0
0.53.0
2 changes: 1 addition & 1 deletion services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.52.0
current_version = 0.53.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: simcore-service-webserver
description: Main service with an interface (http-API & websockets) to the web front-end
version: 0.52.0
version: 0.53.0
servers:
- url: ''
description: webserver
Expand Down Expand Up @@ -15518,6 +15518,11 @@ components:
categoryDisplay:
type: string
title: Categorydisplay
categoryIcon:
anyOf:
- type: string
- type: 'null'
title: Categoryicon
source:
$ref: '#/components/schemas/_ItisVipRestData'
termsOfUseUrl:
Expand All @@ -15533,6 +15538,9 @@ components:
title: _ItisVipResourceRestData
_ItisVipRestData:
properties:
id:
type: integer
title: Id
description:
type: string
title: Description
Expand All @@ -15548,6 +15556,7 @@ components:
title: Doi
type: object
required:
- id
- description
- thumbnail
- features
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import re
from typing import Annotated, Any, Literal, NamedTuple, TypeAlias
from typing import Annotated, Any, Literal, NamedTuple, TypeAlias, cast

from models_library.basic_types import IDStr
from models_library.licenses import FeaturesDict
from models_library.licenses import VIP_DETAILS_EXAMPLE, FeaturesDict
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Field,
HttpUrl,
StringConstraints,
TypeAdapter,
)
from pydantic.config import JsonDict

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

@staticmethod
def _update_json_schema_extra(schema: JsonDict) -> None:
schema.update(
{
"examples": [
# complete
cast(JsonDict, VIP_DETAILS_EXAMPLE),
# minimal
{
"id": 1,
"description": "A detailed description of the VIP model",
"thumbnail": "https://example.com/thumbnail.jpg",
"features": {"date": "2013-02-01"},
"doi": "null",
"license_key": "ABC123XYZ",
"license_version": "1.0",
"protection": "Code",
"available_from_url": "null",
},
]
}
)

model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)


class ItisVipResourceData(BaseModel):
category_id: IDStr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ def _validate_url_contains_category(url: str) -> str:
return url


def _to_categories(
api_url: str, category_map: dict[CategoryID, CategoryDisplay]
) -> list[CategoryTuple]:
return [
CategoryTuple(
url=HttpUrl(api_url.format(category=category_id)),
id=category_id,
display=category_display,
)
for category_id, category_display in category_map.items()
]


class ItisVipSettings(BaseCustomSettings):
LICENSES_ITIS_VIP_API_URL: Annotated[
str, AfterValidator(_validate_url_contains_category)
Expand All @@ -26,13 +39,20 @@ def get_urls(self) -> list[HttpUrl]:
]

def to_categories(self) -> list[CategoryTuple]:
return [
CategoryTuple(
url=HttpUrl(
self.LICENSES_ITIS_VIP_API_URL.format(category=category_id)
),
id=category_id,
display=category_display,
)
for category_id, category_display in self.LICENSES_ITIS_VIP_CATEGORIES.items()
]
return _to_categories(
self.LICENSES_ITIS_VIP_API_URL,
self.LICENSES_ITIS_VIP_CATEGORIES,
)


class SpeagPhantomsSettings(BaseCustomSettings):
LICENSES_SPEAG_PHANTOMS_API_URL: Annotated[
str, AfterValidator(_validate_url_contains_category)
]
LICENSES_SPEAG_PHANTOMS_CATEGORIES: dict[CategoryID, CategoryDisplay]

def to_categories(self) -> list[CategoryTuple]:
return _to_categories(
self.LICENSES_SPEAG_PHANTOMS_API_URL,
self.LICENSES_SPEAG_PHANTOMS_CATEGORIES,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
_itis_vip_service,
_licensed_items_service,
)
from simcore_service_webserver.licenses._itis_vip_settings import ItisVipSettings

from ..redis import get_redis_lock_manager_client_sdk, setup_redis
from ._itis_vip_models import CategoryTuple, ItisVipData, ItisVipResourceData
Expand Down Expand Up @@ -86,15 +85,10 @@ async def sync_resources_with_licensed_items(


def setup_itis_vip_syncer(
app: web.Application, settings: ItisVipSettings, resync_after: datetime.timedelta
app: web.Application,
categories: list[CategoryTuple],
resync_after: datetime.timedelta,
):
categories = settings.to_categories()
if not categories:
_logger.warning(
"Skipping setup_itis_vip_syncer. %s did not provide any category", settings
)
return

setup_redis(app)

async def _lifespan(app_: web.Application):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,21 @@ def setup_licenses(app: web.Application):
app.on_startup.append(_rpc.register_rpc_routes_on_startup)

if settings.LICENSES_ITIS_VIP_SYNCER_ENABLED and settings.LICENSES_ITIS_VIP:
_itis_vip_syncer_service.setup_itis_vip_syncer(
app,
settings=settings.LICENSES_ITIS_VIP,
resync_after=settings.LICENSES_ITIS_VIP_SYNCER_PERIODICITY,
)
categories = []
if settings.LICENSES_ITIS_VIP:
categories += settings.LICENSES_ITIS_VIP.to_categories()

if settings.LICENSES_SPEAG_PHANTOMS:
categories += settings.LICENSES_SPEAG_PHANTOMS.to_categories()

if categories:
_itis_vip_syncer_service.setup_itis_vip_syncer(
app,
categories=categories,
resync_after=settings.LICENSES_ITIS_VIP_SYNCER_PERIODICITY,
)
else:
_logger.warning(
"Skipping setup_itis_vip_syncer. Did not provide any category in settings %s",
settings.model_dump_json(indent=1),
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY
from settings_library.base import BaseCustomSettings

from ._itis_vip_settings import ItisVipSettings
from ._itis_vip_settings import ItisVipSettings, SpeagPhantomsSettings


class LicensesSettings(BaseCustomSettings):
# ITIS - VIP
LICENSES_ITIS_VIP: Annotated[
ItisVipSettings | None,
Field(
description="Settings for VIP license models",
description="Settings for VIP licensed models",
json_schema_extra={"auto_default_from_env": True},
),
]
Expand All @@ -23,6 +23,15 @@ class LicensesSettings(BaseCustomSettings):
days=1
)

# SPEAG - PHANTOMS
LICENSES_SPEAG_PHANTOMS: Annotated[
SpeagPhantomsSettings | None,
Field(
description="Settings for SPEAG licensed phantoms",
json_schema_extra={"auto_default_from_env": True},
),
]

# other licensed resources come here ...


Expand Down
Loading