diff --git a/packages/models-library/src/models_library/functions.py b/packages/models-library/src/models_library/functions.py index 5e9371cd248..75175b62c63 100644 --- a/packages/models-library/src/models_library/functions.py +++ b/packages/models-library/src/models_library/functions.py @@ -1,3 +1,4 @@ +import datetime from collections.abc import Mapping from enum import Enum from typing import Annotated, Any, Literal, TypeAlias @@ -96,6 +97,7 @@ class FunctionBase(BaseModel): class RegisteredFunctionBase(FunctionBase): uid: FunctionID + created_at: datetime.datetime class ProjectFunction(FunctionBase): @@ -152,6 +154,7 @@ class FunctionJobBase(BaseModel): class RegisteredFunctionJobBase(FunctionJobBase): uid: FunctionJobID + created_at: datetime.datetime class ProjectFunctionJob(FunctionJobBase): @@ -207,6 +210,7 @@ class FunctionJobCollection(BaseModel): class RegisteredFunctionJobCollection(FunctionJobCollection): uid: FunctionJobCollectionID + created_at: datetime.datetime class FunctionJobCollectionStatus(BaseModel): @@ -225,6 +229,7 @@ class FunctionJobDB(BaseModel): class RegisteredFunctionJobDB(FunctionJobDB): uuid: FunctionJobID + created: datetime.datetime class FunctionDB(BaseModel): @@ -239,6 +244,7 @@ class FunctionDB(BaseModel): class RegisteredFunctionDB(FunctionDB): uuid: FunctionID + created: datetime.datetime class FunctionJobCollectionDB(BaseModel): @@ -248,6 +254,7 @@ class FunctionJobCollectionDB(BaseModel): class RegisteredFunctionJobCollectionDB(FunctionJobCollectionDB): uuid: FunctionJobCollectionID + created: datetime.datetime class FunctionIDString(ConstrainedStr): diff --git a/services/api-server/VERSION b/services/api-server/VERSION index a3df0a6959e..ac39a106c48 100644 --- a/services/api-server/VERSION +++ b/services/api-server/VERSION @@ -1 +1 @@ -0.8.0 +0.9.0 diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 19ee0370e76..5a64c1baacb 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "osparc.io public API", "description": "osparc-simcore public API specifications", - "version": "0.8.0" + "version": "0.9.0" }, "paths": { "/v0/meta": { @@ -5286,7 +5286,7 @@ "function_jobs" ], "summary": "List Function Jobs", - "description": "List function jobs\n\nNew in *version 0.8.0*", + "description": "List function jobs\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "list_function_jobs", "security": [ { @@ -5346,7 +5346,7 @@ "function_jobs" ], "summary": "Register Function Job", - "description": "Create function job\n\nNew in *version 0.8.0*", + "description": "Create function job\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "register_function_job", "security": [ { @@ -5431,7 +5431,7 @@ "function_jobs" ], "summary": "Get Function Job", - "description": "Get function job\n\nNew in *version 0.8.0*", + "description": "Get function job\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "get_function_job", "security": [ { @@ -6810,7 +6810,7 @@ "functions" ], "summary": "Register Function", - "description": "Create function\n\nNew in *version 0.8.0*", + "description": "Create function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "register_function", "security": [ { @@ -6903,7 +6903,7 @@ "functions" ], "summary": "List Functions", - "description": "List functions\n\nNew in *version 0.8.0*", + "description": "List functions\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "list_functions", "security": [ { @@ -6965,7 +6965,7 @@ "functions" ], "summary": "Get Function", - "description": "Get function\n\nNew in *version 0.8.0*", + "description": "Get function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "get_function", "security": [ { @@ -7098,7 +7098,7 @@ "functions" ], "summary": "List Function Jobs For Functionid", - "description": "List function jobs for a function\n\nNew in *version 0.8.0*", + "description": "List function jobs for a function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "list_function_jobs_for_functionid", "security": [ { @@ -7170,7 +7170,7 @@ "functions" ], "summary": "Update Function Title", - "description": "Update function\n\nNew in *version 0.8.0*", + "description": "Update function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "update_function_title", "security": [ { @@ -7257,7 +7257,7 @@ "functions" ], "summary": "Update Function Description", - "description": "Update function\n\nNew in *version 0.8.0*", + "description": "Update function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "update_function_description", "security": [ { @@ -7568,7 +7568,7 @@ "functions" ], "summary": "Run Function", - "description": "Run function\n\nNew in *version 0.8.0*", + "description": "Run function\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "run_function", "security": [ { @@ -7664,7 +7664,7 @@ "functions" ], "summary": "Map Function", - "description": "Map function over input parameters\n\nNew in *version 0.8.0*", + "description": "Map function over input parameters\n\nNew in *version 0.8.0*\n\nAdded in *version 0.9.0*: add `created_at` field in the registered function-related objects", "operationId": "map_function", "security": [ { @@ -10094,11 +10094,17 @@ "type": "string", "format": "uuid", "title": "Uid" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" } }, "type": "object", "required": [ - "uid" + "uid", + "created_at" ], "title": "RegisteredFunctionJobCollection" }, @@ -10164,6 +10170,11 @@ "format": "uuid", "title": "Uid" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, "project_id": { "type": "string", "format": "uuid", @@ -10176,6 +10187,7 @@ "output_schema", "default_inputs", "uid", + "created_at", "project_id" ], "title": "RegisteredProjectFunction" @@ -10230,6 +10242,11 @@ "format": "uuid", "title": "Uid" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, "project_job_id": { "type": "string", "format": "uuid", @@ -10242,6 +10259,7 @@ "inputs", "outputs", "uid", + "created_at", "project_job_id" ], "title": "RegisteredProjectFunctionJob" @@ -10308,6 +10326,11 @@ "format": "uuid", "title": "Uid" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, "code_url": { "type": "string", "title": "Code Url" @@ -10319,6 +10342,7 @@ "output_schema", "default_inputs", "uid", + "created_at", "code_url" ], "title": "RegisteredPythonCodeFunction" @@ -10372,6 +10396,11 @@ "type": "string", "format": "uuid", "title": "Uid" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" } }, "type": "object", @@ -10379,7 +10408,8 @@ "function_uid", "inputs", "outputs", - "uid" + "uid", + "created_at" ], "title": "RegisteredPythonCodeFunctionJob" }, @@ -10445,6 +10475,11 @@ "format": "uuid", "title": "Uid" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, "solver_key": { "type": "string", "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", @@ -10462,6 +10497,7 @@ "output_schema", "default_inputs", "uid", + "created_at", "solver_key", "solver_version" ], @@ -10517,6 +10553,11 @@ "format": "uuid", "title": "Uid" }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, "solver_job_id": { "type": "string", "format": "uuid", @@ -10529,6 +10570,7 @@ "inputs", "outputs", "uid", + "created_at", "solver_job_id" ], "title": "RegisteredSolverFunctionJob" diff --git a/services/api-server/setup.cfg b/services/api-server/setup.cfg index 8bb3e9b8f8e..21f72b9aecc 100644 --- a/services/api-server/setup.cfg +++ b/services/api-server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.8.0 +current_version = 0.9.0 commit = True message = services/api-server version: {current_version} → {new_version} tag = False @@ -11,12 +11,12 @@ commit_args = --no-verify asyncio_mode = auto asyncio_default_fixture_loop_scope = function addopts = --strict-markers -markers = +markers = slow: marks tests as slow (deselect with '-m "not slow"') acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows." testit: "marks test to run during development" [mypy] -plugins = +plugins = pydantic.mypy sqlalchemy.ext.mypy.plugin diff --git a/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py index bc975354e53..eb50f8bd0ef 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py @@ -24,7 +24,11 @@ ) from ..dependencies.services import get_api_client from ..dependencies.webserver_rpc import get_wb_api_rpc_client -from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION, create_route_description +from ._constants import ( + FMSG_CHANGELOG_ADDED_IN_VERSION, + FMSG_CHANGELOG_NEW_IN_VERSION, + create_route_description, +) from .function_jobs_routes import function_job_status, get_function_job # pylint: disable=too-many-arguments @@ -41,6 +45,30 @@ }, } +ENDPOINTS = [ + "list_function_job_collections", + "register_function_job_collection", + "get_function_job_collection", + "delete_function_job_collection", +] +CHANGE_LOGS = {} +for endpoint in ENDPOINTS: + CHANGE_LOGS[endpoint] = [ + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.8.0"), + ] + if endpoint in [ + "list_function_job_collections", + "register_function_job_collection", + "get_function_job_collection", + "function_job_collection_list_function_jobs", + ]: + CHANGE_LOGS[endpoint].append( + FMSG_CHANGELOG_ADDED_IN_VERSION.format( + "0.9.0", + "add `created_at` field in the registered function-related objects", + ) + ) + @function_job_collections_router.get( "", diff --git a/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py index ea39cb05ab0..cd461b95fb3 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py @@ -31,7 +31,11 @@ from ..dependencies.webserver_http import get_webserver_session from ..dependencies.webserver_rpc import get_wb_api_rpc_client from . import solvers_jobs, solvers_jobs_read, studies_jobs -from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION, create_route_description +from ._constants import ( + FMSG_CHANGELOG_ADDED_IN_VERSION, + FMSG_CHANGELOG_NEW_IN_VERSION, + create_route_description, +) # pylint: disable=too-many-arguments # pylint: disable=cyclic-import @@ -46,15 +50,33 @@ }, } -FIRST_RELEASE_VERSION = "0.8.0" +ENDPOINTS = [ + "list_function_jobs", + "register_function_job", + "get_function_job", + "delete_function_job", + "function_job_status", + "function_job_outputs", +] +CHANGE_LOGS = {} +for endpoint in ENDPOINTS: + CHANGE_LOGS[endpoint] = [ + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.8.0"), + ] + if endpoint in ["list_function_jobs", "register_function_job", "get_function_job"]: + CHANGE_LOGS[endpoint].append( + FMSG_CHANGELOG_ADDED_IN_VERSION.format( + "0.9.0", + "add `created_at` field in the registered function-related objects", + ) + ) @function_job_router.get( "", response_model=Page[RegisteredFunctionJob], description=create_route_description( - base="List function jobs", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + base="List function jobs", changelog=CHANGE_LOGS["list_function_jobs"] ), ) async def list_function_jobs( @@ -82,7 +104,7 @@ async def list_function_jobs( response_model=RegisteredFunctionJob, description=create_route_description( base="Create function job", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + changelog=CHANGE_LOGS["register_function_job"], ), ) async def register_function_job( @@ -102,7 +124,7 @@ async def register_function_job( responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, description=create_route_description( base="Get function job", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + changelog=CHANGE_LOGS["get_function_job"], ), ) async def get_function_job( @@ -122,7 +144,7 @@ async def get_function_job( responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, description=create_route_description( base="Delete function job", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + changelog=CHANGE_LOGS["delete_function_job"], ), ) async def delete_function_job( @@ -142,7 +164,7 @@ async def delete_function_job( responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, description=create_route_description( base="Get function job status", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + changelog=CHANGE_LOGS["function_job_status"], ), ) async def function_job_status( @@ -222,7 +244,7 @@ async def get_function_from_functionjobid( responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, description=create_route_description( base="Get function job outputs", - changelog=[FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION)], + changelog=CHANGE_LOGS["function_job_outputs"], ), ) async def function_job_outputs( diff --git a/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py index 1cd933a1bb2..4551c313c16 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py @@ -43,7 +43,11 @@ from ..dependencies.webserver_http import get_webserver_session from ..dependencies.webserver_rpc import get_wb_api_rpc_client from . import solvers_jobs, studies_jobs -from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION, create_route_description +from ._constants import ( + FMSG_CHANGELOG_ADDED_IN_VERSION, + FMSG_CHANGELOG_NEW_IN_VERSION, + create_route_description, +) from .function_jobs_routes import register_function_job # pylint: disable=too-many-arguments @@ -58,7 +62,42 @@ }, } -FIRST_RELEASE_VERSION = "0.8.0" + +ENDPOINTS = [ + "register_function", + "get_function", + "list_functions", + "list_function_jobs_for_functionid", + "update_function_title", + "update_function_description", + "get_function_inputschema", + "get_function_outputschema", + "validate_function_inputs", + "run_function", + "delete_function", + "map_function", +] +CHANGE_LOGS = {} +for endpoint in ENDPOINTS: + CHANGE_LOGS[endpoint] = [ + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.8.0"), + ] + if endpoint in [ + "register_function", + "get_function", + "list_functions", + "list_function_jobs_for_functionid", + "update_function_title", + "update_function_description", + "run_function", + "map_function", + ]: + CHANGE_LOGS[endpoint].append( + FMSG_CHANGELOG_ADDED_IN_VERSION.format( + "0.9.0", + "add `created_at` field in the registered function-related objects", + ) + ) @function_router.post( @@ -67,9 +106,7 @@ responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Create function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["register_function"], ), ) async def register_function( @@ -89,9 +126,7 @@ async def register_function( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Get function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["get_function"], ), ) async def get_function( @@ -110,9 +145,7 @@ async def get_function( response_model=Page[RegisteredFunction], description=create_route_description( base="List functions", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.8.0"), - ], + changelog=CHANGE_LOGS["list_functions"], ), ) async def list_functions( @@ -140,9 +173,7 @@ async def list_functions( response_model=Page[RegisteredFunctionJob], description=create_route_description( base="List function jobs for a function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["list_function_jobs_for_functionid"], ), ) async def list_function_jobs_for_functionid( @@ -173,9 +204,7 @@ async def list_function_jobs_for_functionid( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Update function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["update_function_title"], ), ) async def update_function_title( @@ -200,9 +229,7 @@ async def update_function_title( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Update function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["update_function_description"], ), ) async def update_function_description( @@ -244,9 +271,7 @@ def _join_inputs( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Get function input schema", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["get_function_inputschema"], ), ) async def get_function_inputschema( @@ -267,9 +292,7 @@ async def get_function_inputschema( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Get function output schema", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["get_function_outputschema"], ), ) async def get_function_outputschema( @@ -293,9 +316,7 @@ async def get_function_outputschema( }, description=create_route_description( base="Validate inputs against the function's input schema", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["validate_function_inputs"], ), ) async def validate_function_inputs( @@ -333,9 +354,7 @@ async def validate_function_inputs( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Run function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["run_function"], ), ) async def run_function( # noqa: PLR0913 @@ -470,9 +489,7 @@ async def run_function( # noqa: PLR0913 responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Delete function", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["delete_function"], ), ) async def delete_function( @@ -500,9 +517,7 @@ async def delete_function( responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, description=create_route_description( base="Map function over input parameters", - changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format(FIRST_RELEASE_VERSION), - ], + changelog=CHANGE_LOGS["map_function"], ), ) async def map_function( # noqa: PLR0913 diff --git a/services/api-server/src/simcore_service_api_server/api/routes/programs.py b/services/api-server/src/simcore_service_api_server/api/routes/programs.py index 0f3de634193..2ccd9d5ed7b 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/programs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/programs.py @@ -42,10 +42,10 @@ description=create_route_description( base="Lists the latest of all available programs", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10 ) async def list_programs( program_service: Annotated[ProgramService, Depends(get_program_service)], @@ -77,10 +77,10 @@ async def list_programs( description=create_route_description( base="Lists the latest of all available programs", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10 ) async def list_program_history( program_key: ProgramKeyId, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py index c162e359422..f6dee01f569 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py @@ -83,10 +83,10 @@ async def list_solvers( description=create_route_description( base="Lists all available solvers (paginated)", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9-rc1"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10-rc1"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10-rc1 ) async def list_all_solvers_paginated( page_params: Annotated[PaginationParams, Depends()], @@ -246,10 +246,10 @@ async def list_solver_releases( description=create_route_description( base="Lists all releases of a give solver (paginated)", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9-rc1"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10-rc1"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10-rc1 ) async def list_solver_releases_paginated( solver_key: SolverKeyId, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_read.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_read.py index 7210b9b2c97..f46ea4d9892 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_read.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_read.py @@ -130,10 +130,10 @@ description=create_route_description( base="List of all jobs created for any released solver (paginated)", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9-rc1"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10-rc1"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10-rc1 ) async def list_all_solvers_jobs( page_params: Annotated[PaginationParams, Depends()], diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index c19845df2aa..11e8a22e4a7 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -52,17 +52,13 @@ from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client, get_study_service from ..dependencies.webserver_http import AuthSession, get_webserver_session -from ..dependencies.webserver_rpc import ( - get_wb_api_rpc_client, -) +from ..dependencies.webserver_rpc import get_wb_api_rpc_client from ._constants import ( FMSG_CHANGELOG_CHANGED_IN_VERSION, FMSG_CHANGELOG_NEW_IN_VERSION, create_route_description, ) -from .solvers_jobs import ( - JOBS_STATUS_CODES, -) +from .solvers_jobs import JOBS_STATUS_CODES # pylint: disable=too-many-arguments @@ -87,10 +83,10 @@ def _compose_job_resource_name(study_key, job_id) -> str: description=create_route_description( base="List of all jobs created for a given study (paginated)", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9-rc1"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10-rc1"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10-rc1 ) async def list_study_jobs( study_id: StudyID, @@ -217,10 +213,10 @@ async def create_study_job( description=create_route_description( base="Gets a jobs for a given study", changelog=[ - FMSG_CHANGELOG_NEW_IN_VERSION.format("0.9-rc1"), + FMSG_CHANGELOG_NEW_IN_VERSION.format("0.10-rc1"), ], ), - include_in_schema=False, # TO BE RELEASED in 0.9 + include_in_schema=False, # TO BE RELEASED in 0.10-rc1 ) async def get_study_job( study_id: StudyID, diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index e71a2a22774..891fdc533f4 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -5,6 +5,7 @@ # pylint: disable=no-self-use # pylint: disable=cyclic-import +import datetime from collections.abc import Callable from typing import Any from uuid import uuid4 @@ -119,7 +120,13 @@ def mock_function( @pytest.fixture def mock_registered_function(mock_function: Function) -> RegisteredFunction: - return RegisteredProjectFunction(**{**mock_function.dict(), "uid": str(uuid4())}) + return RegisteredProjectFunction( + **{ + **mock_function.dict(), + "uid": str(uuid4()), + "created_at": datetime.datetime.now(datetime.UTC), + } + ) @pytest.fixture @@ -141,7 +148,11 @@ def mock_registered_function_job( mock_function_job: FunctionJob, ) -> RegisteredFunctionJob: return RegisteredProjectFunctionJob( - **{**mock_function_job.dict(), "uid": str(uuid4())} + **{ + **mock_function_job.dict(), + "uid": str(uuid4()), + "created_at": datetime.datetime.now(datetime.UTC), + } ) @@ -165,7 +176,11 @@ def mock_registered_function_job_collection( mock_function_job_collection: FunctionJobCollection, ) -> RegisteredFunctionJobCollection: return RegisteredFunctionJobCollection( - **{**mock_function_job_collection.model_dump(), "uid": str(uuid4())} + **{ + **mock_function_job_collection.model_dump(), + "uid": str(uuid4()), + "created_at": datetime.datetime.now(datetime.UTC), + } ) diff --git a/services/api-server/tests/unit/api_functions/test_api_routers_functions.py b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py index 743a83c5cc2..dcac6487618 100644 --- a/services/api-server/tests/unit/api_functions/test_api_routers_functions.py +++ b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py @@ -1,6 +1,7 @@ # pylint: disable=unused-argument # pylint: disable=redefined-outer-name +import datetime from collections.abc import Callable from typing import Any from uuid import uuid4 @@ -27,12 +28,11 @@ async def test_register_function( mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], mock_function: ProjectFunction, auth: httpx.BasicAuth, + mock_registered_function: RegisteredProjectFunction, ) -> None: - registered_function = RegisteredProjectFunction( - **{**mock_function.model_dump(), "uid": str(uuid4())} + mock_handler_in_functions_rpc_interface( + "register_function", mock_registered_function ) - - mock_handler_in_functions_rpc_interface("register_function", registered_function) response = await client.post( f"{API_VTAG}/functions", json=mock_function.model_dump(mode="json"), auth=auth ) @@ -40,7 +40,7 @@ async def test_register_function( data = response.json() returned_function = RegisteredProjectFunction.model_validate(data) assert returned_function.uid is not None - assert returned_function == registered_function + assert returned_function == mock_registered_function async def test_register_function_invalid( @@ -391,6 +391,7 @@ async def test_register_function_job_collection( { **mock_function_job_collection.model_dump(), "uid": str(uuid4()), + "created_at": datetime.datetime.now(datetime.UTC), } ) ) @@ -425,6 +426,7 @@ async def test_get_function_job_collection( "title": "Test Collection", "description": "A test function job collection", "job_ids": [str(uuid4()), str(uuid4())], + "created_at": datetime.datetime.now(datetime.UTC), } ) ) @@ -456,6 +458,7 @@ async def test_list_function_job_collections( "title": "Test Collection", "description": "A test function job collection", "job_ids": [str(uuid4()), str(uuid4())], + "created_at": datetime.datetime.now(datetime.UTC), } ) ) diff --git a/services/web/server/VERSION b/services/web/server/VERSION index 328185caaeb..4a46fb5b7a1 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.67.0 +0.68.0 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 13e5b2c3d4e..8400824ccb5 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.67.0 +current_version = 0.68.0 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False 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 eadb6dcc1f9..8addd2a9eac 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 @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.67.0 + version: 0.68.0 servers: - url: '' description: webserver @@ -15462,6 +15462,10 @@ components: type: string format: uuid title: Uid + createdAt: + type: string + format: date-time + title: Createdat projectId: type: string format: uuid @@ -15472,6 +15476,7 @@ components: - outputSchema - defaultInputs - uid + - createdAt - projectId title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: @@ -15514,6 +15519,10 @@ components: type: string format: uuid title: Uid + createdAt: + type: string + format: date-time + title: Createdat solverKey: type: string pattern: ^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$ @@ -15528,6 +15537,7 @@ components: - outputSchema - defaultInputs - uid + - createdAt - solverKey - solverVersion title: RegisteredSolverFunctionGet 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 e6d261d66c8..fd29f603d7c 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 @@ -112,6 +112,7 @@ async def register_function_job_collection( title=registered_function_job_collection.title, description=registered_function_job_collection.description, job_ids=registered_job_ids, + created_at=registered_function_job_collection.created, ) @@ -174,6 +175,7 @@ async def get_function_job_collection( title=returned_function_job_collection.title, description=returned_function_job_collection.description, job_ids=returned_job_ids, + created_at=returned_function_job_collection.created, ) @@ -248,6 +250,7 @@ async def list_function_job_collections( title=function_job_collection.title, description=function_job_collection.description, job_ids=job_ids, + created_at=function_job_collection.created, ) for function_job_collection, job_ids in returned_function_job_collections ], page @@ -372,6 +375,7 @@ async def find_cached_function_jobs( project_job_id=returned_function_job.class_specific_data[ "project_job_id" ], + created_at=returned_function_job.created, ) ) elif returned_function_job.function_class == FunctionClass.SOLVER: @@ -386,6 +390,7 @@ async def find_cached_function_jobs( solver_job_id=returned_function_job.class_specific_data[ "solver_job_id" ], + created_at=returned_function_job.created, ) ) else: @@ -442,6 +447,7 @@ def _decode_function( output_schema=function.output_schema, project_id=function.class_specific_data["project_id"], default_inputs=function.default_inputs, + created_at=function.created, ) if function.function_class == FunctionClass.SOLVER: @@ -454,6 +460,7 @@ def _decode_function( solver_key=function.class_specific_data["solver_key"], solver_version=function.class_specific_data["solver_version"], default_inputs=function.default_inputs, + created_at=function.created, ) raise UnsupportedFunctionClassError(function_class=function.function_class) @@ -530,6 +537,7 @@ def _decode_functionjob( inputs=functionjob_db.inputs, outputs=functionjob_db.outputs, project_job_id=functionjob_db.class_specific_data["project_job_id"], + created_at=functionjob_db.created, ) if functionjob_db.function_class == FunctionClass.SOLVER: @@ -541,6 +549,7 @@ def _decode_functionjob( inputs=functionjob_db.inputs, outputs=functionjob_db.outputs, solver_job_id=functionjob_db.class_specific_data["solver_job_id"], + created_at=functionjob_db.created, ) raise UnsupportedFunctionJobClassError( 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 d6859c089b4..98dfa687c23 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 @@ -3,10 +3,17 @@ # pylint:disable=redefined-outer-name -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Awaitable, Callable +from uuid import uuid4 import pytest from aiohttp.test_utils import TestClient +from models_library.api_schemas_webserver.functions import ( + Function, + JSONFunctionInputSchema, + JSONFunctionOutputSchema, + ProjectFunction, +) from models_library.products import ProductName from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict @@ -15,21 +22,64 @@ from servicelib.rabbitmq.rpc_interfaces.webserver.functions import ( functions_rpc_interface as functions_rpc, ) +from settings_library.rabbit import RabbitSettings +from simcore_service_webserver.application_settings import ApplicationSettings @pytest.fixture def app_environment( + rabbit_service: RabbitSettings, app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatch, -): - return setenvs_from_dict( +) -> EnvVarsDict: + new_envs = setenvs_from_dict( monkeypatch, { - **app_environment, # WARNING: AFTER env_devel_dict because HOST are set to 127.0.0.1 in here + **app_environment, + "RABBIT_HOST": rabbit_service.RABBIT_HOST, + "RABBIT_PORT": f"{rabbit_service.RABBIT_PORT}", + "RABBIT_USER": rabbit_service.RABBIT_USER, + "RABBIT_SECURE": f"{rabbit_service.RABBIT_SECURE}", + "RABBIT_PASSWORD": rabbit_service.RABBIT_PASSWORD.get_secret_value(), + "WEBSERVER_DEV_FEATURES_ENABLED": "1", "WEBSERVER_FUNCTIONS": "1", }, ) + settings = ApplicationSettings.create_from_envs() + assert settings.WEBSERVER_RABBITMQ + + return new_envs + + +@pytest.fixture +async def rpc_client( + rabbitmq_rpc_client: Callable[[str], Awaitable[RabbitMQRPCClient]], +) -> RabbitMQRPCClient: + return await rabbitmq_rpc_client("client") + + +@pytest.fixture +def mock_function() -> Function: + return ProjectFunction( + title="Test Function", + description="A test function", + input_schema=JSONFunctionInputSchema( + schema_content={ + "type": "object", + "properties": {"input1": {"type": "string"}}, + } + ), + output_schema=JSONFunctionOutputSchema( + schema_content={ + "type": "object", + "properties": {"output1": {"type": "string"}}, + } + ), + project_id=uuid4(), + default_inputs=None, + ) + @pytest.fixture async def other_logged_user( diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_job_collections_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_job_collections_controller_rpc.py new file mode 100644 index 00000000000..d784a9fad3b --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_job_collections_controller_rpc.py @@ -0,0 +1,345 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +import datetime +from uuid import uuid4 + +import pytest +from aiohttp.test_utils import TestClient +from common_library.users_enums import UserRole +from models_library.api_schemas_webserver.functions import ( + FunctionIDString, + FunctionJobCollection, + ProjectFunction, + ProjectFunctionJob, +) +from models_library.functions import FunctionJobCollectionsListFilters +from models_library.functions_errors import ( + FunctionJobCollectionReadAccessDeniedError, + FunctionJobIDNotFoundError, +) +from models_library.products import ProductName +from pytest_simcore.helpers.webserver_login import UserInfoDict +from servicelib.rabbitmq import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.functions import ( + functions_rpc_interface as functions_rpc, +) + +pytest_simcore_core_services_selection = ["rabbit"] + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_function_job_collection( + client: TestClient, + mock_function: ProjectFunction, + rpc_client: RabbitMQRPCClient, + logged_user: UserInfoDict, + other_logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + function_job_ids = [] + for _ in range(3): + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=registered_function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_job.uid is not None + function_job_ids.append(registered_job.uid) + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + # Register the function job collection + registered_collection = await functions_rpc.register_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection=function_job_collection, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_collection.uid is not None + assert registered_collection.created_at - datetime.datetime.now( + datetime.UTC + ) < datetime.timedelta(seconds=60) + + # Get the function job collection + retrieved_collection = await functions_rpc.get_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection_id=registered_collection.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert retrieved_collection.uid == registered_collection.uid + assert registered_collection.job_ids == function_job_ids + + # Test denied access for another user + with pytest.raises(FunctionJobCollectionReadAccessDeniedError): + await functions_rpc.get_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection_id=registered_collection.uid, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + + # Test denied access for another product + with pytest.raises(FunctionJobCollectionReadAccessDeniedError): + await functions_rpc.get_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection_id=registered_collection.uid, + user_id=other_logged_user["id"], + product_name="this_is_not_osparc", + ) + + # Attempt to delete the function job collection by another user + with pytest.raises(FunctionJobCollectionReadAccessDeniedError): + await functions_rpc.delete_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection_id=registered_collection.uid, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + + await functions_rpc.delete_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection_id=registered_collection.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + # Attempt to retrieve the deleted collection + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_collection.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_function_job_collections( + client: TestClient, + mock_function: ProjectFunction, + rpc_client: RabbitMQRPCClient, + clean_functions: None, + clean_function_job_collections: None, + logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # List function job collections when none are registered + collections, page_meta = await functions_rpc.list_function_job_collections( + rabbitmq_rpc_client=rpc_client, + pagination_limit=10, + pagination_offset=0, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the list is empty + assert page_meta.count == 0 + assert page_meta.total == 0 + assert page_meta.offset == 0 + assert len(collections) == 0 + + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + # Create a function job collection + function_job_ids = [] + for _ in range(3): + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=registered_function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_job.uid is not None + function_job_ids.append(registered_job.uid) + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + # Register the function job collection + registered_collections = [ + await functions_rpc.register_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection=function_job_collection, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + for _ in range(3) + ] + assert all( + registered_collection.uid is not None + for registered_collection in registered_collections + ) + + # List function job collections + collections, page_params = await functions_rpc.list_function_job_collections( + rabbitmq_rpc_client=rpc_client, + pagination_limit=2, + pagination_offset=1, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the list contains the registered collection + assert page_params.count == 2 + assert page_params.total == 3 + assert page_params.offset == 1 + assert len(collections) == 2 + assert collections[0].uid in [ + collection.uid for collection in registered_collections + ] + assert collections[1].uid in [ + collection.uid for collection in registered_collections + ] + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_function_job_collections_filtered_function_id( + client: TestClient, + rpc_client: RabbitMQRPCClient, + mock_function: ProjectFunction, + clean_functions: None, + clean_function_job_collections: None, + logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + other_registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + registered_collections = [] + for coll_i in range(5): + if coll_i < 3: + function_id = registered_function.uid + else: + function_id = other_registered_function.uid + # Create a function job collection + function_job_ids = [] + for _ in range(3): + registered_function_job = ProjectFunctionJob( + function_uid=function_id, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=registered_function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_job.uid is not None + function_job_ids.append(registered_job.uid) + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + # Register the function job collection + registered_collection = await functions_rpc.register_function_job_collection( + rabbitmq_rpc_client=rpc_client, + function_job_collection=function_job_collection, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + registered_collections.append(registered_collection) + + # List function job collections with a specific function ID + collections, page_meta = await functions_rpc.list_function_job_collections( + rabbitmq_rpc_client=rpc_client, + pagination_limit=10, + pagination_offset=1, + filters=FunctionJobCollectionsListFilters( + has_function_id=FunctionIDString(registered_function.uid) + ), + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the list contains the registered collection + assert page_meta.count == 2 + assert page_meta.total == 3 + assert page_meta.offset == 1 + + assert len(collections) == 2 + assert collections[0].uid in [ + collection.uid for collection in registered_collections + ] + assert collections[1].uid in [ + collection.uid for collection in registered_collections + ] diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_jobs_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_jobs_controller_rpc.py new file mode 100644 index 00000000000..f7b6b16e2df --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_jobs_controller_rpc.py @@ -0,0 +1,353 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +import datetime +from uuid import uuid4 + +import pytest +from aiohttp.test_utils import TestClient +from common_library.users_enums import UserRole +from models_library.api_schemas_webserver.functions import ( + ProjectFunction, + ProjectFunctionJob, +) +from models_library.functions_errors import ( + FunctionJobIDNotFoundError, + FunctionJobReadAccessDeniedError, +) +from models_library.products import ProductName +from pytest_simcore.helpers.webserver_login import UserInfoDict +from servicelib.rabbitmq import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.functions import ( + functions_rpc_interface as functions_rpc, +) + +pytest_simcore_core_services_selection = ["rabbit"] + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_register_get_delete_function_job( + client: TestClient, + rpc_client: RabbitMQRPCClient, + mock_function: ProjectFunction, + logged_user: UserInfoDict, + other_logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the registered job matches the input job + assert registered_job.function_uid == function_job.function_uid + assert registered_job.inputs == function_job.inputs + assert registered_job.outputs == function_job.outputs + assert registered_job.created_at - datetime.datetime.now( + datetime.UTC + ) < datetime.timedelta(seconds=60) + + # Retrieve the function job using its ID + retrieved_job = await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the retrieved job matches the registered job + assert retrieved_job.function_uid == registered_job.function_uid + assert retrieved_job.inputs == registered_job.inputs + assert retrieved_job.outputs == registered_job.outputs + + # Test denied access for another user + with pytest.raises(FunctionJobReadAccessDeniedError): + await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + + # Test denied access for anothe product + with pytest.raises(FunctionJobReadAccessDeniedError): + await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=other_logged_user["id"], + product_name="this_is_not_osparc", + ) + + with pytest.raises(FunctionJobReadAccessDeniedError): + # Attempt to delete the function job by another user + await functions_rpc.delete_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + + # Delete the function job using its ID + await functions_rpc.delete_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Attempt to retrieve the deleted job + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=registered_job.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_function_job_not_found( + client: TestClient, + rpc_client: RabbitMQRPCClient, + logged_user: UserInfoDict, + osparc_product_name: ProductName, + clean_functions: None, +): + # Attempt to retrieve a function job that does not exist + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job( + rabbitmq_rpc_client=rpc_client, + function_job_id=uuid4(), + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_function_jobs( + client: TestClient, + rpc_client: RabbitMQRPCClient, + mock_function: ProjectFunction, + logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # List function jobs + jobs, _ = await functions_rpc.list_function_jobs( + rabbitmq_rpc_client=rpc_client, + pagination_limit=10, + pagination_offset=0, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the list contains the registered job + assert len(jobs) > 0 + assert any(j.uid == registered_job.uid for j in jobs) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_function_jobs_for_functionid( + client: TestClient, + rpc_client: RabbitMQRPCClient, + mock_function: ProjectFunction, + logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + first_registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + second_registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + first_registered_function_jobs = [] + second_registered_function_jobs = [] + for i_job in range(6): + if i_job < 3: + function_job = ProjectFunctionJob( + function_uid=first_registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + first_registered_function_jobs.append( + await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + ) + else: + function_job = ProjectFunctionJob( + function_uid=second_registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + second_registered_function_jobs.append( + await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + ) + + # List function jobs for a specific function ID + jobs, _ = await functions_rpc.list_function_jobs( + rabbitmq_rpc_client=rpc_client, + pagination_limit=10, + pagination_offset=0, + filter_by_function_id=first_registered_function.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the list contains the registered job + assert len(jobs) > 0 + assert len(jobs) == 3 + assert all(j.function_uid == first_registered_function.uid for j in jobs) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_find_cached_function_jobs( + client: TestClient, + rpc_client: RabbitMQRPCClient, + logged_user: UserInfoDict, + other_logged_user: UserInfoDict, + osparc_product_name: ProductName, + mock_function: ProjectFunction, + clean_functions: None, +): + + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + registered_function_jobs = [] + for value in range(5): + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": value if value < 4 else 1}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + rabbitmq_rpc_client=rpc_client, + function_job=function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + registered_function_jobs.append(registered_job) + + # Find cached function jobs + cached_jobs = await functions_rpc.find_cached_function_jobs( + rabbitmq_rpc_client=rpc_client, + function_id=registered_function.uid, + inputs={"input1": 1}, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the cached jobs contain the registered job + assert cached_jobs is not None + assert len(cached_jobs) == 2 + assert {job.uid for job in cached_jobs} == { + registered_function_jobs[1].uid, + registered_function_jobs[4].uid, + } + + cached_jobs = await functions_rpc.find_cached_function_jobs( + rabbitmq_rpc_client=rpc_client, + function_id=registered_function.uid, + inputs={"input1": 1}, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the cached jobs does not contain the registered job for the other user + assert cached_jobs is None 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 c01111c877d..5f4650a3f1e 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 @@ -21,6 +21,8 @@ from servicelib.aiohttp import status from simcore_service_webserver.db.models import UserRole +pytest_simcore_core_services_selection = ["rabbit"] + @pytest.fixture def mock_function() -> dict[str, Any]: diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py index 86c32d56dde..c6f2fecc710 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py @@ -1,98 +1,33 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument -from collections.abc import Awaitable, Callable +import datetime from uuid import uuid4 import pytest from aiohttp.test_utils import TestClient from common_library.users_enums import UserRole from models_library.api_schemas_webserver.functions import ( - Function, - FunctionIDString, - FunctionJobCollection, JSONFunctionInputSchema, JSONFunctionOutputSchema, ProjectFunction, - ProjectFunctionJob, ) -from models_library.functions import FunctionJobCollectionsListFilters # import simcore_service_webserver.functions._functions_controller_rpc as functions_rpc from models_library.functions_errors import ( FunctionIDNotFoundError, - FunctionJobCollectionReadAccessDeniedError, - FunctionJobIDNotFoundError, - FunctionJobReadAccessDeniedError, FunctionReadAccessDeniedError, ) from models_library.products import ProductName -from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict -from pytest_simcore.helpers.typing_env import EnvVarsDict from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.rabbitmq import RabbitMQRPCClient from servicelib.rabbitmq.rpc_interfaces.webserver.functions import ( functions_rpc_interface as functions_rpc, ) -from settings_library.rabbit import RabbitSettings -from simcore_service_webserver.application_settings import ApplicationSettings pytest_simcore_core_services_selection = ["rabbit"] -@pytest.fixture -def app_environment( - rabbit_service: RabbitSettings, - app_environment: EnvVarsDict, - monkeypatch: pytest.MonkeyPatch, -) -> EnvVarsDict: - new_envs = setenvs_from_dict( - monkeypatch, - { - **app_environment, - "RABBIT_HOST": rabbit_service.RABBIT_HOST, - "RABBIT_PORT": f"{rabbit_service.RABBIT_PORT}", - "RABBIT_USER": rabbit_service.RABBIT_USER, - "RABBIT_SECURE": f"{rabbit_service.RABBIT_SECURE}", - "RABBIT_PASSWORD": rabbit_service.RABBIT_PASSWORD.get_secret_value(), - }, - ) - - settings = ApplicationSettings.create_from_envs() - assert settings.WEBSERVER_RABBITMQ - - return new_envs - - -@pytest.fixture -async def rpc_client( - rabbitmq_rpc_client: Callable[[str], Awaitable[RabbitMQRPCClient]], -) -> RabbitMQRPCClient: - return await rabbitmq_rpc_client("client") - - -@pytest.fixture -def mock_function() -> Function: - return ProjectFunction( - title="Test Function", - description="A test function", - input_schema=JSONFunctionInputSchema( - schema_content={ - "type": "object", - "properties": {"input1": {"type": "string"}}, - } - ), - output_schema=JSONFunctionOutputSchema( - schema_content={ - "type": "object", - "properties": {"output1": {"type": "string"}}, - } - ), - project_id=uuid4(), - default_inputs=None, - ) - - @pytest.mark.parametrize( "user_role", [UserRole.USER], @@ -114,6 +49,10 @@ async def test_register_get_delete_function( product_name=osparc_product_name, ) assert registered_function.uid is not None + assert registered_function.created_at - datetime.datetime.now( + datetime.UTC + ) < datetime.timedelta(seconds=60) + # Retrieve the function from the repository to verify it was saved saved_function = await functions_rpc.get_function( rabbitmq_rpc_client=rpc_client, @@ -122,14 +61,6 @@ async def test_register_get_delete_function( product_name=osparc_product_name, ) - with pytest.raises(FunctionReadAccessDeniedError): - await functions_rpc.get_function( - rabbitmq_rpc_client=rpc_client, - function_id=registered_function.uid, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - # Assert the saved function matches the input function assert saved_function.uid is not None assert saved_function.title == mock_function.title @@ -138,6 +69,7 @@ async def test_register_get_delete_function( # Ensure saved_function is of type ProjectFunction before accessing project_id assert isinstance(saved_function, ProjectFunction) assert saved_function.project_id == mock_function.project_id + assert saved_function.created_at == registered_function.created_at # Assert the returned function matches the expected result assert registered_function.title == mock_function.title @@ -145,6 +77,14 @@ async def test_register_get_delete_function( assert isinstance(registered_function, ProjectFunction) assert registered_function.project_id == mock_function.project_id + with pytest.raises(FunctionReadAccessDeniedError): + await functions_rpc.get_function( + rabbitmq_rpc_client=rpc_client, + function_id=registered_function.uid, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + ) + with pytest.raises(FunctionReadAccessDeniedError): # Attempt to delete the function by another user await functions_rpc.delete_function( @@ -557,642 +497,3 @@ async def test_delete_function( product_name=osparc_product_name, ) assert registered_function.uid is not None - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_register_get_delete_function_job( - client: TestClient, - rpc_client: RabbitMQRPCClient, - mock_function: ProjectFunction, - logged_user: UserInfoDict, - other_logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_function.uid is not None - - function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the registered job matches the input job - assert registered_job.function_uid == function_job.function_uid - assert registered_job.inputs == function_job.inputs - assert registered_job.outputs == function_job.outputs - - # Retrieve the function job using its ID - retrieved_job = await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the retrieved job matches the registered job - assert retrieved_job.function_uid == registered_job.function_uid - assert retrieved_job.inputs == registered_job.inputs - assert retrieved_job.outputs == registered_job.outputs - - # Test denied access for another user - with pytest.raises(FunctionJobReadAccessDeniedError): - await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - - # Test denied access for anothe product - with pytest.raises(FunctionJobReadAccessDeniedError): - await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=other_logged_user["id"], - product_name="this_is_not_osparc", - ) - - with pytest.raises(FunctionJobReadAccessDeniedError): - # Attempt to delete the function job by another user - await functions_rpc.delete_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - - # Delete the function job using its ID - await functions_rpc.delete_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Attempt to retrieve the deleted job - with pytest.raises(FunctionJobIDNotFoundError): - await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_job.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_get_function_job_not_found( - client: TestClient, - rpc_client: RabbitMQRPCClient, - logged_user: UserInfoDict, - osparc_product_name: ProductName, - clean_functions: None, -): - # Attempt to retrieve a function job that does not exist - with pytest.raises(FunctionJobIDNotFoundError): - await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=uuid4(), - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_list_function_jobs( - client: TestClient, - rpc_client: RabbitMQRPCClient, - mock_function: ProjectFunction, - logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_function.uid is not None - - function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # List function jobs - jobs, _ = await functions_rpc.list_function_jobs( - rabbitmq_rpc_client=rpc_client, - pagination_limit=10, - pagination_offset=0, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the list contains the registered job - assert len(jobs) > 0 - assert any(j.uid == registered_job.uid for j in jobs) - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_list_function_jobs_for_functionid( - client: TestClient, - rpc_client: RabbitMQRPCClient, - mock_function: ProjectFunction, - logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # Register the function first - first_registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - second_registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - first_registered_function_jobs = [] - second_registered_function_jobs = [] - for i_job in range(6): - if i_job < 3: - function_job = ProjectFunctionJob( - function_uid=first_registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - first_registered_function_jobs.append( - await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - ) - else: - function_job = ProjectFunctionJob( - function_uid=second_registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - second_registered_function_jobs.append( - await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - ) - - # List function jobs for a specific function ID - jobs, _ = await functions_rpc.list_function_jobs( - rabbitmq_rpc_client=rpc_client, - pagination_limit=10, - pagination_offset=0, - filter_by_function_id=first_registered_function.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the list contains the registered job - assert len(jobs) > 0 - assert len(jobs) == 3 - assert all(j.function_uid == first_registered_function.uid for j in jobs) - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_function_job_collection( - client: TestClient, - mock_function: ProjectFunction, - rpc_client: RabbitMQRPCClient, - logged_user: UserInfoDict, - other_logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_function.uid is not None - - registered_function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - function_job_ids = [] - for _ in range(3): - registered_function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=registered_function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_job.uid is not None - function_job_ids.append(registered_job.uid) - - function_job_collection = FunctionJobCollection( - title="Test Function Job Collection", - description="A test function job collection", - job_ids=function_job_ids, - ) - - # Register the function job collection - registered_collection = await functions_rpc.register_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection=function_job_collection, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_collection.uid is not None - - # Get the function job collection - retrieved_collection = await functions_rpc.get_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection_id=registered_collection.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert retrieved_collection.uid == registered_collection.uid - assert registered_collection.job_ids == function_job_ids - - # Test denied access for another user - with pytest.raises(FunctionJobCollectionReadAccessDeniedError): - await functions_rpc.get_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection_id=registered_collection.uid, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - - # Test denied access for another product - with pytest.raises(FunctionJobCollectionReadAccessDeniedError): - await functions_rpc.get_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection_id=registered_collection.uid, - user_id=other_logged_user["id"], - product_name="this_is_not_osparc", - ) - - # Attempt to delete the function job collection by another user - with pytest.raises(FunctionJobCollectionReadAccessDeniedError): - await functions_rpc.delete_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection_id=registered_collection.uid, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - - await functions_rpc.delete_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection_id=registered_collection.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - # Attempt to retrieve the deleted collection - with pytest.raises(FunctionJobIDNotFoundError): - await functions_rpc.get_function_job( - rabbitmq_rpc_client=rpc_client, - function_job_id=registered_collection.uid, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_list_function_job_collections( - client: TestClient, - mock_function: ProjectFunction, - rpc_client: RabbitMQRPCClient, - clean_functions: None, - clean_function_job_collections: None, - logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # List function job collections when none are registered - collections, page_meta = await functions_rpc.list_function_job_collections( - rabbitmq_rpc_client=rpc_client, - pagination_limit=10, - pagination_offset=0, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the list is empty - assert page_meta.count == 0 - assert page_meta.total == 0 - assert page_meta.offset == 0 - assert len(collections) == 0 - - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_function.uid is not None - - # Create a function job collection - function_job_ids = [] - for _ in range(3): - registered_function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=registered_function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_job.uid is not None - function_job_ids.append(registered_job.uid) - - function_job_collection = FunctionJobCollection( - title="Test Function Job Collection", - description="A test function job collection", - job_ids=function_job_ids, - ) - - # Register the function job collection - registered_collections = [ - await functions_rpc.register_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection=function_job_collection, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - for _ in range(3) - ] - assert all( - registered_collection.uid is not None - for registered_collection in registered_collections - ) - - # List function job collections - collections, page_params = await functions_rpc.list_function_job_collections( - rabbitmq_rpc_client=rpc_client, - pagination_limit=2, - pagination_offset=1, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the list contains the registered collection - assert page_params.count == 2 - assert page_params.total == 3 - assert page_params.offset == 1 - assert len(collections) == 2 - assert collections[0].uid in [ - collection.uid for collection in registered_collections - ] - assert collections[1].uid in [ - collection.uid for collection in registered_collections - ] - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_list_function_job_collections_filtered_function_id( - client: TestClient, - rpc_client: RabbitMQRPCClient, - mock_function: ProjectFunction, - clean_functions: None, - clean_function_job_collections: None, - logged_user: UserInfoDict, - osparc_product_name: ProductName, -): - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - other_registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - registered_collections = [] - for coll_i in range(5): - if coll_i < 3: - function_id = registered_function.uid - else: - function_id = other_registered_function.uid - # Create a function job collection - function_job_ids = [] - for _ in range(3): - registered_function_job = ProjectFunctionJob( - function_uid=function_id, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": "value1"}, - outputs={"output1": "result1"}, - ) - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=registered_function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - assert registered_job.uid is not None - function_job_ids.append(registered_job.uid) - - function_job_collection = FunctionJobCollection( - title="Test Function Job Collection", - description="A test function job collection", - job_ids=function_job_ids, - ) - - # Register the function job collection - registered_collection = await functions_rpc.register_function_job_collection( - rabbitmq_rpc_client=rpc_client, - function_job_collection=function_job_collection, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - registered_collections.append(registered_collection) - - # List function job collections with a specific function ID - collections, page_meta = await functions_rpc.list_function_job_collections( - rabbitmq_rpc_client=rpc_client, - pagination_limit=10, - pagination_offset=1, - filters=FunctionJobCollectionsListFilters( - has_function_id=FunctionIDString(registered_function.uid) - ), - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the list contains the registered collection - assert page_meta.count == 2 - assert page_meta.total == 3 - assert page_meta.offset == 1 - - assert len(collections) == 2 - assert collections[0].uid in [ - collection.uid for collection in registered_collections - ] - assert collections[1].uid in [ - collection.uid for collection in registered_collections - ] - - -@pytest.mark.parametrize( - "user_role", - [UserRole.USER], -) -async def test_find_cached_function_jobs( - client: TestClient, - rpc_client: RabbitMQRPCClient, - logged_user: UserInfoDict, - other_logged_user: UserInfoDict, - osparc_product_name: ProductName, - mock_function: ProjectFunction, - clean_functions: None, -): - - # Register the function first - registered_function = await functions_rpc.register_function( - rabbitmq_rpc_client=rpc_client, - function=mock_function, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - registered_function_jobs = [] - for value in range(5): - function_job = ProjectFunctionJob( - function_uid=registered_function.uid, - title="Test Function Job", - description="A test function job", - project_job_id=uuid4(), - inputs={"input1": value if value < 4 else 1}, - outputs={"output1": "result1"}, - ) - - # Register the function job - registered_job = await functions_rpc.register_function_job( - rabbitmq_rpc_client=rpc_client, - function_job=function_job, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - registered_function_jobs.append(registered_job) - - # Find cached function jobs - cached_jobs = await functions_rpc.find_cached_function_jobs( - rabbitmq_rpc_client=rpc_client, - function_id=registered_function.uid, - inputs={"input1": 1}, - user_id=logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the cached jobs contain the registered job - assert cached_jobs is not None - assert len(cached_jobs) == 2 - assert {job.uid for job in cached_jobs} == { - registered_function_jobs[1].uid, - registered_function_jobs[4].uid, - } - - cached_jobs = await functions_rpc.find_cached_function_jobs( - rabbitmq_rpc_client=rpc_client, - function_id=registered_function.uid, - inputs={"input1": 1}, - user_id=other_logged_user["id"], - product_name=osparc_product_name, - ) - - # Assert the cached jobs does not contain the registered job for the other user - assert cached_jobs is None