From 19bea5189893ef7b41ce90b8775978daba338c38 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 16 Jul 2025 14:20:29 +0200 Subject: [PATCH 01/43] feat: add readFunctions when getting permissions --- .../api_schemas_webserver/users.py | 1 + .../functions/_controller/_functions_rest.py | 5 ++++- .../unit/with_dbs/04/functions_rpc/conftest.py | 3 ++- .../test_functions_controller_rest.py | 16 +++++++++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/users.py b/packages/models-library/src/models_library/api_schemas_webserver/users.py index b81d13ed086..3c9aa3dd9e9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/users.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/users.py @@ -407,4 +407,5 @@ def from_domain_model(cls, permission: UserPermission) -> Self: class MyFunctionPermissionsGet(OutputSchema): + read_functions: bool write_functions: bool 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 9d263d58136..4d5be7210ef 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 @@ -125,5 +125,8 @@ async def list_user_functions_permissions(request: web.Request) -> web.Response: assert function_permissions.user_id == req_ctx.user_id # nosec return envelope_json_response( - MyFunctionPermissionsGet(write_functions=function_permissions.write_functions) + MyFunctionPermissionsGet( + read_functions=function_permissions.read_functions, + write_functions=function_permissions.write_functions, + ) ) diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py index 3649ba419a2..31d4939417a 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py @@ -200,6 +200,7 @@ async def logged_user_function_api_access_rights( asyncpg_engine: AsyncEngine, logged_user: UserInfoDict, *, + expected_read_functions: bool, expected_write_functions: bool, ) -> AsyncIterator[dict[str, Any]]: cm = insert_and_get_row_lifespan( @@ -208,7 +209,7 @@ async def logged_user_function_api_access_rights( values={ "group_id": logged_user["primary_gid"], "product_name": FRONTEND_APP_DEFAULT, - "read_functions": True, + "read_functions": expected_read_functions, "write_functions": expected_write_functions, "execute_functions": True, "read_function_jobs": True, 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 68bc2837355..77f26b9a1cb 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 @@ -115,13 +115,26 @@ async def test_register_get_delete_function( @pytest.mark.parametrize("user_role", [UserRole.USER]) -@pytest.mark.parametrize("expected_write_functions", [True, False]) +@pytest.mark.parametrize( + "expected_read_functions,expected_write_functions", + [ + (True, True), + (True, False), + (False, True), # Weird, but allowed for testing purposes + (False, False), + ], +) async def test_list_user_functions_permissions( client: TestClient, logged_user: UserInfoDict, + expected_read_functions: bool, expected_write_functions: bool, logged_user_function_api_access_rights: dict[str, Any], ): + assert ( + logged_user_function_api_access_rights["read_functions"] + == expected_read_functions + ) assert ( logged_user_function_api_access_rights["write_functions"] == expected_write_functions @@ -133,4 +146,5 @@ async def test_list_user_functions_permissions( assert not error function_permissions = MyFunctionPermissionsGet.model_validate(data) + assert function_permissions.read_functions == expected_read_functions assert function_permissions.write_functions == expected_write_functions From 298bdd96a46ed541533cc703783b0169058984d5 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 16 Jul 2025 14:45:20 +0200 Subject: [PATCH 02/43] fix: update openapi-spec --- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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 ce122ca58cc..35b0060c756 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 @@ -13023,11 +13023,15 @@ components: title: MarkerUI MyFunctionPermissionsGet: properties: + readFunctions: + type: boolean + title: Readfunctions writeFunctions: type: boolean title: Writefunctions type: object required: + - readFunctions - writeFunctions title: MyFunctionPermissionsGet MyGroupsGet: From fa40d712ade4d5962f4bbd76a771d57e0d6d5224 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 10:03:55 +0200 Subject: [PATCH 03/43] feat: add update endpoint --- .../functions/_controller/_functions_rest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 4d5be7210ef..6d8ab6ef4cc 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,6 +82,22 @@ async def get_function(request: web.Request) -> web.Response: ) +@routes.put( + "/{VTAG}/functions/{function_id}", + name="update_function", +) +@login_required +@permission_required("function.write") +@handle_rest_requests_exceptions +async def update_function(request: web.Request) -> web.Response: + path_params = parse_request_path_parameters_as(FunctionPathParams, request) + function_id = path_params.function_id + + req_ctx = AuthenticatedRequestContext.model_validate(request) + + raise NotImplementedError + + @routes.delete( f"/{VTAG}/functions/{{function_id}}", name="delete_function", From 9af987e96e6f4d8113cfc05c5877389b5384f8c9 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 11:22:20 +0200 Subject: [PATCH 04/43] feat: add update functions endpoint --- .../api_schemas_webserver/functions.py | 4 ++ .../src/models_library/functions.py | 5 ++ .../functions/_controller/_functions_rest.py | 20 +++++++- .../functions/_controller/_functions_rpc.py | 9 ++-- .../functions/_functions_repository.py | 47 ++----------------- .../functions/_functions_service.py | 27 ++--------- 6 files changed, 42 insertions(+), 70 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 c1f4a7b55e5..f8d022d330b 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 @@ -23,6 +23,7 @@ FunctionOutputs, FunctionOutputSchema, FunctionSchemaClass, + FunctionUpdate, JSONFunctionInputSchema, JSONFunctionOutputSchema, ProjectFunction, @@ -131,3 +132,6 @@ class ProjectFunctionToRegister(ProjectFunction, InputSchema): ... RegisteredProjectFunctionGet | RegisteredSolverFunctionGet, Field(discriminator="function_class"), ] + + +class RegisteredFunctionUpdate(FunctionUpdate, InputSchema): ... diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index 4ab6fe389b4..966d5a9b974 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -101,6 +101,11 @@ class RegisteredFunctionBase(FunctionBase): created_at: datetime.datetime +class FunctionUpdate(BaseModel): + title: str | None = None + description: str | None = None + + class ProjectFunction(FunctionBase): function_class: Literal[FunctionClass.PROJECT] = FunctionClass.PROJECT project_id: ProjectID 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 6d8ab6ef4cc..7bd8ba821e3 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 @@ -4,6 +4,7 @@ FunctionToRegister, RegisteredFunction, RegisteredFunctionGet, + RegisteredFunctionUpdate, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from pydantic import TypeAdapter @@ -82,7 +83,7 @@ async def get_function(request: web.Request) -> web.Response: ) -@routes.put( +@routes.patch( "/{VTAG}/functions/{function_id}", name="update_function", ) @@ -93,9 +94,24 @@ async def update_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) function_id = path_params.function_id + function_update = TypeAdapter(RegisteredFunctionUpdate).validate_python( + await request.json() + ) req_ctx = AuthenticatedRequestContext.model_validate(request) - raise NotImplementedError + updated_function = await _functions_service.update_function( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + function=function_update, + ) + + return envelope_json_response( + TypeAdapter(RegisteredFunctionUpdate).validate_python( + updated_function.model_dump(mode="json") + ) + ) @routes.delete( diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py index f77b0701115..470c532d03e 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py @@ -11,6 +11,7 @@ FunctionJobCollectionsListFilters, FunctionJobID, FunctionOutputSchema, + FunctionUpdate, FunctionUserApiAccessRights, RegisteredFunction, RegisteredFunctionJob, @@ -318,12 +319,12 @@ async def update_function_title( function_id: FunctionID, title: str, ) -> RegisteredFunction: - return await _functions_service.update_function_title( + return await _functions_service.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - title=title, + function=FunctionUpdate(title=title), ) @@ -342,12 +343,12 @@ async def update_function_description( function_id: FunctionID, description: str, ) -> RegisteredFunction: - return await _functions_service.update_function_description( + return await _functions_service.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - description=description, + function=FunctionUpdate(description=description), ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py index cec64473248..65fc448f1e3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py @@ -20,6 +20,7 @@ FunctionOutputs, FunctionOutputSchema, FunctionsApiAccessRights, + FunctionUpdate, FunctionUserApiAccessRights, RegisteredFunctionDB, RegisteredFunctionJobCollectionDB, @@ -633,14 +634,14 @@ async def delete_function( ) -async def _update_function_attribute( +async def update_function( app: web.Application, - connection: AsyncConnection | None, + connection: AsyncConnection | None = None, *, user_id: UserID, product_name: ProductName, function_id: FunctionID, - **update_kwargs, + function: FunctionUpdate, ) -> RegisteredFunctionDB: async with transaction_context(get_asyncpg_engine(app), connection) as transaction: await check_user_api_access_rights( @@ -667,7 +668,7 @@ async def _update_function_attribute( result = await transaction.execute( functions_table.update() .where(functions_table.c.uuid == function_id) - .values(**update_kwargs) + .values(**function.model_dump(exclude_none=True, exclude_unset=True)) .returning(*_FUNCTIONS_TABLE_COLS) ) row = result.one_or_none() @@ -678,44 +679,6 @@ async def _update_function_attribute( return RegisteredFunctionDB.model_validate(row) -async def update_function_title( - app: web.Application, - connection: AsyncConnection | None = None, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - title: str, -) -> RegisteredFunctionDB: - return await _update_function_attribute( - app, - connection=connection, - user_id=user_id, - product_name=product_name, - function_id=function_id, - title=title, - ) - - -async def update_function_description( - app: web.Application, - connection: AsyncConnection | None = None, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - description: str, -) -> RegisteredFunctionDB: - return await _update_function_attribute( - app, - connection=connection, - user_id=user_id, - product_name=product_name, - function_id=function_id, - description=description, - ) - - async def get_function_job( app: web.Application, connection: AsyncConnection | None = None, diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py index d74dbe52217..75ba208ab8f 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py @@ -14,6 +14,7 @@ FunctionJobDB, FunctionJobID, FunctionOutputSchema, + FunctionUpdate, FunctionUserAccessRights, FunctionUserApiAccessRights, RegisteredFunction, @@ -291,38 +292,20 @@ async def delete_function_job_collection( ) -async def update_function_title( +async def update_function( app: web.Application, *, user_id: UserID, product_name: ProductName, function_id: FunctionID, - title: str, + function: FunctionUpdate, ) -> RegisteredFunction: - updated_function = await _functions_repository.update_function_title( + updated_function = await _functions_repository.update_function( app=app, user_id=user_id, product_name=product_name, function_id=function_id, - title=title, - ) - return _decode_function(updated_function) - - -async def update_function_description( - app: web.Application, - *, - user_id: UserID, - product_name: ProductName, - function_id: FunctionID, - description: str, -) -> RegisteredFunction: - updated_function = await _functions_repository.update_function_description( - app=app, - user_id=user_id, - product_name=product_name, - function_id=function_id, - description=description, + function=function, ) return _decode_function(updated_function) From ee89ede23f14e5308ef33555e4df700c64ced6f5 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 11:31:01 +0200 Subject: [PATCH 05/43] fix: update openapi-spec --- api/specs/web-server/_functions.py | 11 +++++ .../api/v0/openapi.yaml | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index 72f4c8af48e..e10f725762f 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -11,6 +11,7 @@ from models_library.api_schemas_webserver.functions import ( FunctionToRegister, RegisteredFunctionGet, + RegisteredFunctionUpdate, ) from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG @@ -44,6 +45,16 @@ async def get_function( ): ... +@router.patch( + "/functions/{function_id}", + response_model=Envelope[RegisteredFunctionGet], +) +async def update_function( + _body: RegisteredFunctionUpdate, + _path: Annotated[FunctionPathParams, Depends()], +): ... + + @router.delete( "/functions/{function_id}", status_code=status.HTTP_204_NO_CONTENT, 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 35b0060c756..225baa71107 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 @@ -3395,6 +3395,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' + patch: + tags: + - functions + summary: Update Function + operationId: update_function + parameters: + - name: function_id + in: path + required: true + schema: + type: string + format: uuid + title: Function Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisteredFunctionUpdate' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' delete: tags: - functions @@ -15978,6 +16004,20 @@ components: - name - message title: RegisterPhoneNextPage + RegisteredFunctionUpdate: + properties: + title: + anyOf: + - type: string + - type: 'null' + title: Title + description: + anyOf: + - type: string + - type: 'null' + title: Description + type: object + title: RegisteredFunctionUpdate RegisteredProjectFunctionGet: properties: functionClass: From 80535cc879ce0f47d9eb1e2aca05b97d2cf8aa20 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 13:20:09 +0200 Subject: [PATCH 06/43] fix: path --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7bd8ba821e3..e033777501a 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 @@ -84,7 +84,7 @@ async def get_function(request: web.Request) -> web.Response: @routes.patch( - "/{VTAG}/functions/{function_id}", + f"/{VTAG}/functions/{{function_id}}", name="update_function", ) @login_required From a5f70663b63e4eb15de85aa1a0bff580ec9f9d18 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:23:57 +0200 Subject: [PATCH 07/43] feat: add batch get projects --- .../projects/_projects_repository.py | 20 ++++++++++++++++++- .../projects/_projects_service.py | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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 705e4970ee7..98a3c179569 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 @@ -8,7 +8,7 @@ from common_library.exclude import Unset, is_set from models_library.basic_types import IDStr from models_library.groups import GroupID -from models_library.projects import ProjectID +from models_library.projects import Project, ProjectID from models_library.rest_ordering import OrderBy, OrderDirection from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from models_library.workspaces import WorkspaceID @@ -151,6 +151,24 @@ async def batch_get_project_name( return [rows.get(project_uuid) for project_uuid in projects_uuids_str] +async def batch_get_projects( + app: web.Application, + connection: AsyncConnection | None = None, + *, + project_uuids: list[ProjectID], +) -> list[Project]: + if not project_uuids: + return [] + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + query = ( + sql.select(projects) + .select_from(projects) + .where(projects.c.uuid.in_([f"{uuid}" for uuid in project_uuids])) + ) + result = await conn.stream(query) + return [Project.model_validate(row) async for row in result] + + def _select_trashed_by_primary_gid_query() -> sql.Select: return sql.select( projects.c.uuid, 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 6152340916d..716e94294e7 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 @@ -275,6 +275,15 @@ async def batch_get_project_name( return [name if name else "Unknown" for name in get_project_names] +async def batch_get_projects( + app: web.Application, project_uuids: list[ProjectID] +) -> list[Project]: + return await _projects_repository.batch_get_projects( + app=app, + project_uuids=project_uuids, + ) + + # # UPDATE project ----------------------------------------------------- # From 2d87133ed7d7b7c1c4a4583fdef73c044b9b9dd6 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:32:50 +0200 Subject: [PATCH 08/43] fix: use new models --- .../projects/_projects_repository.py | 6 +++--- .../simcore_service_webserver/projects/_projects_service.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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 98a3c179569..d368a04b8dc 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 @@ -8,7 +8,7 @@ from common_library.exclude import Unset, is_set from models_library.basic_types import IDStr from models_library.groups import GroupID -from models_library.projects import Project, ProjectID +from models_library.projects import ProjectID from models_library.rest_ordering import OrderBy, OrderDirection from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from models_library.workspaces import WorkspaceID @@ -156,7 +156,7 @@ async def batch_get_projects( connection: AsyncConnection | None = None, *, project_uuids: list[ProjectID], -) -> list[Project]: +) -> list[ProjectDBGet]: if not project_uuids: return [] async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: @@ -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 [Project.model_validate(row) async for row in result] + return [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 716e94294e7..621f7554549 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 @@ -153,7 +153,7 @@ ProjectTooManyProjectOpenedError, ProjectTypeAndTemplateIncompatibilityError, ) -from .models import ProjectDict, ProjectPatchInternalExtended +from .models import ProjectDBGet, ProjectDict, ProjectPatchInternalExtended from .settings import ProjectsSettings, get_plugin_settings from .utils import extract_dns_without_default_port @@ -277,7 +277,7 @@ async def batch_get_project_name( async def batch_get_projects( app: web.Application, project_uuids: list[ProjectID] -) -> list[Project]: +) -> list[ProjectDBGet]: return await _projects_repository.batch_get_projects( app=app, project_uuids=project_uuids, From 14be90359616346c418c094bbf37f70e66a933c2 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 16:34:47 +0200 Subject: [PATCH 09/43] feat: add list endpoint --- api/specs/web-server/_functions.py | 7 +++++++ .../functions/_controller/_functions_rest.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index e10f725762f..15ec04e5aaa 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -36,6 +36,13 @@ async def register_function( ) -> Envelope[RegisteredFunctionGet]: ... +@router.get( + "/functions", + response_model=Envelope[list[RegisteredFunctionGet]], +) +async def list_functions() -> Envelope[list[RegisteredFunctionGet]]: ... + + @router.get( "/functions/{function_id}", response_model=Envelope[RegisteredFunctionGet], 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 e033777501a..60968203a87 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 @@ -57,6 +57,17 @@ async def register_function(request: web.Request) -> web.Response: ) +@routes.get( + f"/{VTAG}/functions", + name="list_functions", +) +@login_required +@permission_required("function.read") +@handle_rest_requests_exceptions +async def list_functions(request: web.Request) -> web.Response: + raise NotImplementedError + + @routes.get( f"/{VTAG}/functions/{{function_id}}", name="get_function", @@ -76,6 +87,8 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) + # TODO: enrich with project data + return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( registered_function.model_dump(mode="json") From 6043c7d5c8a1536cac9b59e5b6829b65c1120956 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 23:40:49 +0200 Subject: [PATCH 10/43] feat: get function --- .../api_schemas_webserver/functions.py | 4 +- .../api/v0/openapi.yaml | 46 +++++++++++++++++++ .../functions/_controller/_functions_rest.py | 23 +++++++++- 3 files changed, 71 insertions(+), 2 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 f8d022d330b..963f82c78e9 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 @@ -114,7 +114,9 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... -class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): ... +class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + thumbnail: str | None = None + template_id: int | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... 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 225baa71107..5bb030b36b9 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 @@ -3348,6 +3348,18 @@ paths: $ref: '#/components/schemas/EnvelopedError' description: Service Unavailable /v0/functions: + get: + tags: + - functions + summary: List Functions + operationId: list_functions + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' post: tags: - functions @@ -11265,6 +11277,30 @@ components: title: Error type: object title: Envelope[dict[str, Any]] + Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____: + properties: + data: + anyOf: + - items: + oneOf: + - $ref: '#/components/schemas/RegisteredProjectFunctionGet' + - $ref: '#/components/schemas/RegisteredSolverFunctionGet' + discriminator: + propertyName: functionClass + mapping: + PROJECT: '#/components/schemas/RegisteredProjectFunctionGet' + SOLVER: '#/components/schemas/RegisteredSolverFunctionGet' + type: array + - type: 'null' + title: Data + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[list[Annotated[Union[RegisteredProjectFunctionGet, RegisteredSolverFunctionGet], + FieldInfo(annotation=NoneType, required=True, discriminator='function_class')]]] Envelope_list_Annotated_str__StringConstraints___: properties: data: @@ -16067,6 +16103,16 @@ components: type: string format: uuid title: Projectid + thumbnail: + anyOf: + - type: string + - type: 'null' + title: Thumbnail + templateId: + anyOf: + - type: integer + - type: 'null' + title: Templateid type: object required: - 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 60968203a87..f899bc7c47e 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 @@ -5,8 +5,10 @@ RegisteredFunction, RegisteredFunctionGet, RegisteredFunctionUpdate, + RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet +from models_library.functions import FunctionClass from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -87,7 +89,26 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - # TODO: enrich with project data + if registered_function.function_class == FunctionClass.PROJECT: + from ...projects import _projects_service + + assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec + + project_dict = await _projects_service.get_project_for_user( + app=request.app, + project_uuid=f"{registered_function.project_id}", + user_id=req_ctx.user_id, + ) + + return envelope_json_response( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( + registered_function.model_dump(mode="json") + | { + "thumbnail": project_dict.get("thumbnail", None), + "template_id": project_dict.get("project_id", None), + } + ) + ) return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( From 7e67484a9325793ebf78b98fdc6e5311b7cfba40 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 17 Jul 2025 23:51:59 +0200 Subject: [PATCH 11/43] fix: minor --- api/specs/web-server/_functions.py | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++-- .../functions/_controller/_functions_rest.py | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index 15ec04e5aaa..a3516d253c4 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -40,7 +40,7 @@ async def register_function( "/functions", response_model=Envelope[list[RegisteredFunctionGet]], ) -async def list_functions() -> Envelope[list[RegisteredFunctionGet]]: ... +async def list_functions(): ... @router.get( 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 5bb030b36b9..8498bcf21e0 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 @@ -11277,8 +11277,8 @@ components: title: Error type: object title: Envelope[dict[str, Any]] - Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____: - properties: + ? Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____ + : properties: data: anyOf: - items: 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 f899bc7c47e..6c5099f8ccc 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 @@ -19,6 +19,7 @@ from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required from ...models import AuthenticatedRequestContext +from ...projects import _projects_service from ...security.decorators import permission_required from ...utils_aiohttp import envelope_json_response from .. import _functions_service @@ -90,8 +91,6 @@ async def get_function(request: web.Request) -> web.Response: ) if registered_function.function_class == FunctionClass.PROJECT: - from ...projects import _projects_service - assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec project_dict = await _projects_service.get_project_for_user( From bcd0ff717fd724969bdbcefd88859846a510a0b8 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 11:08:22 +0200 Subject: [PATCH 12/43] feat: add function list --- api/specs/web-server/_functions.py | 8 +- .../api/v0/openapi.yaml | 71 ++++++++++++---- .../functions/_controller/_functions_rest.py | 83 ++++++++++++++++++- .../_controller/_functions_rest_schemas.py | 11 +++ .../projects/_projects_service.py | 4 +- 5 files changed, 157 insertions(+), 20 deletions(-) diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index a3516d253c4..1946f408d86 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -7,6 +7,7 @@ from typing import Annotated +from _common import as_query from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.functions import ( FunctionToRegister, @@ -16,7 +17,9 @@ from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.functions._controller._functions_rest_schemas import ( + FunctionGetQueryParams, FunctionPathParams, + FunctionsListQueryParams, ) router = APIRouter( @@ -40,7 +43,9 @@ async def register_function( "/functions", response_model=Envelope[list[RegisteredFunctionGet]], ) -async def list_functions(): ... +async def list_functions( + _query: Annotated[as_query(FunctionsListQueryParams), Depends()], +): ... @router.get( @@ -49,6 +54,7 @@ async def list_functions(): ... ) async def get_function( _path: Annotated[FunctionPathParams, Depends()], + _query: Annotated[as_query(FunctionGetQueryParams), Depends()], ): ... 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 8498bcf21e0..7f4f5558e46 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 @@ -3348,37 +3348,25 @@ paths: $ref: '#/components/schemas/EnvelopedError' description: Service Unavailable /v0/functions: - get: - tags: - - functions - summary: List Functions - operationId: list_functions - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' post: tags: - functions summary: Register Function operationId: register_function requestBody: + required: true content: application/json: schema: oneOf: - $ref: '#/components/schemas/ProjectFunctionToRegister' - $ref: '#/components/schemas/SolverFunctionToRegister' - title: ' Body' discriminator: propertyName: functionClass mapping: PROJECT: '#/components/schemas/ProjectFunctionToRegister' SOLVER: '#/components/schemas/SolverFunctionToRegister' - required: true + title: ' Body' responses: '200': description: Successful Response @@ -3386,6 +3374,40 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____' + get: + tags: + - functions + summary: List Functions + operationId: list_functions + parameters: + - name: include_extras + in: query + required: false + schema: + type: boolean + default: false + title: Include Extras + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_Annotated_Union_RegisteredProjectFunctionGet__RegisteredSolverFunctionGet___FieldInfo_annotation_NoneType__required_True__discriminator__function_class_____' /v0/functions/{function_id}: get: tags: @@ -3400,6 +3422,27 @@ paths: type: string format: uuid title: Function Id + - name: include_extras + in: query + required: false + schema: + type: boolean + default: false + title: Include Extras + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset responses: '200': description: Successful Response 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 6c5099f8ccc..0ea96ec233e 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 @@ -8,12 +8,15 @@ RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet -from models_library.functions import FunctionClass +from models_library.functions import FunctionClass, RegisteredProjectFunction +from models_library.rest_pagination import Page +from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( handle_validation_as_http_error, parse_request_path_parameters_as, + parse_request_query_parameters_as, ) from ..._meta import API_VTAG as VTAG @@ -24,7 +27,11 @@ from ...utils_aiohttp import envelope_json_response from .. import _functions_service from ._functions_rest_exceptions import handle_rest_requests_exceptions -from ._functions_rest_schemas import FunctionPathParams +from ._functions_rest_schemas import ( + FunctionGetQueryParams, + FunctionPathParams, + FunctionsListQueryParams, +) routes = web.RouteTableDef() @@ -68,7 +75,68 @@ async def register_function(request: web.Request) -> web.Response: @permission_required("function.read") @handle_rest_requests_exceptions async def list_functions(request: web.Request) -> web.Response: - raise NotImplementedError + query_params: FunctionsListQueryParams = parse_request_query_parameters_as( + FunctionsListQueryParams, request + ) + + req_ctx = AuthenticatedRequestContext.model_validate(request) + functions, page_meta_info = await _functions_service.list_functions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + pagination_limit=query_params.limit, + pagination_offset=query_params.offset, + ) + + chunk = [] + + 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 = await _projects_service.batch_get_projects( + request.app, + project_uuids=project_ids, + ) + projects_map = {f"{p.uuid}": p for p in projects} + + for function in functions: + if ( + query_params.include_extras + and function.function_class == FunctionClass.PROJECT + ): + assert isinstance(function, RegisteredProjectFunction) # nosec + project = projects_map.get(f"{function.project_id}") + if project: + chunk.append( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( + function.model_dump(mode="json") + | { + "thumbnail": project.thumbnail, + "template_id": project.id, + } + ) + ) + else: + chunk.append( + TypeAdapter(RegisteredFunctionGet).validate_python( + function.model_dump(mode="json") + ) + ) + + page = Page[RegisteredFunctionGet].model_validate( + paginate_data( + chunk=chunk, + request_url=request.url, + total=page_meta_info.total, + limit=query_params.limit, + offset=query_params.offset, + ) + ) + return envelope_json_response(page) @routes.get( @@ -82,6 +150,10 @@ async def get_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) function_id = path_params.function_id + query_params: FunctionGetQueryParams = parse_request_query_parameters_as( + FunctionGetQueryParams, request + ) + req_ctx = AuthenticatedRequestContext.model_validate(request) registered_function: RegisteredFunction = await _functions_service.get_function( app=request.app, @@ -90,7 +162,10 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - if registered_function.function_class == FunctionClass.PROJECT: + if ( + query_params.include_extras + and registered_function.function_class == FunctionClass.PROJECT + ): assert isinstance(registered_function, RegisteredProjectFunctionGet) # nosec project_dict = await _projects_service.get_project_for_user( diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py index f97f5728511..fa6147e52e4 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py @@ -1,4 +1,5 @@ from models_library.functions import FunctionID +from models_library.rest_pagination import PageQueryParameters from pydantic import BaseModel, ConfigDict from ...models import AuthenticatedRequestContext @@ -11,4 +12,14 @@ class FunctionPathParams(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="forbid") +class _FunctionQueryParams(BaseModel): + include_extras: bool = False + + +class FunctionGetQueryParams(PageQueryParameters, _FunctionQueryParams): ... + + +class FunctionsListQueryParams(PageQueryParameters, _FunctionQueryParams): ... + + __all__: tuple[str, ...] = ("AuthenticatedRequestContext",) 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 621f7554549..b2b50257b1c 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 @@ -276,7 +276,9 @@ async def batch_get_project_name( async def batch_get_projects( - app: web.Application, project_uuids: list[ProjectID] + app: web.Application, + *, + project_uuids: list[ProjectID], ) -> list[ProjectDBGet]: return await _projects_repository.batch_get_projects( app=app, From 44ad1ce249f2947f4ba83feda72c45e628d9e13e Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 11:46:54 +0200 Subject: [PATCH 13/43] fix: function listing --- .../functions/_controller/_functions_rest.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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 0ea96ec233e..602cd4324bf 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 @@ -126,6 +126,15 @@ async def list_functions(request: web.Request) -> web.Response: function.model_dump(mode="json") ) ) + else: + chunk.extend( + [ + TypeAdapter(RegisteredFunctionGet).validate_python( + function.model_dump(mode="json") + ) + for function in functions + ] + ) page = Page[RegisteredFunctionGet].model_validate( paginate_data( From 4ce827124bed9ee25525eebbf6c02a4c518a5fe1 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 12:50:36 +0200 Subject: [PATCH 14/43] fix: project id missing --- .../functions/_controller/_functions_rest.py | 56 +++++++++---------- .../_controller/_functions_rest_schemas.py | 2 +- 2 files changed, 28 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 602cd4324bf..c6ca081ed71 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 @@ -23,6 +23,7 @@ from ...login.decorators import login_required from ...models import AuthenticatedRequestContext from ...projects import _projects_service +from ...projects.models import ProjectDBGet from ...security.decorators import permission_required from ...utils_aiohttp import envelope_json_response from .. import _functions_service @@ -88,7 +89,10 @@ async def list_functions(request: web.Request) -> web.Response: pagination_offset=query_params.offset, ) - chunk = [] + chunk: list[RegisteredFunctionGet] = [] + projects_map: dict[str, ProjectDBGet] = ( + {} + ) # ProjectDBGet has to be renamed at some point! if query_params.include_extras: project_ids = [] @@ -101,40 +105,34 @@ async def list_functions(request: web.Request) -> web.Response: request.app, project_uuids=project_ids, ) - projects_map = {f"{p.uuid}": p for p in projects} - - for function in functions: - if ( - query_params.include_extras - and function.function_class == FunctionClass.PROJECT - ): - assert isinstance(function, RegisteredProjectFunction) # nosec - project = projects_map.get(f"{function.project_id}") - if project: - chunk.append( - TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function.model_dump(mode="json") - | { - "thumbnail": project.thumbnail, - "template_id": project.id, - } - ) - ) - else: + for project in projects: + projects_map[f"{project.uuid}"] = project + + for function in functions: + if ( + query_params.include_extras + and function.function_class == FunctionClass.PROJECT + ): + assert isinstance(function, RegisteredProjectFunction) # nosec + project = projects_map.get(f"{function.project_id}") + if project: chunk.append( - TypeAdapter(RegisteredFunctionGet).validate_python( + TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") + | { + "thumbnail": ( + f"{project.thumbnail}" if project.thumbnail else None + ), + "template_id": project.id, + } ) ) - else: - chunk.extend( - [ + else: + chunk.append( TypeAdapter(RegisteredFunctionGet).validate_python( function.model_dump(mode="json") ) - for function in functions - ] - ) + ) page = Page[RegisteredFunctionGet].model_validate( paginate_data( @@ -188,7 +186,7 @@ async def get_function(request: web.Request) -> web.Response: registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), - "template_id": project_dict.get("project_id", None), + "template_id": project_dict.get("id", None), } ) ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py index fa6147e52e4..79030f202be 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py @@ -16,7 +16,7 @@ class _FunctionQueryParams(BaseModel): include_extras: bool = False -class FunctionGetQueryParams(PageQueryParameters, _FunctionQueryParams): ... +class FunctionGetQueryParams(_FunctionQueryParams): ... class FunctionsListQueryParams(PageQueryParameters, _FunctionQueryParams): ... From a65a99be91dee6e3333719510a32655886ab9147 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:36:25 +0200 Subject: [PATCH 15/43] fix: list functions pagination --- .../functions/_controller/_functions_rest.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 c6ca081ed71..3ce5dd58bb8 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 @@ -9,7 +9,7 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import FunctionClass, RegisteredProjectFunction -from models_library.rest_pagination import Page +from models_library.rest_pagination import ItemT, Page from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -18,6 +18,8 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) +from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON +from servicelib.rest_constants import RESPONSE_MODEL_POLICY from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required @@ -37,6 +39,13 @@ routes = web.RouteTableDef() +def _create_json_response_from_page(page: Page[ItemT]): + return web.Response( + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), + content_type=MIMETYPE_APPLICATION_JSON, + ) + + @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -143,7 +152,7 @@ async def list_functions(request: web.Request) -> web.Response: offset=query_params.offset, ) ) - return envelope_json_response(page) + return _create_json_response_from_page(page) @routes.get( From e21a10b809d9f375ca65938a44224a5f89848012 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:42:07 +0200 Subject: [PATCH 16/43] fix: enable frontend --- .../client/source/class/osparc/data/Resources.js | 8 ++++++++ .../client/source/class/osparc/store/Functions.js | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index dffd67aaac2..e4e2492a0ac 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -631,6 +631,14 @@ qx.Class.define("osparc.data.Resources", { create: { method: "POST", url: statics.API + "/functions" + }, + getOne: { + method: "GET", + url: statics.API + "/functions/{functionId}?include_extras=true" + }, + getPage: { + method: "GET", + url: statics.API + "/functions?include_extras=true" } } }, diff --git a/services/static-webserver/client/source/class/osparc/store/Functions.js b/services/static-webserver/client/source/class/osparc/store/Functions.js index 487e844c911..7e951e6c327 100644 --- a/services/static-webserver/client/source/class/osparc/store/Functions.js +++ b/services/static-webserver/client/source/class/osparc/store/Functions.js @@ -92,7 +92,7 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunctionsPaginated: function(params, options) { - const isBackendReady = false; + const isBackendReady = true; if (!isBackendReady) { return new Promise(resolve => { const response = this.__dummyResponse(); @@ -110,7 +110,7 @@ qx.Class.define("osparc.store.Functions", { }, fetchFunction: function(functionId) { - const isBackendReady = false; + const isBackendReady = true; if (!isBackendReady) { return new Promise(resolve => { const response = this.__dummyResponse(); From e70e67e7383be259895b3ef3fbd1fc2f09c65ccd Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:51:28 +0200 Subject: [PATCH 17/43] fix: openapi spec --- .../api/v0/openapi.yaml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 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 7f4f5558e46..5f83c872432 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 @@ -3429,20 +3429,6 @@ paths: type: boolean default: false title: Include Extras - - name: limit - in: query - required: false - schema: - type: integer - default: 20 - title: Limit - - name: offset - in: query - required: false - schema: - type: integer - default: 0 - title: Offset responses: '200': description: Successful Response @@ -16146,6 +16132,10 @@ components: type: string format: uuid title: Projectid + uuid: + type: string + format: uuid + title: Uuid thumbnail: anyOf: - type: string @@ -16164,6 +16154,7 @@ components: - uid - createdAt - projectId + - uuid title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: From 3c462331036c4aec9587cb8c0d80c3e6285bae9d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 13:51:54 +0200 Subject: [PATCH 18/43] fix: add function ID --- .../src/models_library/api_schemas_webserver/functions.py | 1 + .../functions/_controller/_functions_rest.py | 1 + 2 files changed, 2 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 963f82c78e9..fca93be4701 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 @@ -115,6 +115,7 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + uuid: FunctionID thumbnail: str | None = None template_id: int | None = None 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 3ce5dd58bb8..fc8ce0f46d2 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 @@ -129,6 +129,7 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { + "uuid": function.uid, "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), From d2866549d6008f7b35140f314363081be6f9e06c Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:01:49 +0200 Subject: [PATCH 19/43] fix: remap keys --- .../functions/_controller/_functions_rest.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 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 fc8ce0f46d2..cd373399e5c 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,5 @@ from aiohttp import web +from common_library.dict_tools import remap_keys from models_library.api_schemas_webserver.functions import ( Function, FunctionToRegister, @@ -127,9 +128,13 @@ async def list_functions(request: web.Request) -> web.Response: if project: chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - function.model_dump(mode="json") + remap_keys( + function.model_dump(mode="json"), + rename={ + "uid": "uuid", + }, + ) | { - "uuid": function.uid, "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), @@ -193,7 +198,12 @@ async def get_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - registered_function.model_dump(mode="json") + remap_keys( + registered_function.model_dump(mode="json"), + rename={ + "uid": "uuid", + }, + ) | { "thumbnail": project_dict.get("thumbnail", None), "template_id": project_dict.get("id", None), From a29729b19dd9c7c810194c7fa70fc94d6f2f1eef Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:20:52 +0200 Subject: [PATCH 20/43] fix: id key name --- .../source/class/osparc/data/model/Function.js | 2 +- .../functions/_controller/_functions_rest.py | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 58f89f7fc99..35f8b733618 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -29,7 +29,7 @@ qx.Class.define("osparc.data.model.Function", { this.base(arguments); this.set({ - uuid: functionData.uuid, + uuid: functionData.uid, functionType: functionData.functionClass, name: functionData.name, description: functionData.description, 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 cd373399e5c..3ce5dd58bb8 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,5 +1,4 @@ from aiohttp import web -from common_library.dict_tools import remap_keys from models_library.api_schemas_webserver.functions import ( Function, FunctionToRegister, @@ -128,12 +127,7 @@ async def list_functions(request: web.Request) -> web.Response: if project: chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - remap_keys( - function.model_dump(mode="json"), - rename={ - "uid": "uuid", - }, - ) + function.model_dump(mode="json") | { "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None @@ -198,12 +192,7 @@ async def get_function(request: web.Request) -> web.Response: return envelope_json_response( TypeAdapter(RegisteredProjectFunctionGet).validate_python( - remap_keys( - registered_function.model_dump(mode="json"), - rename={ - "uid": "uuid", - }, - ) + registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), "template_id": project_dict.get("id", None), From fcedfc043cd0574aa822efa117be37622f53155d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 14:28:50 +0200 Subject: [PATCH 21/43] fix: remove unused uuid --- .../src/models_library/api_schemas_webserver/functions.py | 1 - 1 file changed, 1 deletion(-) 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 fca93be4701..963f82c78e9 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 @@ -115,7 +115,6 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): - uuid: FunctionID thumbnail: str | None = None template_id: int | None = None From 0c506ad7ffab939b401dbbc1229b52506ac75665 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:29:07 +0200 Subject: [PATCH 22/43] fix: template ID --- .../src/models_library/api_schemas_webserver/functions.py | 4 ++++ .../client/source/class/osparc/data/model/Function.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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 963f82c78e9..7814c1ed6ed 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 @@ -47,6 +47,7 @@ UnsupportedFunctionClassError, UnsupportedFunctionFunctionJobClassCombinationError, ) +from ..projects import ProjectID from ._base import InputSchema, OutputSchema __all__ = [ @@ -115,6 +116,9 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): + uid: Annotated[FunctionID, Field(alias="uuid")] + title: Annotated[str, Field(alias="name")] = "" + project_id: Annotated[ProjectID, Field(alias="template_id")] thumbnail: str | None = None template_id: int | None = None diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 35f8b733618..58f89f7fc99 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -29,7 +29,7 @@ qx.Class.define("osparc.data.model.Function", { this.base(arguments); this.set({ - uuid: functionData.uid, + uuid: functionData.uuid, functionType: functionData.functionClass, name: functionData.name, description: functionData.description, From 8cbf75e3e68da1a06ccf31851e614c27107d0a70 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:31:10 +0200 Subject: [PATCH 23/43] fix: use template ID --- .../functions/_controller/_functions_rest.py | 2 -- 1 file changed, 2 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 3ce5dd58bb8..5217e6ee041 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 @@ -132,7 +132,6 @@ async def list_functions(request: web.Request) -> web.Response: "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), - "template_id": project.id, } ) ) @@ -195,7 +194,6 @@ async def get_function(request: web.Request) -> web.Response: registered_function.model_dump(mode="json") | { "thumbnail": project_dict.get("thumbnail", None), - "template_id": project_dict.get("id", None), } ) ) From f06b3bf1e495c6e315f22dc0e384f6c6c416946a Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 15:47:13 +0200 Subject: [PATCH 24/43] feat: retrieve modified --- packages/models-library/src/models_library/functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index 966d5a9b974..da462beb234 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -255,6 +255,7 @@ class FunctionDB(BaseModel): class RegisteredFunctionDB(FunctionDB): uuid: FunctionID created: datetime.datetime + modified: datetime.datetime class FunctionJobCollectionDB(BaseModel): From 48f19aff4c3222c825ee8b204a5ede6de2b63355 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:00:45 +0200 Subject: [PATCH 25/43] feat: expose modified at --- .../src/models_library/api_schemas_webserver/functions.py | 7 +++++-- packages/models-library/src/models_library/functions.py | 1 + .../functions/_functions_service.py | 2 ++ 3 files changed, 8 insertions(+), 2 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 7814c1ed6ed..f196f0ec546 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,3 +1,4 @@ +import datetime from typing import Annotated, TypeAlias from pydantic import Field @@ -118,9 +119,11 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] title: Annotated[str, Field(alias="name")] = "" - project_id: Annotated[ProjectID, Field(alias="template_id")] + project_id: Annotated[ProjectID, Field(alias="templateId")] + created_at: Annotated[datetime.datetime, Field(alias="creationDate")] + modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] thumbnail: str | None = None - template_id: int | None = None + template_id: ProjectID | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index da462beb234..6f1cbaf136f 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -99,6 +99,7 @@ class FunctionBase(BaseModel): class RegisteredFunctionBase(FunctionBase): uid: FunctionID created_at: datetime.datetime + modified_at: datetime.datetime class FunctionUpdate(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py index 75ba208ab8f..e23cf0317c3 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py @@ -457,6 +457,7 @@ def _decode_function( project_id=function.class_specific_data["project_id"], default_inputs=function.default_inputs, created_at=function.created, + modified_at=function.modified, ) if function.function_class == FunctionClass.SOLVER: @@ -470,6 +471,7 @@ def _decode_function( solver_version=function.class_specific_data["solver_version"], default_inputs=function.default_inputs, created_at=function.created, + modified_at=function.modified, ) raise UnsupportedFunctionClassError(function_class=function.function_class) From 0a969b51d4d8584a65f6577de642a95ff7682f9b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:09:02 +0200 Subject: [PATCH 26/43] fix: return proper templateId --- .../src/models_library/api_schemas_webserver/functions.py | 1 - 1 file changed, 1 deletion(-) 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 f196f0ec546..aeadde571e7 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 @@ -123,7 +123,6 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] thumbnail: str | None = None - template_id: ProjectID | None = None class SolverFunctionToRegister(SolverFunction, InputSchema): ... From e8ca8bf33e6131a9436599c04cd6a90581421a80 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 18 Jul 2025 16:36:55 +0200 Subject: [PATCH 27/43] fix: typecheck --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5217e6ee041..c8a9bfdf328 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 @@ -99,7 +99,7 @@ async def list_functions(request: web.Request) -> web.Response: ) chunk: list[RegisteredFunctionGet] = [] - projects_map: dict[str, ProjectDBGet] = ( + projects_map: dict[str, ProjectDBGet | None] = ( {} ) # ProjectDBGet has to be renamed at some point! From 4840afde1f80f4eb1d4e1f57e760ac45ab6a2e55 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 10:09:09 +0200 Subject: [PATCH 28/43] tests: add list functions --- .../test_functions_controller_rest.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 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 77f26b9a1cb..5bfcdc8654d 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 @@ -18,6 +18,7 @@ RegisteredProjectFunctionGet, ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet +from pydantic import TypeAdapter from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_users import UserInfoDict from servicelib.aiohttp import status @@ -50,13 +51,14 @@ def mock_function() -> dict[str, Any]: @pytest.mark.parametrize( - "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_delete,expected_get2", + "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_delete,expected_get2", [ ( UserRole.USER, True, status.HTTP_201_CREATED, status.HTTP_200_OK, + status.HTTP_200_OK, status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND, ), @@ -67,16 +69,18 @@ def mock_function() -> dict[str, Any]: status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, + status.HTTP_403_FORBIDDEN, ), ], indirect=["add_user_function_api_access_rights"], ) -async def test_register_get_delete_function( +async def test_function_workflow( client: TestClient, logged_user: UserInfoDict, mock_function: dict[str, Any], expected_register: HTTPStatus, expected_get: HTTPStatus, + expected_list: HTTPStatus, expected_delete: HTTPStatus, expected_get2: HTTPStatus, add_user_function_api_access_rights: AsyncIterator[None], @@ -101,6 +105,16 @@ async def test_register_get_delete_function( retrieved_function = RegisteredProjectFunctionGet.model_validate(data) assert retrieved_function.uid == returned_function.uid + url = client.app.router["list_functions"].url_for() + response = await client.get(url) + data, error = await assert_status(response, expected_list) + if not error: + retrieved_functions = TypeAdapter( + list[RegisteredProjectFunctionGet] + ).validate_python(data) + assert len(retrieved_functions) == 1 + assert retrieved_functions[0].uid == returned_function_uid + url = client.app.router["delete_function"].url_for( function_id=str(returned_function_uid) ) From a72715869287c448595cb78357d232eb6854af5d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 12:58:33 +0200 Subject: [PATCH 29/43] tests: add function update --- .../functions/_controller/_functions_rest.py | 4 +-- .../security/_authz_access_roles.py | 1 + .../test_functions_controller_rest.py | 31 ++++++++++++++++--- 3 files changed, 30 insertions(+), 6 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 c8a9bfdf328..af2865370fb 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 @@ -210,7 +210,7 @@ async def get_function(request: web.Request) -> web.Response: name="update_function", ) @login_required -@permission_required("function.write") +@permission_required("function.update") @handle_rest_requests_exceptions async def update_function(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(FunctionPathParams, request) @@ -230,7 +230,7 @@ async def update_function(request: web.Request) -> web.Response: ) return envelope_json_response( - TypeAdapter(RegisteredFunctionUpdate).validate_python( + TypeAdapter(RegisteredFunctionGet).validate_python( updated_function.model_dump(mode="json") ) ) diff --git a/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py b/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py index 0edd5b2a10c..0c0579a0b69 100644 --- a/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py +++ b/services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py @@ -78,6 +78,7 @@ class PermissionDict(TypedDict, total=False): "project.workspaces.*", "function.create", "function.read", + "function.update", "function.execute", "function.delete", "resource-usage.read", 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 5bfcdc8654d..24e412431eb 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 @@ -51,7 +51,7 @@ def mock_function() -> dict[str, Any]: @pytest.mark.parametrize( - "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_delete,expected_get2", + "user_role,add_user_function_api_access_rights,expected_register,expected_get,expected_list,expected_update,expected_delete,expected_get2", [ ( UserRole.USER, @@ -59,6 +59,7 @@ def mock_function() -> dict[str, Any]: status.HTTP_201_CREATED, status.HTTP_200_OK, status.HTTP_200_OK, + status.HTTP_200_OK, status.HTTP_204_NO_CONTENT, status.HTTP_404_NOT_FOUND, ), @@ -70,6 +71,7 @@ def mock_function() -> dict[str, Any]: status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, status.HTTP_403_FORBIDDEN, + status.HTTP_403_FORBIDDEN, ), ], indirect=["add_user_function_api_access_rights"], @@ -81,11 +83,13 @@ async def test_function_workflow( expected_register: HTTPStatus, expected_get: HTTPStatus, expected_list: HTTPStatus, + expected_update: HTTPStatus, expected_delete: HTTPStatus, expected_get2: HTTPStatus, add_user_function_api_access_rights: AsyncIterator[None], request: pytest.FixtureRequest, ) -> None: + # Register a new function url = client.app.router["register_function"].url_for() response = await client.post(url, json=mock_function) data, error = await assert_status(response, expected_status_code=expected_register) @@ -96,8 +100,9 @@ async def test_function_workflow( assert returned_function.uid is not None returned_function_uid = returned_function.uid + # Get the registered function url = client.app.router["get_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.get(url) data, error = await assert_status(response, expected_get) @@ -105,6 +110,7 @@ async def test_function_workflow( retrieved_function = RegisteredProjectFunctionGet.model_validate(data) assert retrieved_function.uid == returned_function.uid + # List existing functions url = client.app.router["list_functions"].url_for() response = await client.get(url) data, error = await assert_status(response, expected_list) @@ -115,14 +121,31 @@ async def test_function_workflow( assert len(retrieved_functions) == 1 assert retrieved_functions[0].uid == returned_function_uid + # Update existing function + new_title = "Test Function (edited)" + new_description = "A test function (edited)" + url = client.app.router["update_function"].url_for( + function_id=f"{returned_function_uid}" + ) + response = await client.patch( + url, json={"title": new_title, "description": new_description} + ) + data, error = await assert_status(response, expected_update) + if not error: + updated_function = RegisteredProjectFunctionGet.model_validate(data) + assert updated_function.title == new_title + assert updated_function.description == new_description + + # Delete existing function url = client.app.router["delete_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.delete(url) data, error = await assert_status(response, expected_delete) + # Check if the function was effectively deleted url = client.app.router["get_function"].url_for( - function_id=str(returned_function_uid) + function_id=f"{returned_function_uid}" ) response = await client.get(url) data, error = await assert_status(response, expected_get2) From edc8c2d64ca476e4b6c9363384050c78a087cc59 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 13:27:25 +0200 Subject: [PATCH 30/43] fix: typecheck --- .../functions/_controller/_functions_rest.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 af2865370fb..d504df22df8 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,7 +39,7 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]): +def _create_json_response_from_page(page: Page[ItemT]) -> web.Response: return web.Response( text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, @@ -110,12 +110,13 @@ async def list_functions(request: web.Request) -> web.Response: assert isinstance(function, RegisteredProjectFunction) project_ids.append(function.project_id) - projects = await _projects_service.batch_get_projects( - request.app, - project_uuids=project_ids, - ) - for project in projects: - projects_map[f"{project.uuid}"] = project + projects_map = { + f"{p.uuid}": p + for p in await _projects_service.batch_get_projects( + request.app, + project_uuids=project_ids, + ) + } for function in functions: if ( @@ -123,8 +124,7 @@ async def list_functions(request: web.Request) -> web.Response: and function.function_class == FunctionClass.PROJECT ): assert isinstance(function, RegisteredProjectFunction) # nosec - project = projects_map.get(f"{function.project_id}") - if project: + if project := projects_map.get(f"{function.project_id}"): chunk.append( TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") From b2a189872c9f612968775139a8eb9e142a1752d0 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 13:44:22 +0200 Subject: [PATCH 31/43] fix: add modified_at field --- services/api-server/tests/unit/api_functions/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index f25334c5a28..7eca63325a9 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -130,6 +130,7 @@ def mock_registered_project_function(mock_function: Function) -> RegisteredFunct **mock_function.dict(), "uid": str(uuid4()), "created_at": datetime.datetime.now(datetime.UTC), + "modified_at": datetime.datetime.now(datetime.UTC), } ) @@ -150,6 +151,7 @@ def mock_registered_solver_function( "default_inputs": None, "uid": str(uuid4()), "created_at": datetime.datetime.now(datetime.UTC), + "modified_at": datetime.datetime.now(datetime.UTC), "solver_key": "simcore/services/comp/ans-model", "solver_version": "1.0.1", } From b57a590a0cb6708ff69c8bd4fa0258a17250c4d4 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 14:01:56 +0200 Subject: [PATCH 32/43] fix: deprecated pydantic --- .../tests/unit/api_functions/conftest.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index 7eca63325a9..f248fcfcf2d 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -127,8 +127,8 @@ def mock_function( def mock_registered_project_function(mock_function: Function) -> RegisteredFunction: return RegisteredProjectFunction( **{ - **mock_function.dict(), - "uid": str(uuid4()), + **mock_function.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), "modified_at": datetime.datetime.now(datetime.UTC), } @@ -149,7 +149,7 @@ def mock_registered_solver_function( "input_schema": sample_input_schema, "output_schema": sample_output_schema, "default_inputs": None, - "uid": str(uuid4()), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), "modified_at": datetime.datetime.now(datetime.UTC), "solver_key": "simcore/services/comp/ans-model", @@ -168,7 +168,7 @@ def mock_project_function_job( "description": "A test function job", "inputs": {"key": "value"}, "outputs": None, - "project_job_id": str(uuid4()), + "project_job_id": f"{uuid4()}", "function_class": FunctionClass.PROJECT, } return ProjectFunctionJob(**mock_function_job) @@ -180,8 +180,8 @@ def mock_registered_project_function_job( ) -> RegisteredFunctionJob: return RegisteredProjectFunctionJob( **{ - **mock_project_function_job.dict(), - "uid": str(uuid4()), + **mock_project_function_job.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) @@ -208,8 +208,8 @@ def mock_registered_solver_function_job( ) -> RegisteredFunctionJob: return RegisteredSolverFunctionJob( **{ - **mock_solver_function_job.dict(), - "uid": str(uuid4()), + **mock_solver_function_job.model_dump(), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) @@ -224,7 +224,7 @@ def mock_function_job_collection( "description": "A test function job collection", "function_uid": mock_registered_project_function_job.function_uid, "function_class": FunctionClass.PROJECT, - "project_id": str(uuid4()), + "project_id": f"{uuid4()}", "function_job_ids": [ mock_registered_project_function_job.uid for _ in range(5) ], @@ -239,7 +239,7 @@ def mock_registered_function_job_collection( return RegisteredFunctionJobCollection( **{ **mock_function_job_collection.model_dump(), - "uid": str(uuid4()), + "uid": f"{uuid4()}", "created_at": datetime.datetime.now(datetime.UTC), } ) From 57010e5e51ec4f8680aafdc100f2b0663057d365 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 14:11:32 +0200 Subject: [PATCH 33/43] fix: make openapi-spec --- .../api/v0/openapi.yaml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 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 5f83c872432..873083cb713 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 @@ -16090,9 +16090,9 @@ components: const: PROJECT title: Functionclass default: PROJECT - title: + name: type: string - title: Title + title: Name default: '' description: type: string @@ -16120,41 +16120,36 @@ components: type: object - type: 'null' title: Defaultinputs - uid: + uuid: type: string format: uuid - title: Uid - createdAt: + title: Uuid + creationDate: type: string format: date-time - title: Createdat - projectId: + title: Creationdate + lastChangeDate: type: string - format: uuid - title: Projectid - uuid: + format: date-time + title: Lastchangedate + templateId: type: string format: uuid - title: Uuid + title: Templateid thumbnail: anyOf: - type: string - type: 'null' title: Thumbnail - templateId: - anyOf: - - type: integer - - type: 'null' - title: Templateid type: object required: - inputSchema - outputSchema - defaultInputs - - uid - - createdAt - - projectId - uuid + - creationDate + - lastChangeDate + - templateId title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: @@ -16201,6 +16196,10 @@ components: type: string format: date-time title: Createdat + modifiedAt: + type: string + format: date-time + title: Modifiedat solverKey: type: string pattern: ^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$ @@ -16216,6 +16215,7 @@ components: - defaultInputs - uid - createdAt + - modifiedAt - solverKey - solverVersion title: RegisteredSolverFunctionGet From 606f2f77d0841f02441856836acf76760c8aa777 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 15:04:41 +0200 Subject: [PATCH 34/43] fix: make openapi-spec --- services/api-server/openapi.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 40b9594f3e5..588d54a792f 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -10623,6 +10623,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "project_id": { "type": "string", "format": "uuid", @@ -10636,6 +10641,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "project_id" ], "title": "RegisteredProjectFunction" @@ -10782,6 +10788,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "code_url": { "type": "string", "title": "Code Url" @@ -10794,6 +10805,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "code_url" ], "title": "RegisteredPythonCodeFunction" @@ -10934,6 +10946,11 @@ "format": "date-time", "title": "Created At" }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + }, "solver_key": { "type": "string", "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", @@ -10952,6 +10969,7 @@ "default_inputs", "uid", "created_at", + "modified_at", "solver_key", "solver_version" ], From 89571eb5c75fc22a3ed4f929df2a6e6cabea927b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Mon, 21 Jul 2025 15:48:09 +0200 Subject: [PATCH 35/43] fix: rename --- .../src/models_library/api_schemas_webserver/functions.py | 1 - .../client/source/class/osparc/data/model/Function.js | 2 +- 2 files changed, 1 insertion(+), 2 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 aeadde571e7..01c4ee64df3 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 @@ -118,7 +118,6 @@ class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): ... class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] - title: Annotated[str, Field(alias="name")] = "" project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] diff --git a/services/static-webserver/client/source/class/osparc/data/model/Function.js b/services/static-webserver/client/source/class/osparc/data/model/Function.js index 58f89f7fc99..1dae7c4047b 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Function.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Function.js @@ -31,7 +31,7 @@ qx.Class.define("osparc.data.model.Function", { this.set({ uuid: functionData.uuid, functionType: functionData.functionClass, - name: functionData.name, + name: functionData.title, description: functionData.description, inputSchema: functionData.inputSchema || this.getInputSchema(), outputSchema: functionData.outputSchema || this.getOutputSchema(), From ebed70d54e43ec77566f27132dc1a80587e4c221 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 09:21:13 +0200 Subject: [PATCH 36/43] fix: use common create_json_response_from_page --- .../folders/_folders_rest.py | 17 ++++------------- .../functions/_controller/_functions_rest.py | 15 +++------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py b/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py index 5219a500f1a..b43c75ca571 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_rest.py @@ -8,7 +8,7 @@ ) from models_library.folders import FolderTuple from models_library.rest_ordering import OrderBy -from models_library.rest_pagination import ItemT, Page +from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -16,13 +16,11 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.rest_constants import RESPONSE_MODEL_POLICY from .._meta import API_VTAG as VTAG from ..login.decorators import login_required from ..security.decorators import permission_required -from ..utils_aiohttp import envelope_json_response +from ..utils_aiohttp import create_json_response_from_page, envelope_json_response from . import _folders_service from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ( @@ -39,13 +37,6 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]): - return web.Response( - text=page.model_dump_json(**RESPONSE_MODEL_POLICY), - content_type=MIMETYPE_APPLICATION_JSON, - ) - - @routes.post(f"/{VTAG}/folders", name="create_folder") @login_required @permission_required("folder.create") @@ -107,7 +98,7 @@ async def list_folders(request: web.Request): offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get(f"/{VTAG}/folders:search", name="list_folders_full_search") @@ -143,7 +134,7 @@ async def list_folders_full_search(request: web.Request): offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get(f"/{VTAG}/folders/{{folder_id}}", name="get_folder") 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 c2d56756eab..4486aa04bec 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 @@ -9,7 +9,7 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import FunctionClass, RegisteredProjectFunction -from models_library.rest_pagination import ItemT, Page +from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data from pydantic import TypeAdapter from servicelib.aiohttp import status @@ -18,8 +18,6 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.rest_constants import RESPONSE_MODEL_POLICY from ..._meta import API_VTAG as VTAG from ...login.decorators import login_required @@ -27,7 +25,7 @@ from ...projects import _projects_service from ...projects.models import ProjectDBGet from ...security.decorators import permission_required -from ...utils_aiohttp import envelope_json_response +from ...utils_aiohttp import create_json_response_from_page, envelope_json_response from .. import _functions_service from ._functions_rest_exceptions import handle_rest_requests_exceptions from ._functions_rest_schemas import ( @@ -39,13 +37,6 @@ routes = web.RouteTableDef() -def _create_json_response_from_page(page: Page[ItemT]) -> web.Response: - return web.Response( - text=page.model_dump_json(**RESPONSE_MODEL_POLICY), - content_type=MIMETYPE_APPLICATION_JSON, - ) - - @routes.post(f"/{VTAG}/functions", name="register_function") @login_required @handle_rest_requests_exceptions @@ -150,7 +141,7 @@ async def list_functions(request: web.Request) -> web.Response: offset=query_params.offset, ) ) - return _create_json_response_from_page(page) + return create_json_response_from_page(page) @routes.get( From d03ac5936cab63be26f39b4632da931ddb44616b Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 09:40:45 +0200 Subject: [PATCH 37/43] fix: update field --- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 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 05b6f6c92ec..e65828c3803 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 @@ -16035,9 +16035,9 @@ components: const: PROJECT title: Functionclass default: PROJECT - name: + title: type: string - title: Name + title: Title default: '' description: type: string From 945f2318832605fe3d8a62899926da6582e6f48e Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:32:32 +0200 Subject: [PATCH 38/43] feat: add accessRights --- .../src/models_library/api_schemas_webserver/functions.py | 2 ++ .../functions/_controller/_functions_rest.py | 6 ++++++ 2 files changed, 8 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 01c4ee64df3..86dd30545ee 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 @@ -5,6 +5,7 @@ from ..functions import ( Function, + FunctionAccessRights, FunctionBase, FunctionClass, FunctionClassSpecificData, @@ -121,6 +122,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] + access_rights: FunctionAccessRights thumbnail: str | None = None 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 4486aa04bec..e1c10f04ae3 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 @@ -119,6 +119,12 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { + "accessRights": await _functions_service.get_function_user_permissions( + request.app, + user_id=req_ctx.user_id, + function_id=function.uid, + product_name=req_ctx.product_name, + ), "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None ), From 5e3f9f65b3a6a848e5023cc4a7a04e78046218e4 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:43:27 +0200 Subject: [PATCH 39/43] fix: rename field --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e1c10f04ae3..9bbce3cc48d 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 @@ -119,7 +119,7 @@ async def list_functions(request: web.Request) -> web.Response: TypeAdapter(RegisteredProjectFunctionGet).validate_python( function.model_dump(mode="json") | { - "accessRights": await _functions_service.get_function_user_permissions( + "access_rights": await _functions_service.get_function_user_permissions( request.app, user_id=req_ctx.user_id, function_id=function.uid, From 740c5516cdd3a8df6263eb46b2810250834c6ac8 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 10:46:47 +0200 Subject: [PATCH 40/43] fix: make openapi-spec --- .../api/v0/openapi.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 e65828c3803..7c377a655cf 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 @@ -12324,6 +12324,23 @@ components: required: - name title: FolderReplaceBodyParams + FunctionAccessRights: + properties: + read: + type: boolean + title: Read + default: false + write: + type: boolean + title: Write + default: false + execute: + type: boolean + title: Execute + default: false + additionalProperties: false + type: object + title: FunctionAccessRights GetProjectInactivityResponse: properties: is_inactive: @@ -16081,6 +16098,8 @@ components: type: string format: uuid title: Templateid + accessRights: + $ref: '#/components/schemas/FunctionAccessRights' thumbnail: anyOf: - type: string @@ -16095,6 +16114,7 @@ components: - creationDate - lastChangeDate - templateId + - accessRights title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: From b900510a45b7adf58a1e288bd15f12811016388f Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 11:12:17 +0200 Subject: [PATCH 41/43] fix: order --- .../functions/_controller/_functions_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9bbce3cc48d..0030562a0eb 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 @@ -122,8 +122,8 @@ async def list_functions(request: web.Request) -> web.Response: "access_rights": await _functions_service.get_function_user_permissions( request.app, user_id=req_ctx.user_id, - function_id=function.uid, product_name=req_ctx.product_name, + function_id=function.uid, ), "thumbnail": ( f"{project.thumbnail}" if project.thumbnail else None From d440ad0abf0b9e93a30383cc1c954c809de55ea6 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 11:21:39 +0200 Subject: [PATCH 42/43] fix: access_rights not required --- .../src/models_library/api_schemas_webserver/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 86dd30545ee..43e41961076 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 @@ -122,7 +122,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] - access_rights: FunctionAccessRights + access_rights: FunctionAccessRights | None = None thumbnail: str | None = None From 38ebf35509c191cc28382e8bb712af3dfb6f40d9 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Tue, 22 Jul 2025 12:18:37 +0200 Subject: [PATCH 43/43] fix: make openapi-spec --- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 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 7c377a655cf..a6ea4a93db0 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 @@ -16099,7 +16099,9 @@ components: format: uuid title: Templateid accessRights: - $ref: '#/components/schemas/FunctionAccessRights' + anyOf: + - $ref: '#/components/schemas/FunctionAccessRights' + - type: 'null' thumbnail: anyOf: - type: string @@ -16114,7 +16116,6 @@ components: - creationDate - lastChangeDate - templateId - - accessRights title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: