From b1961b8a1b641192c936bc92b11b7d956c607b13 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 24 Jul 2025 11:09:16 +0200 Subject: [PATCH 01/11] fix: solver functions get response --- .../src/models_library/api_schemas_webserver/functions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index d7883745754b..7ba4fd523a06 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -116,6 +116,10 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] + created_at: Annotated[datetime.datetime, Field(alias="creationDate")] + modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] + access_rights: FunctionAccessRights | None = None + thumbnail: str | None = None class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): From 4c549d59d10a4cca6db8923b20a394164aef9087 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 24 Jul 2025 15:02:40 +0200 Subject: [PATCH 02/11] feat: enrich solver functions response --- .../functions/_controller/_functions_rest.py | 243 +++++++++++------- .../functions/_services_metadata/__init__.py | 0 .../functions/_services_metadata/_errors.py | 5 + .../functions/_services_metadata/_models.py | 8 + .../_services_metadata/_repository.py | 69 +++++ .../functions/_services_metadata/_service.py | 26 ++ .../functions/_services_metadata/service.py | 9 + .../projects/_projects_repository.py | 10 +- .../projects/_projects_service.py | 6 +- 9 files changed, 281 insertions(+), 95 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/__init__.py create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/_errors.py create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/_models.py create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py create mode 100644 services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index d450ef5f5f1c..ee3630bdf0af 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -1,3 +1,5 @@ +from typing import Any + from aiohttp import web from models_library.api_schemas_webserver.functions import ( Function, @@ -5,13 +7,19 @@ RegisteredFunction, RegisteredFunctionGet, RegisteredFunctionUpdate, - RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet -from models_library.functions import FunctionClass, RegisteredProjectFunction +from models_library.functions import ( + FunctionClass, + FunctionID, + RegisteredProjectFunction, + RegisteredSolverFunction, +) from models_library.products import ProductName +from models_library.projects import ProjectID from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data +from models_library.services_types import ServiceKey, ServiceVersion from models_library.users import UserID from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -29,6 +37,8 @@ from ...security.decorators import permission_required from ...utils_aiohttp import create_json_response_from_page, envelope_json_response from .. import _functions_service +from .._services_metadata import service as _services_metadata_service +from .._services_metadata.service import ServiceMetadata from ._functions_rest_exceptions import handle_rest_requests_exceptions from ._functions_rest_schemas import ( FunctionGetQueryParams, @@ -39,30 +49,76 @@ routes = web.RouteTableDef() -async def _add_extras_to_project_function( - function: RegisteredProjectFunction, +async def _build_function_access_rights_dict( app: web.Application, user_id: UserID, product_name: ProductName, -) -> dict: - assert isinstance(function, RegisteredProjectFunction) # nosec + function_id: FunctionID, +) -> dict[str, Any]: + access_rights = await _functions_service.get_function_user_permissions( + app=app, + user_id=user_id, + product_name=product_name, + function_id=function_id, + ) + + return { + "access_rights": access_rights.model_dump(), + } + +async def _build_project_function_extras_dict( + app: web.Application, + *, + user_id: UserID, + function: RegisteredProjectFunction, +) -> dict[str, Any]: project_dict = await _projects_service.get_project_for_user( app=app, project_uuid=f"{function.project_id}", user_id=user_id, ) - function_with_extras = function.model_dump(mode="json") | { - "access_rights": await _functions_service.get_function_user_permissions( - app, + return { + "thumbnail": project_dict.get("thumbnail", None), + } + + +async def _build_function_extras( + app: web.Application, user_id: UserID, *, function: RegisteredFunction +) -> dict[str, Any]: + extras: dict[str, Any] = {} + if function.function_class == FunctionClass.PROJECT: + assert isinstance(function, RegisteredProjectFunction) + extras |= await _build_project_function_extras_dict( + function=function, + app=app, user_id=user_id, - product_name=product_name, - function_id=function.uid, + ) + elif function.function_class == FunctionClass.SOLVER: + assert isinstance(function, RegisteredSolverFunction) + extras |= await _build_solver_function_extras_dict( + app, + function=function, + ) + return extras + + +async def _build_solver_function_extras_dict( + app: web.Application, + *, + function: RegisteredSolverFunction, +) -> dict[str, Any]: + services_metadata = await _services_metadata_service.get_service_metadata( + app, + key=function.solver_key, + version=function.solver_version, + ) + return { + "thumbnail": ( + f"{services_metadata.thumbnail}" if services_metadata.thumbnail else None ), - "thumbnail": project_dict.get("thumbnail", None), } - return function_with_extras @routes.post(f"/{VTAG}/functions", name="register_function") @@ -117,55 +173,76 @@ async def list_functions(request: web.Request) -> web.Response: ) chunk: list[RegisteredFunctionGet] = [] - projects_map: dict[str, ProjectDBGet | None] = ( + + projects_cache: dict[ProjectID, ProjectDBGet] = {} + service_metadata_cache: dict[tuple[ServiceKey, ServiceVersion], ServiceMetadata] = ( {} - ) # ProjectDBGet has to be renamed at some point! + ) if query_params.include_extras: - project_ids = [] - for function in functions: - if function.function_class == FunctionClass.PROJECT: - assert isinstance(function, RegisteredProjectFunction) - project_ids.append(function.project_id) - - projects_map = { - f"{p.uuid}": p - for p in await _projects_service.batch_get_projects( + if any( + function.function_class == FunctionClass.PROJECT for function in functions + ): + project_uuids = [ + function.project_id + for function in functions + if function.function_class == FunctionClass.PROJECT + ] + projects_cache = await _projects_service.batch_get_projects( request.app, - project_uuids=project_ids, + project_uuids=project_uuids, ) - } - - for function in functions: - if ( - query_params.include_extras - and function.function_class == FunctionClass.PROJECT + if any( + function.function_class == FunctionClass.SOLVER for function in functions ): - assert isinstance(function, RegisteredProjectFunction) # nosec - if project := projects_map.get(f"{function.project_id}"): - chunk.append( - TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function.model_dump(mode="json") - | { - "access_rights": await _functions_service.get_function_user_permissions( - request.app, - user_id=req_ctx.user_id, - product_name=req_ctx.product_name, - function_id=function.uid, - ), - "thumbnail": ( - f"{project.thumbnail}" if project.thumbnail else None - ), - } - ) - ) - else: - chunk.append( - TypeAdapter(RegisteredFunctionGet).validate_python( - function.model_dump(mode="json") + service_keys_and_versions = [ + (function.solver_key, function.solver_version) + for function in functions + if function.function_class == FunctionClass.SOLVER + ] + service_metadata_cache = ( + await _services_metadata_service.batch_get_service_metadata( + app=request.app, keys_and_versions=service_keys_and_versions ) ) + for function in functions: + access_rights = await _build_function_access_rights_dict( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function.uid, + ) + + extras: dict[str, Any] = {} + if query_params.include_extras: + if function.function_class == FunctionClass.PROJECT: + assert isinstance(function, RegisteredProjectFunction) # nosec + if project := projects_cache.get(function.project_id): + extras = { + "thumbnail": ( + f"{project.thumbnail}" if project.thumbnail else None + ), + } + elif function.function_class == FunctionClass.SOLVER: + assert isinstance(function, RegisteredSolverFunction) + if service_metadata := service_metadata_cache.get( + (function.solver_key, function.solver_version) + ): + extras = { + "thumbnail": ( + f"{service_metadata.thumbnail}" + if service_metadata.thumbnail + else None + ), + } + + chunk.append( + TypeAdapter(RegisteredFunctionGet).validate_python( + function.model_dump() | access_rights | extras + ) + ) + page = Page[RegisteredFunctionGet].model_validate( paginate_data( chunk=chunk, @@ -194,33 +271,29 @@ async def get_function(request: web.Request) -> web.Response: ) req_ctx = AuthenticatedRequestContext.model_validate(request) - registered_function: RegisteredFunction = await _functions_service.get_function( + function = await _functions_service.get_function( app=request.app, function_id=function_id, user_id=req_ctx.user_id, product_name=req_ctx.product_name, ) - if ( - query_params.include_extras - and registered_function.function_class == FunctionClass.PROJECT - ): - function_with_extras = await _add_extras_to_project_function( - function=registered_function, - app=request.app, - user_id=req_ctx.user_id, - product_name=req_ctx.product_name, - ) + access_rights = await _build_function_access_rights_dict( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + ) - return envelope_json_response( - TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function_with_extras - ) - ) + extras = ( + await _build_function_extras(request.app, req_ctx.user_id, function=function) + if query_params.include_extras + else {} + ) return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( - registered_function.model_dump(mode="json") + function.model_dump() | access_rights | extras ) ) @@ -245,7 +318,7 @@ async def update_function(request: web.Request) -> web.Response: ) req_ctx = AuthenticatedRequestContext.model_validate(request) - updated_function = await _functions_service.update_function( + function = await _functions_service.update_function( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -253,26 +326,22 @@ async def update_function(request: web.Request) -> web.Response: function=function_update, ) - if ( - query_params.include_extras - and updated_function.function_class == FunctionClass.PROJECT - ): - function_with_extras = await _add_extras_to_project_function( - function=updated_function, - app=request.app, - user_id=req_ctx.user_id, - product_name=req_ctx.product_name, - ) + access_rights = await _build_function_access_rights_dict( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + ) - return envelope_json_response( - TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function_with_extras - ) - ) + extras = ( + await _build_function_extras(request.app, req_ctx.user_id, function=function) + if query_params.include_extras + else {} + ) return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( - updated_function.model_dump(mode="json") + function.model_dump() | access_rights | extras ) ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/__init__.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_errors.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_errors.py new file mode 100644 index 000000000000..b18455467519 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_errors.py @@ -0,0 +1,5 @@ +from common_library.errors_classes import OsparcErrorMixin + + +class ServiceMetadataNotFoundError(OsparcErrorMixin, Exception): + msg_template = "Service metadata for key {key} and version {version} not found" diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_models.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_models.py new file mode 100644 index 000000000000..e9876b7770f2 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_models.py @@ -0,0 +1,8 @@ +from models_library.services_types import ServiceKey, ServiceVersion +from pydantic import BaseModel, HttpUrl + + +class ServiceMetadata(BaseModel): + key: ServiceKey + version: ServiceVersion + thumbnail: HttpUrl | None diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py new file mode 100644 index 000000000000..c5af84060c08 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py @@ -0,0 +1,69 @@ +from collections.abc import Iterable + +from aiohttp import web +from models_library.services_types import ServiceKey, ServiceVersion +from simcore_postgres_database.models.services import ( + services_meta_data, +) +from simcore_postgres_database.utils_repos import pass_or_acquire_connection +from simcore_service_webserver.functions._services_metadata._errors import ( + ServiceMetadataNotFoundError, +) +from sqlalchemy import select, tuple_ +from sqlalchemy.ext.asyncio import AsyncConnection + +from ...db.plugin import get_asyncpg_engine +from ._models import ServiceMetadata + + +async def batch_service_metadata( + app: web.Application, + connection: AsyncConnection | None = None, + *, + keys_and_versions: Iterable[tuple[ServiceKey, ServiceVersion]], +) -> dict[tuple[ServiceKey, ServiceVersion], ServiceMetadata]: + keys_and_versions = list(keys_and_versions) + if not keys_and_versions: + return {} + + query = select( + services_meta_data.c.key, + services_meta_data.c.version, + services_meta_data.c.thumbnail, + ).where( + tuple_(services_meta_data.c.key, services_meta_data.c.version).in_( + keys_and_versions + ) + ) + + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + result = await conn.execute(query) + rows = result.fetchall() + + return { + (row.key, row.version): ServiceMetadata.model_validate(row) for row in rows + } + + +async def get_service_metadata( + app: web.Application, + connection: AsyncConnection | None = None, + *, + key: ServiceKey, + version: ServiceVersion, +) -> ServiceMetadata: + query = select( + services_meta_data.c.key, + services_meta_data.c.version, + services_meta_data.c.thumbnail, + ).where( + tuple_(services_meta_data.c.key, services_meta_data.c.version) == (key, version) + ) + + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + result = await conn.execute(query) + row = result.one_or_none() + if row is None: + raise ServiceMetadataNotFoundError(key=key, version=version) + + return ServiceMetadata.model_validate(row) diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py new file mode 100644 index 000000000000..3c91989a28a4 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py @@ -0,0 +1,26 @@ +from collections.abc import Iterable + +from aiohttp import web +from models_library.services_types import ServiceKey, ServiceVersion + +from . import _repository +from ._models import ServiceMetadata + + +async def batch_get_service_metadata( + app: web.Application, + *, + keys_and_versions: Iterable[tuple[ServiceKey, ServiceVersion]], +) -> dict[tuple[ServiceKey, ServiceVersion], ServiceMetadata]: + return await _repository.batch_service_metadata( + app, keys_and_versions=keys_and_versions + ) + + +async def get_service_metadata( + app: web.Application, + *, + key: ServiceKey, + version: ServiceVersion, +) -> ServiceMetadata: + return await _repository.get_service_metadata(app, key=key, version=version) diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py new file mode 100644 index 000000000000..e3eee53403ab --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py @@ -0,0 +1,9 @@ +from ._models import ServiceMetadata +from ._service import batch_get_service_metadata, get_service_metadata + +__all__: tuple[str, ...] = ( + "ServiceMetadata", + "batch_get_service_metadata", + "get_service_metadata", +) +# nopycln: file diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py index d368a04b8dc4..af10ce941b1c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py @@ -1,5 +1,5 @@ import logging -from collections.abc import Callable +from collections.abc import Callable, Iterable from datetime import datetime from typing import cast @@ -155,10 +155,10 @@ async def batch_get_projects( app: web.Application, connection: AsyncConnection | None = None, *, - project_uuids: list[ProjectID], -) -> list[ProjectDBGet]: + project_uuids: Iterable[ProjectID], +) -> dict[ProjectID, ProjectDBGet]: if not project_uuids: - return [] + return {} async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: query = ( sql.select(projects) @@ -166,7 +166,7 @@ async def batch_get_projects( .where(projects.c.uuid.in_([f"{uuid}" for uuid in project_uuids])) ) result = await conn.stream(query) - return [ProjectDBGet.model_validate(row) async for row in result] + return {row.uuid: ProjectDBGet.model_validate(row) async for row in result} def _select_trashed_by_primary_gid_query() -> sql.Select: diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index e6079e31fe03..34eadac8746d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -12,7 +12,7 @@ import datetime import logging from collections import defaultdict -from collections.abc import Generator +from collections.abc import Generator, Iterable from contextlib import suppress from decimal import Decimal from pprint import pformat @@ -275,8 +275,8 @@ async def batch_get_project_name( async def batch_get_projects( app: web.Application, *, - project_uuids: list[ProjectID], -) -> list[ProjectDBGet]: + project_uuids: Iterable[ProjectID], +) -> dict[ProjectID, ProjectDBGet]: return await _projects_repository.batch_get_projects( app=app, project_uuids=project_uuids, From 4500527ff251bc6eb8e2b57da9a0e08974a8da1d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 24 Jul 2025 15:51:28 +0200 Subject: [PATCH 03/11] fix: access rights --- .../functions/_controller/_functions_rest.py | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index ee3630bdf0af..1788b9e75b83 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -10,6 +10,7 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import ( + FunctionAccessRights, FunctionClass, FunctionID, RegisteredProjectFunction, @@ -49,12 +50,12 @@ routes = web.RouteTableDef() -async def _build_function_access_rights_dict( +async def _build_function_access_rights( app: web.Application, user_id: UserID, product_name: ProductName, function_id: FunctionID, -) -> dict[str, Any]: +) -> FunctionAccessRights: access_rights = await _functions_service.get_function_user_permissions( app=app, user_id=user_id, @@ -62,9 +63,11 @@ async def _build_function_access_rights_dict( function_id=function_id, ) - return { - "access_rights": access_rights.model_dump(), - } + return FunctionAccessRights( + read=access_rights.read, + write=access_rights.write, + execute=access_rights.execute, + ) async def _build_project_function_extras_dict( @@ -84,6 +87,23 @@ async def _build_project_function_extras_dict( } +async def _build_solver_function_extras_dict( + app: web.Application, + *, + function: RegisteredSolverFunction, +) -> dict[str, Any]: + services_metadata = await _services_metadata_service.get_service_metadata( + app, + key=function.solver_key, + version=function.solver_version, + ) + return { + "thumbnail": ( + f"{services_metadata.thumbnail}" if services_metadata.thumbnail else None + ), + } + + async def _build_function_extras( app: web.Application, user_id: UserID, *, function: RegisteredFunction ) -> dict[str, Any]: @@ -104,23 +124,6 @@ async def _build_function_extras( return extras -async def _build_solver_function_extras_dict( - app: web.Application, - *, - function: RegisteredSolverFunction, -) -> dict[str, Any]: - services_metadata = await _services_metadata_service.get_service_metadata( - app, - key=function.solver_key, - version=function.solver_version, - ) - return { - "thumbnail": ( - f"{services_metadata.thumbnail}" if services_metadata.thumbnail else None - ), - } - - @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -188,7 +191,7 @@ async def list_functions(request: web.Request) -> web.Response: for function in functions if function.function_class == FunctionClass.PROJECT ] - projects_cache = await _projects_service.batch_get_projects( + projects_cache |= await _projects_service.batch_get_projects( request.app, project_uuids=project_uuids, ) @@ -200,14 +203,14 @@ async def list_functions(request: web.Request) -> web.Response: for function in functions if function.function_class == FunctionClass.SOLVER ] - service_metadata_cache = ( + service_metadata_cache |= ( await _services_metadata_service.batch_get_service_metadata( app=request.app, keys_and_versions=service_keys_and_versions ) ) for function in functions: - access_rights = await _build_function_access_rights_dict( + access_rights = await _build_function_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -239,7 +242,7 @@ async def list_functions(request: web.Request) -> web.Response: chunk.append( TypeAdapter(RegisteredFunctionGet).validate_python( - function.model_dump() | access_rights | extras + function.model_dump() | {"access_rights": access_rights, **extras} ) ) @@ -278,7 +281,7 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - access_rights = await _build_function_access_rights_dict( + access_rights = await _build_function_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -293,7 +296,7 @@ async def get_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( - function.model_dump() | access_rights | extras + function.model_dump() | {"access_rights": access_rights, **extras} ) ) @@ -326,7 +329,7 @@ async def update_function(request: web.Request) -> web.Response: function=function_update, ) - access_rights = await _build_function_access_rights_dict( + access_rights = await _build_function_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -341,7 +344,7 @@ async def update_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( - function.model_dump() | access_rights | extras + function.model_dump() | {"access_rights": access_rights, **extras} ) ) From e0173c44acedc929658e09e9891f9371682762e6 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 24 Jul 2025 22:13:40 +0200 Subject: [PATCH 04/11] fix: thumbnail --- .../api_schemas_webserver/functions.py | 6 +-- .../functions/_controller/_functions_rest.py | 39 +++++++------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index 7ba4fd523a06..a52966a8d181 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -1,7 +1,7 @@ import datetime from typing import Annotated, TypeAlias -from pydantic import Field +from pydantic import Field, HttpUrl from ..functions import ( Function, @@ -119,7 +119,7 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] access_rights: FunctionAccessRights | None = None - thumbnail: str | None = None + thumbnail: HttpUrl | None = None class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): @@ -128,7 +128,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] access_rights: FunctionAccessRights | None = None - thumbnail: str | None = None + thumbnail: HttpUrl | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 1788b9e75b83..7d38b0c5629c 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -1,3 +1,4 @@ +import logging from typing import Any from aiohttp import web @@ -50,6 +51,9 @@ routes = web.RouteTableDef() +logger = logging.getLogger(__name__) + + async def _build_function_access_rights( app: web.Application, user_id: UserID, @@ -83,7 +87,7 @@ async def _build_project_function_extras_dict( ) return { - "thumbnail": project_dict.get("thumbnail", None), + "thumbnail": project_dict.get("thumbnail") or None, } @@ -198,11 +202,11 @@ async def list_functions(request: web.Request) -> web.Response: if any( function.function_class == FunctionClass.SOLVER for function in functions ): - service_keys_and_versions = [ + service_keys_and_versions = { (function.solver_key, function.solver_version) for function in functions if function.function_class == FunctionClass.SOLVER - ] + } service_metadata_cache |= ( await _services_metadata_service.batch_get_service_metadata( app=request.app, keys_and_versions=service_keys_and_versions @@ -217,28 +221,13 @@ async def list_functions(request: web.Request) -> web.Response: function_id=function.uid, ) - extras: dict[str, Any] = {} - if query_params.include_extras: - if function.function_class == FunctionClass.PROJECT: - assert isinstance(function, RegisteredProjectFunction) # nosec - if project := projects_cache.get(function.project_id): - extras = { - "thumbnail": ( - f"{project.thumbnail}" if project.thumbnail else None - ), - } - elif function.function_class == FunctionClass.SOLVER: - assert isinstance(function, RegisteredSolverFunction) - if service_metadata := service_metadata_cache.get( - (function.solver_key, function.solver_version) - ): - extras = { - "thumbnail": ( - f"{service_metadata.thumbnail}" - if service_metadata.thumbnail - else None - ), - } + extras = ( + await _build_function_extras( + request.app, req_ctx.user_id, function=function + ) + if query_params.include_extras + else {} + ) chunk.append( TypeAdapter(RegisteredFunctionGet).validate_python( From 8b75bf66768ed52f1ff9fd1c424d5b7b2bbfe136 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 24 Jul 2025 22:28:04 +0200 Subject: [PATCH 05/11] fix: rename --- .../functions/_controller/_functions_rest.py | 8 ++++---- .../_services_metadata/{_service.py => _proxy.py} | 0 .../functions/_services_metadata/{service.py => proxy.py} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename services/web/server/src/simcore_service_webserver/functions/_services_metadata/{_service.py => _proxy.py} (100%) rename services/web/server/src/simcore_service_webserver/functions/_services_metadata/{service.py => proxy.py} (70%) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 7d38b0c5629c..07c89cb8d6c3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -39,8 +39,8 @@ from ...security.decorators import permission_required from ...utils_aiohttp import create_json_response_from_page, envelope_json_response from .. import _functions_service -from .._services_metadata import service as _services_metadata_service -from .._services_metadata.service import ServiceMetadata +from .._services_metadata import proxy as _services_metadata_proxy +from .._services_metadata.proxy import ServiceMetadata from ._functions_rest_exceptions import handle_rest_requests_exceptions from ._functions_rest_schemas import ( FunctionGetQueryParams, @@ -96,7 +96,7 @@ async def _build_solver_function_extras_dict( *, function: RegisteredSolverFunction, ) -> dict[str, Any]: - services_metadata = await _services_metadata_service.get_service_metadata( + services_metadata = await _services_metadata_proxy.get_service_metadata( app, key=function.solver_key, version=function.solver_version, @@ -208,7 +208,7 @@ async def list_functions(request: web.Request) -> web.Response: if function.function_class == FunctionClass.SOLVER } service_metadata_cache |= ( - await _services_metadata_service.batch_get_service_metadata( + await _services_metadata_proxy.batch_get_service_metadata( app=request.app, keys_and_versions=service_keys_and_versions ) ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_proxy.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/functions/_services_metadata/_service.py rename to services/web/server/src/simcore_service_webserver/functions/_services_metadata/_proxy.py diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/proxy.py similarity index 70% rename from services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py rename to services/web/server/src/simcore_service_webserver/functions/_services_metadata/proxy.py index e3eee53403ab..3556c7c36ffe 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/proxy.py @@ -1,5 +1,5 @@ from ._models import ServiceMetadata -from ._service import batch_get_service_metadata, get_service_metadata +from ._proxy import batch_get_service_metadata, get_service_metadata __all__: tuple[str, ...] = ( "ServiceMetadata", From 8207d9ef42613f4502717565742807f3011cc53a Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 25 Jul 2025 10:08:45 +0200 Subject: [PATCH 06/11] fix: get service metadata from_attributes --- .../functions/_services_metadata/_repository.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py index c5af84060c08..99610f93287b 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_services_metadata/_repository.py @@ -41,7 +41,10 @@ async def batch_service_metadata( rows = result.fetchall() return { - (row.key, row.version): ServiceMetadata.model_validate(row) for row in rows + (row.key, row.version): ServiceMetadata.model_validate( + row, from_attributes=True + ) + for row in rows } @@ -66,4 +69,4 @@ async def get_service_metadata( if row is None: raise ServiceMetadataNotFoundError(key=key, version=version) - return ServiceMetadata.model_validate(row) + return ServiceMetadata.model_validate(row, from_attributes=True) From 6f6a4a736f8574aafa3e457b2ff1b7e984e2c1f5 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 25 Jul 2025 11:26:14 +0200 Subject: [PATCH 07/11] tests: add solver function case --- .../test_functions_controller_rest.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py index 24e412431ebd..b6410071111a 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py @@ -15,7 +15,7 @@ FunctionClass, JSONFunctionInputSchema, JSONFunctionOutputSchema, - RegisteredProjectFunctionGet, + RegisteredFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from pydantic import TypeAdapter @@ -27,11 +27,11 @@ pytest_simcore_core_services_selection = ["rabbit"] -@pytest.fixture -def mock_function() -> dict[str, Any]: - return { - "title": "Test Function", - "description": "A test function", +@pytest.fixture(params=[FunctionClass.PROJECT, FunctionClass.SOLVER]) +def mocked_function(request) -> dict[str, Any]: + function_dict = { + "title": f"Test {request.param} Function", + "description": f"A test {request.param} function", "inputSchema": JSONFunctionInputSchema( schema_content={ "type": "object", @@ -44,11 +44,19 @@ def mock_function() -> dict[str, Any]: "properties": {"output1": {"type": "string"}}, }, ).model_dump(mode="json"), - "projectId": str(uuid4()), - "functionClass": FunctionClass.PROJECT, + "functionClass": request.param, "defaultInputs": None, } + match request.param: + case FunctionClass.PROJECT: + function_dict["projectId"] = f"{uuid4()}" + case FunctionClass.SOLVER: + function_dict["solverKey"] = "simcore/services/dynamic/test" + function_dict["solverVersion"] = "1.0.0" + + return function_dict + @pytest.mark.parametrize( "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_update,expected_delete,expected_get2", @@ -79,7 +87,7 @@ def mock_function() -> dict[str, Any]: async def test_function_workflow( client: TestClient, logged_user: UserInfoDict, - mock_function: dict[str, Any], + mocked_function: dict[str, Any], expected_register: HTTPStatus, expected_get: HTTPStatus, expected_list: HTTPStatus, @@ -91,12 +99,12 @@ async def test_function_workflow( ) -> None: # Register a new function url = client.app.router["register_function"].url_for() - response = await client.post(url, json=mock_function) + response = await client.post(url, json=mocked_function) data, error = await assert_status(response, expected_status_code=expected_register) if error: returned_function_uid = uuid4() else: - returned_function = RegisteredProjectFunctionGet.model_validate(data) + returned_function = TypeAdapter(RegisteredFunctionGet).validate_python(data) assert returned_function.uid is not None returned_function_uid = returned_function.uid @@ -107,7 +115,7 @@ async def test_function_workflow( response = await client.get(url) data, error = await assert_status(response, expected_get) if not error: - retrieved_function = RegisteredProjectFunctionGet.model_validate(data) + retrieved_function = TypeAdapter(RegisteredFunctionGet).validate_python(data) assert retrieved_function.uid == returned_function.uid # List existing functions @@ -115,9 +123,9 @@ async def test_function_workflow( response = await client.get(url) data, error = await assert_status(response, expected_list) if not error: - retrieved_functions = TypeAdapter( - list[RegisteredProjectFunctionGet] - ).validate_python(data) + retrieved_functions = TypeAdapter(list[RegisteredFunctionGet]).validate_python( + data + ) assert len(retrieved_functions) == 1 assert retrieved_functions[0].uid == returned_function_uid @@ -132,7 +140,7 @@ async def test_function_workflow( ) data, error = await assert_status(response, expected_update) if not error: - updated_function = RegisteredProjectFunctionGet.model_validate(data) + updated_function = TypeAdapter(RegisteredFunctionGet).validate_python(data) assert updated_function.title == new_title assert updated_function.description == new_description From 39ead96a7917896387f9b9f368c71a038f26f34c Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 25 Jul 2025 12:13:02 +0200 Subject: [PATCH 08/11] fix: remove unused logger --- .../functions/_controller/_functions_rest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 07c89cb8d6c3..9cd78acd6666 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -1,4 +1,3 @@ -import logging from typing import Any from aiohttp import web @@ -51,9 +50,6 @@ routes = web.RouteTableDef() -logger = logging.getLogger(__name__) - - async def _build_function_access_rights( app: web.Application, user_id: UserID, From c3d92521177b4055c4c2639a4a90c19ba9d36231 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 28 Jul 2025 08:54:39 +0200 Subject: [PATCH 09/11] fix: thumbnail --- .../functions/_controller/_functions_rest.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 9cd78acd6666..c0d674475715 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -82,9 +82,10 @@ async def _build_project_function_extras_dict( user_id=user_id, ) - return { - "thumbnail": project_dict.get("thumbnail") or None, - } + extras: dict[str, Any] = {} + if thumbnail := project_dict["thumbnail"]: + extras["thumbnail"] = thumbnail + return extras async def _build_solver_function_extras_dict( @@ -97,30 +98,30 @@ async def _build_solver_function_extras_dict( key=function.solver_key, version=function.solver_version, ) - return { - "thumbnail": ( - f"{services_metadata.thumbnail}" if services_metadata.thumbnail else None - ), - } + extras: dict[str, Any] = {} + if thumbnail := services_metadata.thumbnail: + extras["thumbnail"] = thumbnail + return extras async def _build_function_extras( app: web.Application, user_id: UserID, *, function: RegisteredFunction ) -> dict[str, Any]: extras: dict[str, Any] = {} - if function.function_class == FunctionClass.PROJECT: - assert isinstance(function, RegisteredProjectFunction) - extras |= await _build_project_function_extras_dict( - function=function, - app=app, - user_id=user_id, - ) - elif function.function_class == FunctionClass.SOLVER: - assert isinstance(function, RegisteredSolverFunction) - extras |= await _build_solver_function_extras_dict( - app, - function=function, - ) + match function.function_class: + case FunctionClass.PROJECT: + assert isinstance(function, RegisteredProjectFunction) + extras |= await _build_project_function_extras_dict( + function=function, + app=app, + user_id=user_id, + ) + case FunctionClass.SOLVER: + assert isinstance(function, RegisteredSolverFunction) + extras |= await _build_solver_function_extras_dict( + app, + function=function, + ) return extras From e603d0a54813d9aa0a800b9f90c21122981c3946 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 28 Jul 2025 11:36:06 +0200 Subject: [PATCH 10/11] fix: use caching --- .../functions/_controller/_functions_rest.py | 90 +++++++++---------- .../projects/_projects_repository.py | 5 +- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index c0d674475715..54363cbdb129 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -17,10 +17,8 @@ RegisteredSolverFunction, ) from models_library.products import ProductName -from models_library.projects import ProjectID from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data -from models_library.services_types import ServiceKey, ServiceVersion from models_library.users import UserID from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -70,57 +68,49 @@ async def _build_function_access_rights( ) -async def _build_project_function_extras_dict( - app: web.Application, - *, - user_id: UserID, - function: RegisteredProjectFunction, +def _build_project_function_extras_dict( + project: ProjectDBGet, ) -> dict[str, Any]: - project_dict = await _projects_service.get_project_for_user( - app=app, - project_uuid=f"{function.project_id}", - user_id=user_id, - ) - extras: dict[str, Any] = {} - if thumbnail := project_dict["thumbnail"]: + if thumbnail := project.thumbnail: extras["thumbnail"] = thumbnail return extras -async def _build_solver_function_extras_dict( - app: web.Application, - *, - function: RegisteredSolverFunction, +def _build_solver_function_extras_dict( + service_metadata: ServiceMetadata, ) -> dict[str, Any]: - services_metadata = await _services_metadata_proxy.get_service_metadata( - app, - key=function.solver_key, - version=function.solver_version, - ) + extras: dict[str, Any] = {} - if thumbnail := services_metadata.thumbnail: + if thumbnail := service_metadata.thumbnail: extras["thumbnail"] = thumbnail return extras async def _build_function_extras( - app: web.Application, user_id: UserID, *, function: RegisteredFunction + app: web.Application, *, function: RegisteredFunction ) -> dict[str, Any]: extras: dict[str, Any] = {} match function.function_class: case FunctionClass.PROJECT: assert isinstance(function, RegisteredProjectFunction) - extras |= await _build_project_function_extras_dict( - function=function, + projects = await _projects_service.batch_get_projects( app=app, - user_id=user_id, + project_uuids=[function.project_id], ) + if project := projects.get(function.project_id): + extras |= _build_project_function_extras_dict( + project=project, + ) case FunctionClass.SOLVER: assert isinstance(function, RegisteredSolverFunction) - extras |= await _build_solver_function_extras_dict( + services_metadata = await _services_metadata_proxy.get_service_metadata( app, - function=function, + key=function.solver_key, + version=function.solver_version, + ) + extras |= _build_solver_function_extras_dict( + service_metadata=services_metadata, ) return extras @@ -178,10 +168,7 @@ async def list_functions(request: web.Request) -> web.Response: chunk: list[RegisteredFunctionGet] = [] - projects_cache: dict[ProjectID, ProjectDBGet] = {} - service_metadata_cache: dict[tuple[ServiceKey, ServiceVersion], ServiceMetadata] = ( - {} - ) + extras_map: dict[FunctionID, dict[str, Any]] = {} if query_params.include_extras: if any( @@ -192,10 +179,19 @@ async def list_functions(request: web.Request) -> web.Response: for function in functions if function.function_class == FunctionClass.PROJECT ] - projects_cache |= await _projects_service.batch_get_projects( + projects_cache = await _projects_service.batch_get_projects( request.app, project_uuids=project_uuids, ) + for function in functions: + if function.function_class == FunctionClass.PROJECT: + project = projects_cache.get(function.project_id) + if not project: + continue + extras_map[function.uid] = _build_project_function_extras_dict( + project=project + ) + if any( function.function_class == FunctionClass.SOLVER for function in functions ): @@ -204,11 +200,21 @@ async def list_functions(request: web.Request) -> web.Response: for function in functions if function.function_class == FunctionClass.SOLVER } - service_metadata_cache |= ( + service_metadata_cache = ( await _services_metadata_proxy.batch_get_service_metadata( app=request.app, keys_and_versions=service_keys_and_versions ) ) + for function in functions: + if function.function_class == FunctionClass.SOLVER: + service_metadata = service_metadata_cache.get( + (function.solver_key, function.solver_version) + ) + if not service_metadata: + continue + extras_map[function.uid] = _build_solver_function_extras_dict( + service_metadata=service_metadata + ) for function in functions: access_rights = await _build_function_access_rights( @@ -218,13 +224,7 @@ async def list_functions(request: web.Request) -> web.Response: function_id=function.uid, ) - extras = ( - await _build_function_extras( - request.app, req_ctx.user_id, function=function - ) - if query_params.include_extras - else {} - ) + extras = extras_map.get(function.uid, {}) chunk.append( TypeAdapter(RegisteredFunctionGet).validate_python( @@ -275,7 +275,7 @@ async def get_function(request: web.Request) -> web.Response: ) extras = ( - await _build_function_extras(request.app, req_ctx.user_id, function=function) + await _build_function_extras(request.app, function=function) if query_params.include_extras else {} ) @@ -323,7 +323,7 @@ async def update_function(request: web.Request) -> web.Response: ) extras = ( - await _build_function_extras(request.app, req_ctx.user_id, function=function) + await _build_function_extras(request.app, function=function) if query_params.include_extras else {} ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py index 0c62adfbc1d1..83c357c36570 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_repository.py @@ -183,7 +183,10 @@ async def batch_get_projects( .where(projects.c.uuid.in_([f"{uuid}" for uuid in project_uuids])) ) result = await conn.stream(query) - return {row.uuid: ProjectDBGet.model_validate(row) async for row in result} + return { + ProjectID(row.uuid): ProjectDBGet.model_validate(row) + async for row in result + } def _select_trashed_by_primary_gid_query() -> sql.Select: From d3e0c1cf92ae651c27a2691da1fafd85f95e7a34 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 28 Jul 2025 12:39:56 +0200 Subject: [PATCH 11/11] fix: openapi-spec --- .../api/v0/openapi.yaml | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index fc91d1d393f5..a8b1de9be4de 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -16078,6 +16078,9 @@ components: thumbnail: anyOf: - type: string + maxLength: 2083 + minLength: 1 + format: uri - type: 'null' title: Thumbnail type: object @@ -16131,14 +16134,14 @@ components: type: string format: uuid title: Uuid - createdAt: + creationDate: type: string format: date-time - title: Createdat - modifiedAt: + title: Creationdate + lastChangeDate: type: string format: date-time - title: Modifiedat + title: Lastchangedate solverKey: type: string pattern: ^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$ @@ -16147,14 +16150,26 @@ components: type: string pattern: ^(0|[1-9]\d*)(\.(0|[1-9]\d*)){2}(-(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*)(\.(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*))*)?(\+[-\da-zA-Z]+(\.[-\da-zA-Z-]+)*)?$ title: Solverversion + accessRights: + anyOf: + - $ref: '#/components/schemas/FunctionAccessRights' + - type: 'null' + thumbnail: + anyOf: + - type: string + maxLength: 2083 + minLength: 1 + format: uri + - type: 'null' + title: Thumbnail type: object required: - inputSchema - outputSchema - defaultInputs - uuid - - createdAt - - modifiedAt + - creationDate + - lastChangeDate - solverKey - solverVersion title: RegisteredSolverFunctionGet