From 08741d47f0359537336bed4972f5fd865af19929 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 15:53:54 +0200 Subject: [PATCH 01/15] add capture for run endpoint --- .../tests/mocks/run_function_parent_info.json | 2368 +++++++++++++++++ services/api-server/tests/unit/conftest.py | 21 + 2 files changed, 2389 insertions(+) create mode 100644 services/api-server/tests/mocks/run_function_parent_info.json diff --git a/services/api-server/tests/mocks/run_function_parent_info.json b/services/api-server/tests/mocks/run_function_parent_info.json new file mode 100644 index 000000000000..b9b9f8dc326c --- /dev/null +++ b/services/api-server/tests/mocks/run_function_parent_info.json @@ -0,0 +1,2368 @@ +[ + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:05:50.043464+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.10b07f64-0b19-4f2a-924b-67d9bcadcef8/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:08:08.825919+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b0f371b4-5247-4efc-94fb-eb3338765f73/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:09:08.065507+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.c7256296-53c7-408d-ad2b-53f2e2e17b77/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:11:47.314542+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.55547ef2-50e2-4320-ae1b-ba4fb0c68feb/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:15:55.792723+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.b20cfc08-44cb-4a7c-92a5-112e2560ba61/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:20:31.694095+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.68d96dd3-5ee2-42c8-add2-b0e32196a773/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8", + "task_name": "POST /v0/projects?from_study=e3e70682-c209-4cac-a29f-6fbed82c07cd&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3De3e70682-c209-4cac-a29f-6fbed82c07cd%26hidden%3Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:23:55.125558+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253De3e70682-c209-4cac-a29f-6fbed82c07cd%2526hidden%253Dtrue.54dfa19d-3324-44cf-876b-e30c92f238b8/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": null, + "error": { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "support_id": null, + "status": 404, + "errors": [ + { + "code": "HTTPNotFound", + "message": "Not Found", + "resource": null, + "field": null + } + ], + "logs": [ + { + "message": "Project e3e70682-c209-4cac-a29f-6fbed82c07cd not found", + "level": "ERROR", + "logger": "user" + } + ] + } + }, + "status_code": 404 + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24", + "task_name": "POST /v0/projects?from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:29:34.382817+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.40882c4a-e222-4e04-af43-3a8e45b2df24/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "uuid": "71ec33ca-485a-11f0-bc8c-0242ac140019", + "name": "sleeper (Copy)", + "description": "", + "thumbnail": "", + "type": "STANDARD", + "templateType": null, + "workbench": { + "1cd13000-dd5d-5ebd-964f-7affa26606a9": { + "key": "simcore/services/comp/itis/sleeper", + "version": "2.2.1", + "label": "sleeper", + "progress": 0.0, + "inputs": { + "input_2": 2, + "input_3": false, + "input_4": 0, + "input_5": 0 + }, + "inputsRequired": [], + "inputNodes": [], + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "4": { + "read": true, + "write": true, + "delete": true + } + }, + "creationDate": "2025-06-13T13:29:34.407Z", + "lastChangeDate": "2025-06-13T13:29:34.407Z", + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "trashedAt": null, + "trashedBy": null, + "tags": [], + "classifiers": [], + "quality": { + "enabled": true, + "tsr_target": { + "r01": { + "level": 4, + "references": "" + }, + "r02": { + "level": 4, + "references": "" + }, + "r03": { + "level": 4, + "references": "" + }, + "r04": { + "level": 4, + "references": "" + }, + "r05": { + "level": 4, + "references": "" + }, + "r06": { + "level": 4, + "references": "" + }, + "r07": { + "level": 4, + "references": "" + }, + "r08": { + "level": 4, + "references": "" + }, + "r09": { + "level": 4, + "references": "" + }, + "r10": { + "level": 4, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + }, + "tsr_current": { + "r01": { + "level": 0, + "references": "" + }, + "r02": { + "level": 0, + "references": "" + }, + "r03": { + "level": 0, + "references": "" + }, + "r04": { + "level": 0, + "references": "" + }, + "r05": { + "level": 0, + "references": "" + }, + "r06": { + "level": 0, + "references": "" + }, + "r07": { + "level": 0, + "references": "" + }, + "r08": { + "level": 0, + "references": "" + }, + "r09": { + "level": 0, + "references": "" + }, + "r10": { + "level": 0, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + } + }, + "ui": { + "workbench": { + "1cd13000-dd5d-5ebd-964f-7affa26606a9": { + "position": { + "x": 250, + "y": 100 + } + } + }, + "slideshow": {}, + "currentNodeId": "b00f9b90-4857-11f0-bc8c-0242ac140019", + "mode": "pipeline" + }, + "dev": {}, + "workspaceId": null, + "folderId": null + } + }, + "status_code": 201 + }, + { + "name": "PATCH /projects/71ec33ca-485a-11f0-bc8c-0242ac140019", + "description": "", + "method": "PATCH", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "request_payload": { + "name": "studies/b8dd6dc4-4857-11f0-bc8c-0242ac140019/jobs/71ec33ca-485a-11f0-bc8c-0242ac140019" + }, + "status_code": 204 + }, + { + "name": "GET /projects/71ec33ca-485a-11f0-bc8c-0242ac140019/inputs", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}/inputs", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "response_body": { + "data": {} + } + }, + { + "name": "POST /computations/71ec33ca-485a-11f0-bc8c-0242ac140019:start", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/computations/{project_id}:start", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "request_payload": {}, + "response_body": { + "data": { + "pipeline_id": "71ec33ca-485a-11f0-bc8c-0242ac140019" + } + }, + "status_code": 201 + }, + { + "name": "GET /v2/computations/71ec33ca-485a-11f0-bc8c-0242ac140019", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v2/computations/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "query": "user_id=1", + "response_body": { + "id": "71ec33ca-485a-11f0-bc8c-0242ac140019", + "state": "STARTED", + "result": null, + "pipeline_details": { + "adjacency_list": { + "1cd13000-dd5d-5ebd-964f-7affa26606a9": [] + }, + "progress": 0.05, + "node_states": { + "1cd13000-dd5d-5ebd-964f-7affa26606a9": { + "modified": true, + "dependencies": [], + "currentStatus": "STARTED", + "progress": 0.05 + } + } + }, + "iteration": 1, + "started": "2025-06-13T13:30:18.988214Z", + "stopped": null, + "submitted": "2025-06-13T13:30:18.806995Z", + "url": "http://webserver:8080:30003/v2/computations/71ec33ca-485a-11f0-bc8c-0242ac140019?user_id=1", + "stop_url": "http://webserver:8080:30003/v2/computations/71ec33ca-485a-11f0-bc8c-0242ac140019:stop?user_id=1" + } + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "task_name": "POST /v0/projects?from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "message": "Collecting files of 'sleeper'...", + "percent": 1.0 + }, + "done": false, + "started": "2025-06-13T13:31:09.141214+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.14b1e487-728d-4b99-badc-6993ec76f269", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:31:09.141214+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.14b1e487-728d-4b99-badc-6993ec76f269/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "uuid": "aa679b18-485a-11f0-89d1-0242ac14050c", + "name": "sleeper (Copy)", + "description": "", + "thumbnail": "", + "type": "STANDARD", + "templateType": null, + "workbench": { + "ed78079d-66fa-54fb-aca4-8f37ec436383": { + "key": "simcore/services/comp/itis/sleeper", + "version": "2.2.1", + "label": "sleeper", + "progress": 0.0, + "inputs": { + "input_2": 2, + "input_3": false, + "input_4": 0, + "input_5": 0 + }, + "inputsRequired": [], + "inputNodes": [], + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "4": { + "read": true, + "write": true, + "delete": true + } + }, + "creationDate": "2025-06-13T13:31:09.169Z", + "lastChangeDate": "2025-06-13T13:31:09.169Z", + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "trashedAt": null, + "trashedBy": null, + "tags": [], + "classifiers": [], + "quality": { + "enabled": true, + "tsr_target": { + "r01": { + "level": 4, + "references": "" + }, + "r02": { + "level": 4, + "references": "" + }, + "r03": { + "level": 4, + "references": "" + }, + "r04": { + "level": 4, + "references": "" + }, + "r05": { + "level": 4, + "references": "" + }, + "r06": { + "level": 4, + "references": "" + }, + "r07": { + "level": 4, + "references": "" + }, + "r08": { + "level": 4, + "references": "" + }, + "r09": { + "level": 4, + "references": "" + }, + "r10": { + "level": 4, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + }, + "tsr_current": { + "r01": { + "level": 0, + "references": "" + }, + "r02": { + "level": 0, + "references": "" + }, + "r03": { + "level": 0, + "references": "" + }, + "r04": { + "level": 0, + "references": "" + }, + "r05": { + "level": 0, + "references": "" + }, + "r06": { + "level": 0, + "references": "" + }, + "r07": { + "level": 0, + "references": "" + }, + "r08": { + "level": 0, + "references": "" + }, + "r09": { + "level": 0, + "references": "" + }, + "r10": { + "level": 0, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + } + }, + "ui": { + "workbench": { + "ed78079d-66fa-54fb-aca4-8f37ec436383": { + "position": { + "x": 250, + "y": 100 + } + } + }, + "slideshow": {}, + "currentNodeId": "b00f9b90-4857-11f0-bc8c-0242ac140019", + "mode": "pipeline" + }, + "dev": {}, + "workspaceId": null, + "folderId": null + } + }, + "status_code": 201 + }, + { + "name": "PATCH /projects/aa679b18-485a-11f0-89d1-0242ac14050c", + "description": "", + "method": "PATCH", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "request_payload": { + "name": "studies/b8dd6dc4-4857-11f0-bc8c-0242ac140019/jobs/aa679b18-485a-11f0-89d1-0242ac14050c" + }, + "status_code": 204 + }, + { + "name": "GET /projects/aa679b18-485a-11f0-89d1-0242ac14050c/inputs", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}/inputs", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "response_body": { + "data": {} + } + }, + { + "name": "POST /computations/aa679b18-485a-11f0-89d1-0242ac14050c:start", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/computations/{project_id}:start", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "request_payload": {}, + "response_body": { + "data": { + "pipeline_id": "aa679b18-485a-11f0-89d1-0242ac14050c" + } + }, + "status_code": 201 + }, + { + "name": "GET /v2/computations/aa679b18-485a-11f0-89d1-0242ac14050c", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v2/computations/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "query": "user_id=1", + "response_body": { + "id": "aa679b18-485a-11f0-89d1-0242ac14050c", + "state": "STARTED", + "result": null, + "pipeline_details": { + "adjacency_list": { + "ed78079d-66fa-54fb-aca4-8f37ec436383": [] + }, + "progress": 0.05, + "node_states": { + "ed78079d-66fa-54fb-aca4-8f37ec436383": { + "modified": true, + "dependencies": [], + "currentStatus": "STARTED", + "progress": 0.05 + } + } + }, + "iteration": 1, + "started": "2025-06-13T13:31:17.841816Z", + "stopped": null, + "submitted": "2025-06-13T13:31:17.672682Z", + "url": "http://webserver:8080:30003/v2/computations/aa679b18-485a-11f0-89d1-0242ac14050c?user_id=1", + "stop_url": "http://webserver:8080:30003/v2/computations/aa679b18-485a-11f0-89d1-0242ac14050c:stop?user_id=1" + } + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca", + "task_name": "POST /v0/projects?from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:34:33.232952+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.4280a56d-58e7-4f2c-aa15-f35a137e60ca/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "uuid": "240cb4c6-485b-11f0-89d1-0242ac14050c", + "name": "sleeper (Copy)", + "description": "", + "thumbnail": "", + "type": "STANDARD", + "templateType": null, + "workbench": { + "982c5607-783c-5295-9a4f-26c8e3c21211": { + "key": "simcore/services/comp/itis/sleeper", + "version": "2.2.1", + "label": "sleeper", + "progress": 0.0, + "inputs": { + "input_2": 2, + "input_3": false, + "input_4": 0, + "input_5": 0 + }, + "inputsRequired": [], + "inputNodes": [], + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "4": { + "read": true, + "write": true, + "delete": true + } + }, + "creationDate": "2025-06-13T13:34:33.254Z", + "lastChangeDate": "2025-06-13T13:34:33.254Z", + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "trashedAt": null, + "trashedBy": null, + "tags": [], + "classifiers": [], + "quality": { + "enabled": true, + "tsr_target": { + "r01": { + "level": 4, + "references": "" + }, + "r02": { + "level": 4, + "references": "" + }, + "r03": { + "level": 4, + "references": "" + }, + "r04": { + "level": 4, + "references": "" + }, + "r05": { + "level": 4, + "references": "" + }, + "r06": { + "level": 4, + "references": "" + }, + "r07": { + "level": 4, + "references": "" + }, + "r08": { + "level": 4, + "references": "" + }, + "r09": { + "level": 4, + "references": "" + }, + "r10": { + "level": 4, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + }, + "tsr_current": { + "r01": { + "level": 0, + "references": "" + }, + "r02": { + "level": 0, + "references": "" + }, + "r03": { + "level": 0, + "references": "" + }, + "r04": { + "level": 0, + "references": "" + }, + "r05": { + "level": 0, + "references": "" + }, + "r06": { + "level": 0, + "references": "" + }, + "r07": { + "level": 0, + "references": "" + }, + "r08": { + "level": 0, + "references": "" + }, + "r09": { + "level": 0, + "references": "" + }, + "r10": { + "level": 0, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + } + }, + "ui": { + "workbench": { + "982c5607-783c-5295-9a4f-26c8e3c21211": { + "position": { + "x": 250, + "y": 100 + } + } + }, + "slideshow": {}, + "currentNodeId": "b00f9b90-4857-11f0-bc8c-0242ac140019", + "mode": "pipeline" + }, + "dev": {}, + "workspaceId": null, + "folderId": null + } + }, + "status_code": 201 + }, + { + "name": "PATCH /projects/240cb4c6-485b-11f0-89d1-0242ac14050c", + "description": "", + "method": "PATCH", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "request_payload": { + "name": "studies/b8dd6dc4-4857-11f0-bc8c-0242ac140019/jobs/240cb4c6-485b-11f0-89d1-0242ac14050c" + }, + "status_code": 204 + }, + { + "name": "GET /projects/240cb4c6-485b-11f0-89d1-0242ac14050c/inputs", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}/inputs", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "response_body": { + "data": {} + } + }, + { + "name": "POST /computations/240cb4c6-485b-11f0-89d1-0242ac14050c:start", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/computations/{project_id}:start", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "request_payload": {}, + "response_body": { + "data": { + "pipeline_id": "240cb4c6-485b-11f0-89d1-0242ac14050c" + } + }, + "status_code": 201 + }, + { + "name": "GET /v2/computations/240cb4c6-485b-11f0-89d1-0242ac14050c", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v2/computations/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "query": "user_id=1", + "response_body": { + "id": "240cb4c6-485b-11f0-89d1-0242ac14050c", + "state": "STARTED", + "result": null, + "pipeline_details": { + "adjacency_list": { + "982c5607-783c-5295-9a4f-26c8e3c21211": [] + }, + "progress": 0.05, + "node_states": { + "982c5607-783c-5295-9a4f-26c8e3c21211": { + "modified": true, + "dependencies": [], + "currentStatus": "STARTED", + "progress": 0.05 + } + } + }, + "iteration": 1, + "started": "2025-06-13T13:34:40.872636Z", + "stopped": null, + "submitted": "2025-06-13T13:34:40.690820Z", + "url": "http://webserver:8080:30003/v2/computations/240cb4c6-485b-11f0-89d1-0242ac14050c?user_id=1", + "stop_url": "http://webserver:8080:30003/v2/computations/240cb4c6-485b-11f0-89d1-0242ac14050c:stop?user_id=1" + } + }, + { + "name": "POST /projects", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/projects", + "path_parameters": [] + }, + "query": "from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "response_body": { + "data": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b", + "task_name": "POST /v0/projects?from_study=b8dd6dc4-4857-11f0-bc8c-0242ac140019&hidden=true", + "status_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b", + "result_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b/result", + "abort_href": "http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b" + } + }, + "status_code": 202 + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "task_progress": { + "task_id": "POST%20%2Fv0%2Fprojects%3Ffrom_study%3Db8dd6dc4-4857-11f0-bc8c-0242ac140019%26hidden%3Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b", + "message": "finished", + "percent": 1.0 + }, + "done": true, + "started": "2025-06-13T13:48:21.407753+00:00" + }, + "error": null + } + }, + { + "name": "GET http://webserver:8080/v0/tasks-legacy/POST%2520%252Fv0%252Fprojects%253Ffrom_study%253Db8dd6dc4-4857-11f0-bc8c-0242ac140019%2526hidden%253Dtrue.e43e3bf5-5910-42ad-8abe-636f7aa87d7b/result", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/tasks-legacy/{task_id}/result", + "path_parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "str" + }, + "response_value": "tasks-legacy" + } + ] + }, + "response_body": { + "data": { + "uuid": "11ae9478-485d-11f0-bc8c-0242ac140019", + "name": "sleeper (Copy)", + "description": "", + "thumbnail": "", + "type": "STANDARD", + "templateType": null, + "workbench": { + "4a7d1258-dcfa-55ab-ab53-38d0fa99431a": { + "key": "simcore/services/comp/itis/sleeper", + "version": "2.2.1", + "label": "sleeper", + "progress": 0.0, + "inputs": { + "input_2": 2, + "input_3": false, + "input_4": 0, + "input_5": 0 + }, + "inputsRequired": [], + "inputNodes": [], + "state": { + "modified": true, + "dependencies": [], + "currentStatus": "NOT_STARTED", + "progress": null + } + } + }, + "prjOwner": "bisgaard@itis.swiss", + "accessRights": { + "4": { + "read": true, + "write": true, + "delete": true + } + }, + "creationDate": "2025-06-13T13:48:21.431Z", + "lastChangeDate": "2025-06-13T13:48:21.431Z", + "state": { + "locked": { + "value": false, + "status": "CLOSED" + }, + "state": { + "value": "NOT_STARTED" + } + }, + "trashedAt": null, + "trashedBy": null, + "tags": [], + "classifiers": [], + "quality": { + "enabled": true, + "tsr_target": { + "r01": { + "level": 4, + "references": "" + }, + "r02": { + "level": 4, + "references": "" + }, + "r03": { + "level": 4, + "references": "" + }, + "r04": { + "level": 4, + "references": "" + }, + "r05": { + "level": 4, + "references": "" + }, + "r06": { + "level": 4, + "references": "" + }, + "r07": { + "level": 4, + "references": "" + }, + "r08": { + "level": 4, + "references": "" + }, + "r09": { + "level": 4, + "references": "" + }, + "r10": { + "level": 4, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + }, + "tsr_current": { + "r01": { + "level": 0, + "references": "" + }, + "r02": { + "level": 0, + "references": "" + }, + "r03": { + "level": 0, + "references": "" + }, + "r04": { + "level": 0, + "references": "" + }, + "r05": { + "level": 0, + "references": "" + }, + "r06": { + "level": 0, + "references": "" + }, + "r07": { + "level": 0, + "references": "" + }, + "r08": { + "level": 0, + "references": "" + }, + "r09": { + "level": 0, + "references": "" + }, + "r10": { + "level": 0, + "references": "" + }, + "r03b": { + "references": "" + }, + "r03c": { + "references": "" + }, + "r07b": { + "references": "" + }, + "r07c": { + "references": "" + }, + "r07d": { + "references": "" + }, + "r07e": { + "references": "" + }, + "r08b": { + "references": "" + }, + "r10b": { + "references": "" + } + } + }, + "ui": { + "workbench": { + "4a7d1258-dcfa-55ab-ab53-38d0fa99431a": { + "position": { + "x": 250, + "y": 100 + } + } + }, + "slideshow": {}, + "currentNodeId": "b00f9b90-4857-11f0-bc8c-0242ac140019", + "mode": "pipeline" + }, + "dev": {}, + "workspaceId": null, + "folderId": null + } + }, + "status_code": 201 + }, + { + "name": "PATCH /projects/11ae9478-485d-11f0-bc8c-0242ac140019", + "description": "", + "method": "PATCH", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "request_payload": { + "name": "studies/b8dd6dc4-4857-11f0-bc8c-0242ac140019/jobs/11ae9478-485d-11f0-bc8c-0242ac140019" + }, + "status_code": 204 + }, + { + "name": "GET /projects/11ae9478-485d-11f0-bc8c-0242ac140019/inputs", + "description": "", + "method": "GET", + "host": "webserver", + "path": { + "path": "/v0/projects/{project_id}/inputs", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "projects" + } + ] + }, + "response_body": { + "data": {} + } + }, + { + "name": "POST /computations/11ae9478-485d-11f0-bc8c-0242ac140019:start", + "description": "", + "method": "POST", + "host": "webserver", + "path": { + "path": "/v0/computations/{project_id}:start", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "request_payload": {}, + "response_body": { + "data": { + "pipeline_id": "11ae9478-485d-11f0-bc8c-0242ac140019" + } + }, + "status_code": 201 + }, + { + "name": "GET /v2/computations/11ae9478-485d-11f0-bc8c-0242ac140019", + "description": "", + "method": "GET", + "host": "director-v2", + "path": { + "path": "/v2/computations/{project_id}", + "path_parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "title": "Project Id", + "type": "str", + "format": "uuid" + }, + "response_value": "computations" + } + ] + }, + "query": "user_id=1", + "response_body": { + "id": "11ae9478-485d-11f0-bc8c-0242ac140019", + "state": "STARTED", + "result": null, + "pipeline_details": { + "adjacency_list": { + "4a7d1258-dcfa-55ab-ab53-38d0fa99431a": [] + }, + "progress": 0.05, + "node_states": { + "4a7d1258-dcfa-55ab-ab53-38d0fa99431a": { + "modified": true, + "dependencies": [], + "currentStatus": "STARTED", + "progress": 0.05 + } + } + }, + "iteration": 1, + "started": "2025-06-13T13:48:28.876507Z", + "stopped": null, + "submitted": "2025-06-13T13:48:28.689344Z", + "url": "http://director-v2:8000/v2/computations/11ae9478-485d-11f0-bc8c-0242ac140019?user_id=1", + "stop_url": "http://director-v2:8000/v2/computations/11ae9478-485d-11f0-bc8c-0242ac140019:stop?user_id=1" + } + } +] diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py index 174bf1bd6013..a2766fcf53a7 100644 --- a/services/api-server/tests/unit/conftest.py +++ b/services/api-server/tests/unit/conftest.py @@ -715,6 +715,27 @@ def _mock(webserver_mock_router: MockRouter) -> MockRouter: return _mock +@pytest.fixture +def mock_webserver_patch_project( + app: FastAPI, faker: Faker, services_mocks_enabled: bool +) -> Callable[[MockRouter], MockRouter]: + settings: ApplicationSettings = app.state.settings + assert settings.API_SERVER_WEBSERVER is not None + + def _mock(webserver_mock_router: MockRouter) -> MockRouter: + def _patch_project(request: httpx.Request, *args, **kwargs): + return httpx.Response(status.HTTP_200_OK) + + if services_mocks_enabled: + webserver_mock_router.patch( + path__regex=r"/projects/(?P[\w-]+)$", + name="project_patch", + ).mock(side_effect=_patch_project) + return webserver_mock_router + + return _mock + + @pytest.fixture def openapi_dev_specs(project_slug_dir: Path) -> dict[str, Any]: openapi_file = (project_slug_dir / "openapi-dev.json").resolve() From bfee222516db13b71167e7439ebfce72e5fd18b1 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 15:56:41 +0200 Subject: [PATCH 02/15] add test without checks --- .../test_api_routers_functions.py | 47 ++++++++++++++++++- services/api-server/tests/unit/conftest.py | 21 --------- 2 files changed, 46 insertions(+), 22 deletions(-) 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 f5951f82ea7e..c30c8ef11802 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 @@ -3,6 +3,7 @@ import datetime from collections.abc import Callable +from pathlib import Path from typing import Any from uuid import uuid4 @@ -17,7 +18,7 @@ RegisteredProjectFunction, RegisteredProjectFunctionJob, ) -from models_library.functions import FunctionUserAccessRights +from models_library.functions import FunctionUserAccessRights, RegisteredFunctionJob from models_library.functions_errors import ( FunctionIDNotFoundError, FunctionReadAccessDeniedError, @@ -615,3 +616,47 @@ async def test_run_function_not_allowed( assert response.json()["errors"][0] == ( f"Function {mock_registered_function.uid} execute access denied for user {user_id}" ) + + +async def test_run_function_parent_info( + client: AsyncClient, + mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], + mock_registered_function: RegisteredProjectFunction, + mock_registered_function_job: RegisteredFunctionJob, + auth: httpx.BasicAuth, + user_id: UserID, + mocked_webserver_rest_api_base: respx.MockRouter, + mocked_directorv2_rest_api_base: respx.MockRouter, + mocked_webserver_rpc_api: dict[str, MockType], + create_respx_mock_from_capture, + project_tests_dir: Path, +) -> None: + + capture = "run_function_parent_info.json" + create_respx_mock_from_capture( + respx_mocks=[mocked_webserver_rest_api_base, mocked_directorv2_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[], + ) + + mock_handler_in_functions_rpc_interface( + "get_function_user_permissions", + FunctionUserAccessRights( + user_id=user_id, + execute=True, + read=True, + write=True, + ), + ) + mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface("find_cached_function_jobs", []) + mock_handler_in_functions_rpc_interface( + "register_function_job", mock_registered_function_job + ) + + response = await client.post( + f"{API_VTAG}/functions/{mock_registered_function.uid}:run", + json={}, + auth=auth, + ) + assert response.status_code == status.HTTP_200_OK diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py index a2766fcf53a7..174bf1bd6013 100644 --- a/services/api-server/tests/unit/conftest.py +++ b/services/api-server/tests/unit/conftest.py @@ -715,27 +715,6 @@ def _mock(webserver_mock_router: MockRouter) -> MockRouter: return _mock -@pytest.fixture -def mock_webserver_patch_project( - app: FastAPI, faker: Faker, services_mocks_enabled: bool -) -> Callable[[MockRouter], MockRouter]: - settings: ApplicationSettings = app.state.settings - assert settings.API_SERVER_WEBSERVER is not None - - def _mock(webserver_mock_router: MockRouter) -> MockRouter: - def _patch_project(request: httpx.Request, *args, **kwargs): - return httpx.Response(status.HTTP_200_OK) - - if services_mocks_enabled: - webserver_mock_router.patch( - path__regex=r"/projects/(?P[\w-]+)$", - name="project_patch", - ).mock(side_effect=_patch_project) - return webserver_mock_router - - return _mock - - @pytest.fixture def openapi_dev_specs(project_slug_dir: Path) -> dict[str, Any]: openapi_file = (project_slug_dir / "openapi-dev.json").resolve() From adf5bfbb586abe80c1cf547c3c83843a1756d4d0 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 16:28:24 +0200 Subject: [PATCH 03/15] add check that headers are passed --- .../api/routes/functions_routes.py | 14 ++++-- .../test_api_routers_functions.py | 48 ++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) 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 835922e5f34e..dabfbe997484 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 @@ -2,7 +2,7 @@ from typing import Annotated, Final import jsonschema -from fastapi import APIRouter, Depends, Request, status +from fastapi import APIRouter, Depends, Header, Request, status from fastapi_pagination.api import create_page from jsonschema import ValidationError from models_library.api_schemas_api_server.functions import ( @@ -29,6 +29,8 @@ UnsupportedFunctionClassError, ) from models_library.products import ProductName +from models_library.projects import ProjectID +from models_library.projects_nodes_io import NodeID from models_library.projects_state import RunningState from models_library.users import UserID from servicelib.fastapi.dependencies import get_reverse_url_mapper @@ -372,6 +374,8 @@ async def run_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], + x_simcore_parent_project_uuid: Annotated[ProjectID | None, Header()], + x_simcore_parent_node_id: Annotated[NodeID | None, Header()], ) -> RegisteredFunctionJob: user_permissions: FunctionUserAccessRights = ( @@ -436,8 +440,8 @@ async def run_function( # noqa: PLR0913 webserver_api=webserver_api, wb_api_rpc=wb_api_rpc, url_for=url_for, - x_simcore_parent_project_uuid=None, - x_simcore_parent_node_id=None, + x_simcore_parent_project_uuid=x_simcore_parent_project_uuid, + x_simcore_parent_node_id=x_simcore_parent_node_id, user_id=user_id, product_name=product_name, ) @@ -471,8 +475,8 @@ async def run_function( # noqa: PLR0913 solver_service=solver_service, job_service=job_service, url_for=url_for, - x_simcore_parent_project_uuid=None, - x_simcore_parent_node_id=None, + x_simcore_parent_project_uuid=x_simcore_parent_project_uuid, + x_simcore_parent_node_id=x_simcore_parent_node_id, ) await solvers_jobs.start_job( request=request, 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 c30c8ef11802..34884dccf588 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 @@ -8,7 +8,9 @@ from uuid import uuid4 import httpx +import pytest import respx +from faker import Faker from httpx import AsyncClient from models_library.api_schemas_webserver.functions import ( FunctionJobCollection, @@ -26,9 +28,16 @@ from models_library.rest_pagination import PageMetaInfoLimitOffset from models_library.users import UserID from pytest_mock import MockType +from pytest_simcore.helpers.httpx_calls_capture_models import HttpApiCallCaptureModel from servicelib.aiohttp import status +from servicelib.common_headers import ( + X_SIMCORE_PARENT_NODE_ID, + X_SIMCORE_PARENT_PROJECT_UUID, +) from simcore_service_api_server._meta import API_VTAG +_faker = Faker() + async def test_register_function( client: AsyncClient, @@ -618,6 +627,15 @@ async def test_run_function_not_allowed( ) +@pytest.mark.parametrize( + "parent_project_uuid, parent_node_uuid, expected_status_code", + [ + (None, None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), + ], +) async def test_run_function_parent_info( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], @@ -630,13 +648,33 @@ async def test_run_function_parent_info( mocked_webserver_rpc_api: dict[str, MockType], create_respx_mock_from_capture, project_tests_dir: Path, + parent_project_uuid: str | None, + parent_node_uuid: str | None, + expected_status_code: int, ) -> None: capture = "run_function_parent_info.json" + + def _default_side_effect( + request: httpx.Request, + path_params: dict[str, Any], + capture: HttpApiCallCaptureModel, + ) -> Any: + if request.method == "POST" and request.url.path.endswith("/projects"): + if parent_project_uuid: + _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) + assert _parent_uuid is not None + assert parent_project_uuid == _parent_uuid + if parent_node_uuid: + _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) + assert _parent_node_uuid is not None + assert parent_node_uuid == _parent_node_uuid + return capture.response_body + create_respx_mock_from_capture( respx_mocks=[mocked_webserver_rest_api_base, mocked_directorv2_rest_api_base], capture_path=project_tests_dir / "mocks" / capture, - side_effects_callbacks=[], + side_effects_callbacks=[_default_side_effect] * 50, ) mock_handler_in_functions_rpc_interface( @@ -654,9 +692,15 @@ async def test_run_function_parent_info( "register_function_job", mock_registered_function_job ) + headers = dict() + if parent_project_uuid: + headers[X_SIMCORE_PARENT_PROJECT_UUID] = parent_project_uuid + if parent_node_uuid: + headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid response = await client.post( f"{API_VTAG}/functions/{mock_registered_function.uid}:run", json={}, auth=auth, + headers=headers, ) - assert response.status_code == status.HTTP_200_OK + assert response.status_code == expected_status_code From 2191ac75fca3d019f2aa4fc2cd1a106a94ab0511 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 16:37:35 +0200 Subject: [PATCH 04/15] minor changes --- ..._parent_info.json => run_study_function_parent_info.json} | 0 .../tests/unit/api_functions/test_api_routers_functions.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename services/api-server/tests/mocks/{run_function_parent_info.json => run_study_function_parent_info.json} (100%) diff --git a/services/api-server/tests/mocks/run_function_parent_info.json b/services/api-server/tests/mocks/run_study_function_parent_info.json similarity index 100% rename from services/api-server/tests/mocks/run_function_parent_info.json rename to services/api-server/tests/mocks/run_study_function_parent_info.json 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 34884dccf588..952a4393cf13 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 @@ -636,6 +636,7 @@ async def test_run_function_not_allowed( (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), ], ) +@pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) async def test_run_function_parent_info( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], @@ -651,10 +652,9 @@ async def test_run_function_parent_info( parent_project_uuid: str | None, parent_node_uuid: str | None, expected_status_code: int, + capture: str, ) -> None: - capture = "run_function_parent_info.json" - def _default_side_effect( request: httpx.Request, path_params: dict[str, Any], @@ -697,6 +697,7 @@ def _default_side_effect( headers[X_SIMCORE_PARENT_PROJECT_UUID] = parent_project_uuid if parent_node_uuid: headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid + response = await client.post( f"{API_VTAG}/functions/{mock_registered_function.uid}:run", json={}, From 8b2599730558f7b6242927ec9d76169f6837d558 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 16:54:52 +0200 Subject: [PATCH 05/15] start implementing test for solver function --- .../tests/unit/api_functions/conftest.py | 35 +++- .../test_api_routers_functions.py | 193 ++++++++++++++---- 2 files changed, 179 insertions(+), 49 deletions(-) diff --git a/services/api-server/tests/unit/api_functions/conftest.py b/services/api-server/tests/unit/api_functions/conftest.py index 891fdc533f47..1df264f0e397 100644 --- a/services/api-server/tests/unit/api_functions/conftest.py +++ b/services/api-server/tests/unit/api_functions/conftest.py @@ -26,7 +26,10 @@ RegisteredProjectFunction, RegisteredProjectFunctionJob, ) -from models_library.functions import RegisteredFunctionJobCollection +from models_library.functions import ( + RegisteredFunctionJobCollection, + RegisteredSolverFunction, +) from models_library.functions_errors import FunctionIDNotFoundError from models_library.projects import ProjectID from pytest_mock import MockerFixture @@ -119,7 +122,7 @@ def mock_function( @pytest.fixture -def mock_registered_function(mock_function: Function) -> RegisteredFunction: +def mock_registered_project_function(mock_function: Function) -> RegisteredFunction: return RegisteredProjectFunction( **{ **mock_function.dict(), @@ -130,9 +133,33 @@ def mock_registered_function(mock_function: Function) -> RegisteredFunction: @pytest.fixture -def mock_function_job(mock_registered_function: RegisteredFunction) -> FunctionJob: +def mock_registered_solver_function( + mock_function: Function, + sample_input_schema: JSONFunctionInputSchema, + sample_output_schema: JSONFunctionOutputSchema, +) -> RegisteredFunction: + return RegisteredSolverFunction( + **{ + "title": "test_function", + "function_class": FunctionClass.SOLVER, + "description": "A test function", + "input_schema": sample_input_schema, + "output_schema": sample_output_schema, + "default_inputs": None, + "uid": str(uuid4()), + "created_at": datetime.datetime.now(datetime.UTC), + "solver_key": "simcore/services/comp/ans-model", + "solver_version": "1.0.1", + } + ) + + +@pytest.fixture +def mock_function_job( + mock_registered_project_function: RegisteredFunction, +) -> FunctionJob: mock_function_job = { - "function_uid": mock_registered_function.uid, + "function_uid": mock_registered_project_function.uid, "title": "Test Function Job", "description": "A test function job", "inputs": {"key": "value"}, 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 952a4393cf13..37d9f8d3c653 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 @@ -5,7 +5,7 @@ from collections.abc import Callable from pathlib import Path from typing import Any -from uuid import uuid4 +from uuid import UUID, uuid4 import httpx import pytest @@ -20,7 +20,11 @@ RegisteredProjectFunction, RegisteredProjectFunctionJob, ) -from models_library.functions import FunctionUserAccessRights, RegisteredFunctionJob +from models_library.functions import ( + FunctionUserAccessRights, + RegisteredFunctionJob, + RegisteredSolverFunction, +) from models_library.functions_errors import ( FunctionIDNotFoundError, FunctionReadAccessDeniedError, @@ -44,10 +48,10 @@ 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, + mock_registered_project_function: RegisteredProjectFunction, ) -> None: mock_handler_in_functions_rpc_interface( - "register_function", mock_registered_function + "register_function", mock_registered_project_function ) response = await client.post( f"{API_VTAG}/functions", json=mock_function.model_dump(mode="json"), auth=auth @@ -56,7 +60,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 == mock_registered_function + assert returned_function == mock_registered_project_function async def test_register_function_invalid( @@ -82,16 +86,18 @@ async def test_register_function_invalid( async def test_get_function( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: function_id = str(uuid4()) - mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) response = await client.get(f"{API_VTAG}/functions/{function_id}", auth=auth) assert response.status_code == status.HTTP_200_OK returned_function = RegisteredProjectFunction.model_validate(response.json()) - assert returned_function == mock_registered_function + assert returned_function == mock_registered_project_function async def test_get_function_not_found( @@ -119,7 +125,7 @@ async def test_get_function_read_access_denied( mock_handler_in_functions_rpc_interface: Callable[ [str, Any, Exception | None], None ], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: unauthorized_user_id = "unauthorized user" @@ -127,29 +133,30 @@ async def test_get_function_read_access_denied( "get_function", None, FunctionReadAccessDeniedError( - function_id=mock_registered_function.uid, user_id=unauthorized_user_id + function_id=mock_registered_project_function.uid, + user_id=unauthorized_user_id, ), ) response = await client.get( - f"{API_VTAG}/functions/{mock_registered_function.uid}", auth=auth + f"{API_VTAG}/functions/{mock_registered_project_function.uid}", auth=auth ) assert response.status_code == status.HTTP_403_FORBIDDEN assert response.json()["errors"][0] == ( - f"Function {mock_registered_function.uid} read access denied for user {unauthorized_user_id}" + f"Function {mock_registered_project_function.uid} read access denied for user {unauthorized_user_id}" ) async def test_list_functions( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: mock_handler_in_functions_rpc_interface( "list_functions", ( - [mock_registered_function for _ in range(5)], + [mock_registered_project_function for _ in range(5)], PageMetaInfoLimitOffset(total=5, count=5, limit=10, offset=0), ), ) @@ -160,13 +167,13 @@ async def test_list_functions( assert response.status_code == status.HTTP_200_OK data = response.json()["items"] assert len(data) == 5 - assert data[0]["title"] == mock_registered_function.title + assert data[0]["title"] == mock_registered_project_function.title async def test_update_function_title( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: @@ -174,7 +181,7 @@ async def test_update_function_title( "update_function_title", RegisteredProjectFunction( **{ - **mock_registered_function.model_dump(), + **mock_registered_project_function.model_dump(), "title": "updated_example_function", } ), @@ -183,7 +190,7 @@ async def test_update_function_title( # Update the function title updated_title = {"title": "updated_example_function"} response = await client.patch( - f"{API_VTAG}/functions/{mock_registered_function.uid}/title", + f"{API_VTAG}/functions/{mock_registered_project_function.uid}/title", params=updated_title, auth=auth, ) @@ -195,14 +202,14 @@ async def test_update_function_title( async def test_update_function_description( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: mock_handler_in_functions_rpc_interface( "update_function_description", RegisteredProjectFunction( **{ - **mock_registered_function.model_dump(), + **mock_registered_project_function.model_dump(), "description": "updated_example_function", } ), @@ -211,7 +218,7 @@ async def test_update_function_description( # Update the function description updated_description = {"description": "updated_example_function"} response = await client.patch( - f"{API_VTAG}/functions/{mock_registered_function.uid}/description", + f"{API_VTAG}/functions/{mock_registered_project_function.uid}/description", params=updated_description, auth=auth, ) @@ -223,56 +230,66 @@ async def test_update_function_description( async def test_get_function_input_schema( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: - mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) response = await client.get( - f"{API_VTAG}/functions/{mock_registered_function.uid}/input_schema", auth=auth + f"{API_VTAG}/functions/{mock_registered_project_function.uid}/input_schema", + auth=auth, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert ( - data["schema_content"] == mock_registered_function.input_schema.schema_content + data["schema_content"] + == mock_registered_project_function.input_schema.schema_content ) async def test_get_function_output_schema( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: - mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) response = await client.get( - f"{API_VTAG}/functions/{mock_registered_function.uid}/output_schema", auth=auth + f"{API_VTAG}/functions/{mock_registered_project_function.uid}/output_schema", + auth=auth, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert ( - data["schema_content"] == mock_registered_function.output_schema.schema_content + data["schema_content"] + == mock_registered_project_function.output_schema.schema_content ) async def test_validate_function_inputs( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: - mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) # Validate inputs validate_payload = {"input1": 10} response = await client.post( - f"{API_VTAG}/functions/{mock_registered_function.uid}:validate_inputs", + f"{API_VTAG}/functions/{mock_registered_project_function.uid}:validate_inputs", json=validate_payload, auth=auth, ) @@ -284,14 +301,14 @@ async def test_validate_function_inputs( async def test_delete_function( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: mock_handler_in_functions_rpc_interface("delete_function", None) # Delete the function response = await client.delete( - f"{API_VTAG}/functions/{mock_registered_function.uid}", auth=auth + f"{API_VTAG}/functions/{mock_registered_project_function.uid}", auth=auth ) assert response.status_code == status.HTTP_200_OK @@ -374,7 +391,7 @@ async def test_list_function_jobs_with_function_filter( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], mock_registered_function_job: RegisteredProjectFunctionJob, - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: @@ -388,7 +405,7 @@ async def test_list_function_jobs_with_function_filter( # Now, list function jobs with a filter response = await client.get( - f"{API_VTAG}/functions/{mock_registered_function.uid}/jobs", auth=auth + f"{API_VTAG}/functions/{mock_registered_project_function.uid}/jobs", auth=auth ) assert response.status_code == status.HTTP_200_OK @@ -567,7 +584,7 @@ async def test_list_function_job_collections_with_function_filter( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], mock_registered_function_job_collection: RegisteredFunctionJobCollection, - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, ) -> None: @@ -580,7 +597,7 @@ async def test_list_function_job_collections_with_function_filter( ) response = await client.get( - f"{API_VTAG}/function_job_collections?function_id={mock_registered_function.uid}&limit=2&offset=1", + f"{API_VTAG}/function_job_collections?function_id={mock_registered_project_function.uid}&limit=2&offset=1", auth=auth, ) assert response.status_code == status.HTTP_200_OK @@ -599,7 +616,7 @@ async def test_list_function_job_collections_with_function_filter( async def test_run_function_not_allowed( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_project_function: RegisteredProjectFunction, auth: httpx.BasicAuth, user_id: UserID, mocked_webserver_rest_api_base: respx.MockRouter, @@ -617,15 +634,97 @@ async def test_run_function_not_allowed( ) response = await client.post( - f"{API_VTAG}/functions/{mock_registered_function.uid}:run", + f"{API_VTAG}/functions/{mock_registered_project_function.uid}:run", json={}, auth=auth, ) assert response.status_code == status.HTTP_403_FORBIDDEN assert response.json()["errors"][0] == ( - f"Function {mock_registered_function.uid} execute access denied for user {user_id}" + f"Function {mock_registered_project_function.uid} execute access denied for user {user_id}" + ) + + +@pytest.mark.parametrize( + "parent_project_uuid, parent_node_uuid, expected_status_code", + [ + (None, None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), + ], +) +@pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) +async def test_run_project_function_parent_info( + client: AsyncClient, + mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], + mock_registered_project_function: RegisteredProjectFunction, + mock_registered_function_job: RegisteredFunctionJob, + auth: httpx.BasicAuth, + user_id: UserID, + mocked_webserver_rest_api_base: respx.MockRouter, + mocked_directorv2_rest_api_base: respx.MockRouter, + mocked_webserver_rpc_api: dict[str, MockType], + create_respx_mock_from_capture, + project_tests_dir: Path, + parent_project_uuid: str | None, + parent_node_uuid: str | None, + expected_status_code: int, + capture: str, +) -> None: + + def _default_side_effect( + request: httpx.Request, + path_params: dict[str, Any], + capture: HttpApiCallCaptureModel, + ) -> Any: + if request.method == "POST" and request.url.path.endswith("/projects"): + if parent_project_uuid: + _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) + assert _parent_uuid is not None + assert parent_project_uuid == _parent_uuid + if parent_node_uuid: + _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) + assert _parent_node_uuid is not None + assert parent_node_uuid == _parent_node_uuid + return capture.response_body + + create_respx_mock_from_capture( + respx_mocks=[mocked_webserver_rest_api_base, mocked_directorv2_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[_default_side_effect] * 50, + ) + + mock_handler_in_functions_rpc_interface( + "get_function_user_permissions", + FunctionUserAccessRights( + user_id=user_id, + execute=True, + read=True, + write=True, + ), + ) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) + mock_handler_in_functions_rpc_interface("find_cached_function_jobs", []) + mock_handler_in_functions_rpc_interface( + "register_function_job", mock_registered_function_job ) + headers = dict() + if parent_project_uuid: + headers[X_SIMCORE_PARENT_PROJECT_UUID] = parent_project_uuid + if parent_node_uuid: + headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid + + response = await client.post( + f"{API_VTAG}/functions/{mock_registered_project_function.uid}:run", + json={}, + auth=auth, + headers=headers, + ) + assert response.status_code == expected_status_code + @pytest.mark.parametrize( "parent_project_uuid, parent_node_uuid, expected_status_code", @@ -637,10 +736,10 @@ async def test_run_function_not_allowed( ], ) @pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) -async def test_run_function_parent_info( +async def test_run_solver_function_parent_info( client: AsyncClient, mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_function: RegisteredProjectFunction, + mock_registered_solver_function: RegisteredSolverFunction, mock_registered_function_job: RegisteredFunctionJob, auth: httpx.BasicAuth, user_id: UserID, @@ -655,6 +754,8 @@ async def test_run_function_parent_info( capture: str, ) -> None: + mock_registered_solver_function.uid = UUID("bf27b3f2-34c1-4b26-a3cb-a5c259e6489c") + def _default_side_effect( request: httpx.Request, path_params: dict[str, Any], @@ -686,7 +787,9 @@ def _default_side_effect( write=True, ), ) - mock_handler_in_functions_rpc_interface("get_function", mock_registered_function) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_solver_function + ) mock_handler_in_functions_rpc_interface("find_cached_function_jobs", []) mock_handler_in_functions_rpc_interface( "register_function_job", mock_registered_function_job @@ -699,7 +802,7 @@ def _default_side_effect( headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid response = await client.post( - f"{API_VTAG}/functions/{mock_registered_function.uid}:run", + f"{API_VTAG}/functions/{mock_registered_solver_function.uid}:run", json={}, auth=auth, headers=headers, From 9abfff665b40f45867c042c68290c3ef799d8052 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Jun 2025 17:02:36 +0200 Subject: [PATCH 06/15] further additions to test --- .../tests/unit/api_functions/test_api_routers_functions.py | 1 + 1 file changed, 1 insertion(+) 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 37d9f8d3c653..3335ff774b51 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 @@ -741,6 +741,7 @@ async def test_run_solver_function_parent_info( mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], mock_registered_solver_function: RegisteredSolverFunction, mock_registered_function_job: RegisteredFunctionJob, + mocked_catalog_rpc_api: dict[str, MockType], auth: httpx.BasicAuth, user_id: UserID, mocked_webserver_rest_api_base: respx.MockRouter, From cbfffc1682320a81e6a15fdaba914d574156fb89 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 11:23:30 +0200 Subject: [PATCH 07/15] allow user to explicitly set 'null' as value for headers --- services/api-server/openapi.json | 68 +++++++++++++++++++ .../api/routes/functions_routes.py | 18 ++++- .../test_api_routers_functions.py | 26 ++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 5a64c1baacb7..af19b5d3c6d8 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -7585,6 +7585,40 @@ "format": "uuid", "title": "Function Id" } + }, + { + "name": "x-simcore-parent-project-uuid", + "in": "header", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "X-Simcore-Parent-Project-Uuid" + } + }, + { + "name": "x-simcore-parent-node-id", + "in": "header", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "X-Simcore-Parent-Node-Id" + } } ], "requestBody": { @@ -7681,6 +7715,40 @@ "format": "uuid", "title": "Function Id" } + }, + { + "name": "x-simcore-parent-project-uuid", + "in": "header", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "X-Simcore-Parent-Project-Uuid" + } + }, + { + "name": "x-simcore-parent-node-id", + "in": "header", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "X-Simcore-Parent-Node-Id" + } } ], "requestBody": { 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 73c76c60128a..2c714b828117 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 @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Annotated, Final +from typing import Annotated, Final, TypeAlias import jsonschema from fastapi import APIRouter, Depends, Header, Request, status @@ -33,6 +33,7 @@ from models_library.projects_nodes_io import NodeID from models_library.projects_state import RunningState from models_library.users import UserID +from pydantic import StringConstraints from servicelib.fastapi.dependencies import get_reverse_url_mapper from simcore_service_api_server._service_jobs import JobService @@ -58,6 +59,8 @@ # pylint: disable=too-many-arguments # pylint: disable=cyclic-import +NullString: TypeAlias = Annotated[str, StringConstraints(pattern="^null$")] + function_router = APIRouter() _COMMON_FUNCTION_ERROR_RESPONSES: Final[dict] = { @@ -374,10 +377,15 @@ async def run_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], - x_simcore_parent_project_uuid: Annotated[ProjectID | None, Header()], - x_simcore_parent_node_id: Annotated[NodeID | None, Header()], + x_simcore_parent_project_uuid: Annotated[ProjectID | NullString | None, Header()], + x_simcore_parent_node_id: Annotated[NodeID | NullString | None, Header()], ) -> RegisteredFunctionJob: + if not isinstance(x_simcore_parent_project_uuid, ProjectID): + x_simcore_parent_project_uuid = None + if not isinstance(x_simcore_parent_node_id, NodeID): + x_simcore_parent_node_id = None + # Make sure the user is allowed to execute any function # (read/write right is checked in the other endpoint called in this method) user_api_access_rights = await wb_api_rpc.get_functions_user_api_access_rights( @@ -562,6 +570,8 @@ async def map_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], + x_simcore_parent_project_uuid: Annotated[ProjectID | NullString | None, Header()], + x_simcore_parent_node_id: Annotated[NodeID | NullString, Header()], ) -> RegisteredFunctionJobCollection: function_jobs = [] function_jobs = [ @@ -577,6 +587,8 @@ async def map_function( # noqa: PLR0913 request=request, solver_service=solver_service, job_service=job_service, + x_simcore_parent_project_uuid=x_simcore_parent_project_uuid, + x_simcore_parent_node_id=x_simcore_parent_node_id, ) for function_inputs in function_inputs_list ] 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 6f09813eca86..f5a0c790ce09 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 @@ -659,14 +659,18 @@ async def async_magic(): MagicMock.__await__ = lambda _: async_magic().__await__() response = await client.post( - f"{API_VTAG}/functions/{mock_registered_function.uid}:{funcapi_endpoint}", + f"{API_VTAG}/functions/{mock_registered_project_function.uid}:{funcapi_endpoint}", json=endpoint_inputs, auth=auth, + headers={ + X_SIMCORE_PARENT_PROJECT_UUID: "null", + X_SIMCORE_PARENT_NODE_ID: "null", + }, ) if user_has_execute_right: assert response.status_code == status.HTTP_403_FORBIDDEN assert response.json()["errors"][0] == ( - f"Function {mock_registered_function.uid} execute access denied for user {user_id}" + f"Function {mock_registered_project_function.uid} execute access denied for user {user_id}" ) else: assert response.status_code == status.HTTP_403_FORBIDDEN @@ -741,6 +745,15 @@ def _default_side_effect( mock_handler_in_functions_rpc_interface( "register_function_job", mock_registered_function_job ) + mock_handler_in_functions_rpc_interface( + "get_functions_user_api_access_rights", + FunctionUserApiAccessRights( + user_id=user_id, + execute_functions=True, + write_functions=True, + read_functions=True, + ), + ) headers = dict() if parent_project_uuid: @@ -826,6 +839,15 @@ def _default_side_effect( mock_handler_in_functions_rpc_interface( "register_function_job", mock_registered_function_job ) + mock_handler_in_functions_rpc_interface( + "get_functions_user_api_access_rights", + FunctionUserApiAccessRights( + user_id=user_id, + execute_functions=True, + write_functions=True, + read_functions=True, + ), + ) headers = dict() if parent_project_uuid: From b52fbc00765d5ec2fcbbbf0a0207af0850744820 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 11:56:00 +0200 Subject: [PATCH 08/15] add test for map endpoint --- .../test_api_routers_functions.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) 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 f5a0c790ce09..34ef2708e756 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 @@ -3,6 +3,7 @@ import datetime from collections.abc import Callable +from functools import partial from pathlib import Path from typing import Any from unittest.mock import MagicMock @@ -862,3 +863,110 @@ def _default_side_effect( headers=headers, ) assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "parent_project_uuid, parent_node_uuid, expected_status_code", + [ + (None, None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), + (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), + (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), + ], +) +@pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) +async def test_map_function_parent_info( + client: AsyncClient, + mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], + mock_registered_project_function: RegisteredProjectFunction, + mock_registered_function_job: RegisteredFunctionJob, + auth: httpx.BasicAuth, + user_id: UserID, + mocked_webserver_rest_api_base: respx.MockRouter, + mocked_directorv2_rest_api_base: respx.MockRouter, + mocked_webserver_rpc_api: dict[str, MockType], + create_respx_mock_from_capture, + project_tests_dir: Path, + parent_project_uuid: str | None, + parent_node_uuid: str | None, + expected_status_code: int, + capture: str, +) -> None: + + side_effect_checks = dict() + + def _default_side_effect( + side_effect_checks: dict, + request: httpx.Request, + path_params: dict[str, Any], + capture: HttpApiCallCaptureModel, + ) -> Any: + if request.method == "POST" and request.url.path.endswith("/projects"): + side_effect_checks["headers_checked"] = True + if parent_project_uuid: + _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) + assert _parent_uuid is not None + assert parent_project_uuid == _parent_uuid + if parent_node_uuid: + _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) + assert _parent_node_uuid is not None + assert parent_node_uuid == _parent_node_uuid + return capture.response_body + + create_respx_mock_from_capture( + respx_mocks=[mocked_webserver_rest_api_base, mocked_directorv2_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[partial(_default_side_effect, side_effect_checks)] * 50, + ) + + mock_handler_in_functions_rpc_interface( + "get_function_user_permissions", + FunctionUserAccessRights( + user_id=user_id, + execute=True, + read=True, + write=True, + ), + ) + mock_handler_in_functions_rpc_interface( + "get_function", mock_registered_project_function + ) + mock_handler_in_functions_rpc_interface("find_cached_function_jobs", []) + mock_handler_in_functions_rpc_interface( + "register_function_job", mock_registered_function_job + ) + mock_handler_in_functions_rpc_interface( + "get_functions_user_api_access_rights", + FunctionUserApiAccessRights( + user_id=user_id, + execute_functions=True, + write_functions=True, + read_functions=True, + ), + ) + mock_handler_in_functions_rpc_interface( + "register_function_job_collection", + RegisteredFunctionJobCollection( + uid=UUID(_faker.uuid4()), + title="Test Collection", + description="A test function job collection", + job_ids=[], + created_at=datetime.datetime.now(datetime.UTC), + ), + ) + + headers = dict() + if parent_project_uuid: + headers[X_SIMCORE_PARENT_PROJECT_UUID] = parent_project_uuid + if parent_node_uuid: + headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid + + response = await client.post( + f"{API_VTAG}/functions/{mock_registered_project_function.uid}:map", + json=[{}, {}], + auth=auth, + headers=headers, + ) + if expected_status_code == status.HTTP_200_OK: + assert side_effect_checks["headers_checked"] == True + assert response.status_code == expected_status_code From c9dc011ad6e6338ec2d07e0a4c8c3dc7f87b4280 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 13:15:26 +0200 Subject: [PATCH 09/15] remove solver function run test --- .../test_api_routers_functions.py | 105 +----------------- 1 file changed, 6 insertions(+), 99 deletions(-) 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 34ef2708e756..8b8a97d23ddd 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 @@ -26,7 +26,6 @@ FunctionUserAccessRights, FunctionUserApiAccessRights, RegisteredFunctionJob, - RegisteredSolverFunction, ) from models_library.functions_errors import ( FunctionIDNotFoundError, @@ -687,6 +686,7 @@ async def async_magic(): (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), + ("null", "null", status.HTTP_200_OK), ], ) @pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) @@ -714,11 +714,11 @@ def _default_side_effect( capture: HttpApiCallCaptureModel, ) -> Any: if request.method == "POST" and request.url.path.endswith("/projects"): - if parent_project_uuid: + if parent_project_uuid and parent_project_uuid != "null": _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) assert _parent_uuid is not None assert parent_project_uuid == _parent_uuid - if parent_node_uuid: + if parent_node_uuid and parent_node_uuid != "null": _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) assert _parent_node_uuid is not None assert parent_node_uuid == _parent_node_uuid @@ -778,100 +778,7 @@ def _default_side_effect( (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), - ], -) -@pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) -async def test_run_solver_function_parent_info( - client: AsyncClient, - mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], - mock_registered_solver_function: RegisteredSolverFunction, - mock_registered_function_job: RegisteredFunctionJob, - mocked_catalog_rpc_api: dict[str, MockType], - auth: httpx.BasicAuth, - user_id: UserID, - mocked_webserver_rest_api_base: respx.MockRouter, - mocked_directorv2_rest_api_base: respx.MockRouter, - mocked_webserver_rpc_api: dict[str, MockType], - create_respx_mock_from_capture, - project_tests_dir: Path, - parent_project_uuid: str | None, - parent_node_uuid: str | None, - expected_status_code: int, - capture: str, -) -> None: - - mock_registered_solver_function.uid = UUID("bf27b3f2-34c1-4b26-a3cb-a5c259e6489c") - - def _default_side_effect( - request: httpx.Request, - path_params: dict[str, Any], - capture: HttpApiCallCaptureModel, - ) -> Any: - if request.method == "POST" and request.url.path.endswith("/projects"): - if parent_project_uuid: - _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) - assert _parent_uuid is not None - assert parent_project_uuid == _parent_uuid - if parent_node_uuid: - _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) - assert _parent_node_uuid is not None - assert parent_node_uuid == _parent_node_uuid - return capture.response_body - - create_respx_mock_from_capture( - respx_mocks=[mocked_webserver_rest_api_base, mocked_directorv2_rest_api_base], - capture_path=project_tests_dir / "mocks" / capture, - side_effects_callbacks=[_default_side_effect] * 50, - ) - - mock_handler_in_functions_rpc_interface( - "get_function_user_permissions", - FunctionUserAccessRights( - user_id=user_id, - execute=True, - read=True, - write=True, - ), - ) - mock_handler_in_functions_rpc_interface( - "get_function", mock_registered_solver_function - ) - mock_handler_in_functions_rpc_interface("find_cached_function_jobs", []) - mock_handler_in_functions_rpc_interface( - "register_function_job", mock_registered_function_job - ) - mock_handler_in_functions_rpc_interface( - "get_functions_user_api_access_rights", - FunctionUserApiAccessRights( - user_id=user_id, - execute_functions=True, - write_functions=True, - read_functions=True, - ), - ) - - headers = dict() - if parent_project_uuid: - headers[X_SIMCORE_PARENT_PROJECT_UUID] = parent_project_uuid - if parent_node_uuid: - headers[X_SIMCORE_PARENT_NODE_ID] = parent_node_uuid - - response = await client.post( - f"{API_VTAG}/functions/{mock_registered_solver_function.uid}:run", - json={}, - auth=auth, - headers=headers, - ) - assert response.status_code == expected_status_code - - -@pytest.mark.parametrize( - "parent_project_uuid, parent_node_uuid, expected_status_code", - [ - (None, None, status.HTTP_422_UNPROCESSABLE_ENTITY), - (f"{_faker.uuid4()}", None, status.HTTP_422_UNPROCESSABLE_ENTITY), - (None, f"{_faker.uuid4()}", status.HTTP_422_UNPROCESSABLE_ENTITY), - (f"{_faker.uuid4()}", f"{_faker.uuid4()}", status.HTTP_200_OK), + ("null", "null", status.HTTP_200_OK), ], ) @pytest.mark.parametrize("capture", ["run_study_function_parent_info.json"]) @@ -903,11 +810,11 @@ def _default_side_effect( ) -> Any: if request.method == "POST" and request.url.path.endswith("/projects"): side_effect_checks["headers_checked"] = True - if parent_project_uuid: + if parent_project_uuid and parent_project_uuid != "null": _parent_uuid = request.headers.get(X_SIMCORE_PARENT_PROJECT_UUID) assert _parent_uuid is not None assert parent_project_uuid == _parent_uuid - if parent_node_uuid: + if parent_node_uuid and parent_node_uuid != "null": _parent_node_uuid = request.headers.get(X_SIMCORE_PARENT_NODE_ID) assert _parent_node_uuid is not None assert parent_node_uuid == _parent_node_uuid From 1726889f79d025a2511d1d99db8b013f405b1d12 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 13:23:23 +0200 Subject: [PATCH 10/15] only allow string value null --- services/api-server/openapi.json | 13 +++++++--- .../api/routes/functions_routes.py | 26 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index af19b5d3c6d8..e6471771139c 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -7597,7 +7597,8 @@ "format": "uuid" }, { - "type": "null" + "type": "string", + "pattern": "^null$" } ], "title": "X-Simcore-Parent-Project-Uuid" @@ -7614,7 +7615,8 @@ "format": "uuid" }, { - "type": "null" + "type": "string", + "pattern": "^null$" } ], "title": "X-Simcore-Parent-Node-Id" @@ -7726,6 +7728,10 @@ "type": "string", "format": "uuid" }, + { + "type": "string", + "pattern": "^null$" + }, { "type": "null" } @@ -7744,7 +7750,8 @@ "format": "uuid" }, { - "type": "null" + "type": "string", + "pattern": "^null$" } ], "title": "X-Simcore-Parent-Node-Id" 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 2c714b828117..6e09b1bd978c 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 @@ -377,14 +377,20 @@ async def run_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], - x_simcore_parent_project_uuid: Annotated[ProjectID | NullString | None, Header()], - x_simcore_parent_node_id: Annotated[NodeID | NullString | None, Header()], + x_simcore_parent_project_uuid: Annotated[ProjectID | NullString, Header()], + x_simcore_parent_node_id: Annotated[NodeID | NullString, Header()], ) -> RegisteredFunctionJob: - if not isinstance(x_simcore_parent_project_uuid, ProjectID): - x_simcore_parent_project_uuid = None - if not isinstance(x_simcore_parent_node_id, NodeID): - x_simcore_parent_node_id = None + parent_project_uuid = ( + x_simcore_parent_project_uuid + if isinstance(x_simcore_parent_project_uuid, ProjectID) + else None + ) + parent_node_id = ( + x_simcore_parent_node_id + if isinstance(x_simcore_parent_node_id, NodeID) + else None + ) # Make sure the user is allowed to execute any function # (read/write right is checked in the other endpoint called in this method) @@ -455,8 +461,8 @@ async def run_function( # noqa: PLR0913 webserver_api=webserver_api, wb_api_rpc=wb_api_rpc, url_for=url_for, - x_simcore_parent_project_uuid=x_simcore_parent_project_uuid, - x_simcore_parent_node_id=x_simcore_parent_node_id, + x_simcore_parent_project_uuid=parent_project_uuid, + x_simcore_parent_node_id=parent_node_id, user_id=user_id, product_name=product_name, ) @@ -490,8 +496,8 @@ async def run_function( # noqa: PLR0913 solver_service=solver_service, job_service=job_service, url_for=url_for, - x_simcore_parent_project_uuid=x_simcore_parent_project_uuid, - x_simcore_parent_node_id=x_simcore_parent_node_id, + x_simcore_parent_project_uuid=parent_project_uuid, + x_simcore_parent_node_id=parent_node_id, ) await solvers_jobs.start_job( request=request, From 134fbfbbda1e149822e6ae7483dc7cef35818a56 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 13:26:42 +0200 Subject: [PATCH 11/15] minor cleanup --- services/api-server/openapi.json | 3 --- .../simcore_service_api_server/api/routes/functions_routes.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index e6471771139c..5547b5ae3191 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -7731,9 +7731,6 @@ { "type": "string", "pattern": "^null$" - }, - { - "type": "null" } ], "title": "X-Simcore-Parent-Project-Uuid" 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 6e09b1bd978c..625ed3d0c2fa 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 @@ -576,7 +576,7 @@ async def map_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], - x_simcore_parent_project_uuid: Annotated[ProjectID | NullString | None, Header()], + x_simcore_parent_project_uuid: Annotated[ProjectID | NullString, Header()], x_simcore_parent_node_id: Annotated[NodeID | NullString, Header()], ) -> RegisteredFunctionJobCollection: function_jobs = [] From cdc12314f43fc31845b38964b95daa916618da37 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 14:04:32 +0200 Subject: [PATCH 12/15] make pylint happy --- .../tests/unit/api_functions/test_api_routers_functions.py | 2 ++ 1 file changed, 2 insertions(+) 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 8b8a97d23ddd..98acc73352d1 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,4 +1,6 @@ # pylint: disable=unused-argument +# pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments # pylint: disable=redefined-outer-name import datetime From 75e100f49591814d509daad89bc010e6e4ee63ab Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 14:23:34 +0200 Subject: [PATCH 13/15] pylint --- .../simcore_service_api_server/api/routes/functions_routes.py | 1 + 1 file changed, 1 insertion(+) 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 625ed3d0c2fa..0a8499c98496 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 @@ -1,3 +1,4 @@ +# pylint: disable=too-many-positional-arguments from collections.abc import Callable from typing import Annotated, Final, TypeAlias From 46af347d14a053dff8dd6ca611a10f8111990f83 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Mon, 16 Jun 2025 14:25:45 +0200 Subject: [PATCH 14/15] use Literal @pcrespov --- .../api/routes/functions_routes.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 0a8499c98496..bbfeefc4efc0 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 @@ -1,6 +1,6 @@ # pylint: disable=too-many-positional-arguments from collections.abc import Callable -from typing import Annotated, Final, TypeAlias +from typing import Annotated, Final, Literal import jsonschema from fastapi import APIRouter, Depends, Header, Request, status @@ -34,7 +34,6 @@ from models_library.projects_nodes_io import NodeID from models_library.projects_state import RunningState from models_library.users import UserID -from pydantic import StringConstraints from servicelib.fastapi.dependencies import get_reverse_url_mapper from simcore_service_api_server._service_jobs import JobService @@ -60,8 +59,6 @@ # pylint: disable=too-many-arguments # pylint: disable=cyclic-import -NullString: TypeAlias = Annotated[str, StringConstraints(pattern="^null$")] - function_router = APIRouter() _COMMON_FUNCTION_ERROR_RESPONSES: Final[dict] = { @@ -378,8 +375,8 @@ async def run_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], - x_simcore_parent_project_uuid: Annotated[ProjectID | NullString, Header()], - x_simcore_parent_node_id: Annotated[NodeID | NullString, Header()], + x_simcore_parent_project_uuid: Annotated[ProjectID | Literal["null"], Header()], + x_simcore_parent_node_id: Annotated[NodeID | Literal["null"], Header()], ) -> RegisteredFunctionJob: parent_project_uuid = ( @@ -577,8 +574,8 @@ async def map_function( # noqa: PLR0913 product_name: Annotated[str, Depends(get_product_name)], solver_service: Annotated[SolverService, Depends(get_solver_service)], job_service: Annotated[JobService, Depends(get_job_service)], - x_simcore_parent_project_uuid: Annotated[ProjectID | NullString, Header()], - x_simcore_parent_node_id: Annotated[NodeID | NullString, Header()], + x_simcore_parent_project_uuid: Annotated[ProjectID | Literal["null"], Header()], + x_simcore_parent_node_id: Annotated[NodeID | Literal["null"], Header()], ) -> RegisteredFunctionJobCollection: function_jobs = [] function_jobs = [ From d273dfa6495373e0a7e47eb03678dd3babb75fb0 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Tue, 17 Jun 2025 09:13:55 +0200 Subject: [PATCH 15/15] update OAS --- services/api-server/openapi.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 5547b5ae3191..44c6ae3d16cb 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -7597,8 +7597,8 @@ "format": "uuid" }, { - "type": "string", - "pattern": "^null$" + "const": "null", + "type": "string" } ], "title": "X-Simcore-Parent-Project-Uuid" @@ -7615,8 +7615,8 @@ "format": "uuid" }, { - "type": "string", - "pattern": "^null$" + "const": "null", + "type": "string" } ], "title": "X-Simcore-Parent-Node-Id" @@ -7729,8 +7729,8 @@ "format": "uuid" }, { - "type": "string", - "pattern": "^null$" + "const": "null", + "type": "string" } ], "title": "X-Simcore-Parent-Project-Uuid" @@ -7747,8 +7747,8 @@ "format": "uuid" }, { - "type": "string", - "pattern": "^null$" + "const": "null", + "type": "string" } ], "title": "X-Simcore-Parent-Node-Id"