Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 Down Expand Up @@ -85,7 +86,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 @@ -15533,6 +15533,9 @@ components:
title: _ItisVipResourceRestData
_ItisVipRestData:
properties:
id:
type: integer
title: Id
description:
type: string
title: Description
Expand All @@ -15548,6 +15551,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