Skip to content

Commit 00b0944

Browse files
🎨 propagate job parent ids through api server (#5903)
1 parent dd7da65 commit 00b0944

File tree

8 files changed

+225
-13
lines changed

8 files changed

+225
-13
lines changed

services/api-server/openapi.json

+60
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,26 @@
20382038
},
20392039
"name": "hidden",
20402040
"in": "query"
2041+
},
2042+
{
2043+
"required": false,
2044+
"schema": {
2045+
"type": "string",
2046+
"format": "uuid",
2047+
"title": "X-Simcore-Parent-Project-Uuid"
2048+
},
2049+
"name": "x-simcore-parent-project-uuid",
2050+
"in": "header"
2051+
},
2052+
{
2053+
"required": false,
2054+
"schema": {
2055+
"type": "string",
2056+
"format": "uuid",
2057+
"title": "X-Simcore-Parent-Node-Id"
2058+
},
2059+
"name": "x-simcore-parent-node-id",
2060+
"in": "header"
20412061
}
20422062
],
20432063
"requestBody": {
@@ -3254,6 +3274,26 @@
32543274
},
32553275
"name": "study_id",
32563276
"in": "path"
3277+
},
3278+
{
3279+
"required": false,
3280+
"schema": {
3281+
"type": "string",
3282+
"format": "uuid",
3283+
"title": "X-Simcore-Parent-Project-Uuid"
3284+
},
3285+
"name": "x-simcore-parent-project-uuid",
3286+
"in": "header"
3287+
},
3288+
{
3289+
"required": false,
3290+
"schema": {
3291+
"type": "string",
3292+
"format": "uuid",
3293+
"title": "X-Simcore-Parent-Node-Id"
3294+
},
3295+
"name": "x-simcore-parent-node-id",
3296+
"in": "header"
32573297
}
32583298
],
32593299
"responses": {
@@ -3382,6 +3422,26 @@
33823422
},
33833423
"name": "hidden",
33843424
"in": "query"
3425+
},
3426+
{
3427+
"required": false,
3428+
"schema": {
3429+
"type": "string",
3430+
"format": "uuid",
3431+
"title": "X-Simcore-Parent-Project-Uuid"
3432+
},
3433+
"name": "x-simcore-parent-project-uuid",
3434+
"in": "header"
3435+
},
3436+
{
3437+
"required": false,
3438+
"schema": {
3439+
"type": "string",
3440+
"format": "uuid",
3441+
"title": "X-Simcore-Parent-Node-Id"
3442+
},
3443+
"name": "x-simcore-parent-node-id",
3444+
"in": "header"
33853445
}
33863446
],
33873447
"requestBody": {

services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from collections.abc import Callable
55
from typing import Annotated, Any
66

7-
from fastapi import APIRouter, Depends, Query, Request, status
7+
from fastapi import APIRouter, Depends, Header, Query, Request, status
88
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
99
from models_library.clusters import ClusterID
10+
from models_library.projects import ProjectID
11+
from models_library.projects_nodes_io import NodeID
1012
from pydantic.types import PositiveInt
1113

1214
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
@@ -87,6 +89,8 @@ async def create_job(
8789
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
8890
product_name: Annotated[str, Depends(get_product_name)],
8991
hidden: Annotated[bool, Query()] = True,
92+
x_simcore_parent_project_uuid: Annotated[ProjectID | None, Header()] = None,
93+
x_simcore_parent_node_id: Annotated[NodeID | None, Header()] = None,
9094
):
9195
"""Creates a job in a specific release with given inputs.
9296
@@ -107,7 +111,10 @@ async def create_job(
107111

108112
project_in: ProjectCreateNew = create_new_project_for_job(solver, pre_job, inputs)
109113
new_project: ProjectGet = await webserver_api.create_project(
110-
project_in, is_hidden=hidden
114+
project_in,
115+
is_hidden=hidden,
116+
parent_project_uuid=x_simcore_parent_project_uuid,
117+
parent_node_id=x_simcore_parent_node_id,
111118
)
112119
assert new_project # nosec
113120
assert new_project.uuid == pre_job.id # nosec

services/api-server/src/simcore_service_api_server/api/routes/studies.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
22
from typing import Annotated, Final
33

4-
from fastapi import APIRouter, Depends, status
4+
from fastapi import APIRouter, Depends, Header, status
55
from fastapi_pagination.api import create_page
66
from models_library.api_schemas_webserver.projects import ProjectGet
7+
from models_library.projects import ProjectID
8+
from models_library.projects_nodes_io import NodeID
79

810
from ...models.pagination import OnePage, Page, PaginationParams
911
from ...models.schemas.errors import ErrorGet
@@ -85,9 +87,14 @@ async def get_study(
8587
async def clone_study(
8688
study_id: StudyID,
8789
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
90+
x_simcore_parent_project_uuid: Annotated[ProjectID | None, Header()] = None,
91+
x_simcore_parent_node_id: Annotated[NodeID | None, Header()] = None,
8892
):
8993
project: ProjectGet = await webserver_api.clone_project(
90-
project_id=study_id, hidden=False
94+
project_id=study_id,
95+
hidden=False,
96+
parent_project_uuid=x_simcore_parent_project_uuid,
97+
parent_node_id=x_simcore_parent_node_id,
9198
)
9299
return _create_study_from_project(project)
93100

services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from collections.abc import Callable
33
from typing import Annotated
44

5-
from fastapi import APIRouter, Depends, Query, Request, status
5+
from fastapi import APIRouter, Depends, Header, Query, Request, status
66
from fastapi.responses import RedirectResponse
77
from models_library.api_schemas_webserver.projects import ProjectName, ProjectPatch
88
from models_library.api_schemas_webserver.projects_nodes import NodeOutputs
99
from models_library.clusters import ClusterID
1010
from models_library.function_services_catalog.services import file_picker
11+
from models_library.projects import ProjectID
1112
from models_library.projects_nodes import InputID, InputTypes
13+
from models_library.projects_nodes_io import NodeID
1214
from pydantic import PositiveInt
1315
from servicelib.logging_utils import log_context
1416

@@ -81,11 +83,18 @@ async def create_study_job(
8183
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
8284
url_for: Annotated[Callable, Depends(get_reverse_url_mapper)],
8385
hidden: Annotated[bool, Query()] = True,
86+
x_simcore_parent_project_uuid: ProjectID | None = Header(default=None),
87+
x_simcore_parent_node_id: NodeID | None = Header(default=None),
8488
) -> Job:
8589
"""
8690
hidden -- if True (default) hides project from UI
8791
"""
88-
project = await webserver_api.clone_project(project_id=study_id, hidden=hidden)
92+
project = await webserver_api.clone_project(
93+
project_id=study_id,
94+
hidden=hidden,
95+
parent_project_uuid=x_simcore_parent_project_uuid,
96+
parent_node_id=x_simcore_parent_node_id,
97+
)
8998
job = create_job_from_study(
9099
study_key=study_id, project=project, job_inputs=job_inputs
91100
)

services/api-server/src/simcore_service_api_server/services/webserver.py

+32-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
from models_library.utils.fastapi_encoders import jsonable_encoder
4747
from pydantic import PositiveInt
4848
from servicelib.aiohttp.long_running_tasks.server import TaskStatus
49+
from servicelib.common_headers import (
50+
X_SIMCORE_PARENT_NODE_ID,
51+
X_SIMCORE_PARENT_PROJECT_UUID,
52+
)
4953
from tenacity import TryAgain
5054
from tenacity._asyncio import AsyncRetrying
5155
from tenacity.before_sleep import before_sleep_log
@@ -253,12 +257,22 @@ async def update_me(self, profile_update: ProfileUpdate) -> Profile:
253257

254258
@_exception_mapper({})
255259
async def create_project(
256-
self, project: ProjectCreateNew, *, is_hidden: bool
260+
self,
261+
project: ProjectCreateNew,
262+
*,
263+
is_hidden: bool,
264+
parent_project_uuid: ProjectID | None,
265+
parent_node_id: NodeID | None,
257266
) -> ProjectGet:
258267
# POST /projects --> 202 Accepted
268+
_headers = {
269+
X_SIMCORE_PARENT_PROJECT_UUID: parent_project_uuid,
270+
X_SIMCORE_PARENT_NODE_ID: parent_node_id,
271+
}
259272
response = await self.client.post(
260273
"/projects",
261274
params={"hidden": is_hidden},
275+
headers={k: f"{v}" for k, v in _headers.items() if v is not None},
262276
json=jsonable_encoder(project, by_alias=True, exclude={"state"}),
263277
cookies=self.session_cookies,
264278
)
@@ -267,10 +281,25 @@ async def create_project(
267281
return ProjectGet.parse_obj(result)
268282

269283
@_exception_mapper(_JOB_STATUS_MAP)
270-
async def clone_project(self, *, project_id: UUID, hidden: bool) -> ProjectGet:
284+
async def clone_project(
285+
self,
286+
*,
287+
project_id: UUID,
288+
hidden: bool,
289+
parent_project_uuid: ProjectID | None,
290+
parent_node_id: NodeID | None,
291+
) -> ProjectGet:
271292
query = {"from_study": project_id, "hidden": hidden}
293+
_headers = {
294+
X_SIMCORE_PARENT_PROJECT_UUID: parent_project_uuid,
295+
X_SIMCORE_PARENT_NODE_ID: parent_node_id,
296+
}
297+
272298
response = await self.client.post(
273-
"/projects", cookies=self.session_cookies, params=query
299+
"/projects",
300+
cookies=self.session_cookies,
301+
params=query,
302+
headers={k: f"{v}" for k, v in _headers.items() if v is not None},
274303
)
275304
response.raise_for_status()
276305
result = await self._wait_for_long_running_task_results(response)

services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_delete.py

+32-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pathlib import Path
66
from typing import TypedDict
7+
from uuid import UUID
78

89
import httpx
910
import jinja2
@@ -13,9 +14,15 @@
1314
from pydantic import parse_file_as
1415
from pytest_simcore.helpers.httpx_calls_capture_models import HttpApiCallCaptureModel
1516
from respx import MockRouter
17+
from servicelib.common_headers import (
18+
X_SIMCORE_PARENT_NODE_ID,
19+
X_SIMCORE_PARENT_PROJECT_UUID,
20+
)
1621
from simcore_service_api_server.models.schemas.jobs import Job, JobInputs
1722
from starlette import status
1823

24+
_faker = Faker()
25+
1926

2027
class MockedBackendApiDict(TypedDict):
2128
catalog: MockRouter | None
@@ -158,6 +165,10 @@ async def test_create_and_delete_solver_job(
158165
# Run a job and delete when finished
159166

160167

168+
@pytest.mark.parametrize(
169+
"parent_node_id, parent_project_id",
170+
[(_faker.uuid4(), _faker.uuid4()), (None, None)],
171+
)
161172
@pytest.mark.parametrize("hidden", [True, False])
162173
async def test_create_job(
163174
auth: httpx.BasicAuth,
@@ -166,29 +177,47 @@ async def test_create_job(
166177
solver_version: str,
167178
mocked_backend_services_apis_for_create_and_delete_solver_job: MockedBackendApiDict,
168179
hidden: bool,
180+
parent_project_id: UUID | None,
181+
parent_node_id: UUID | None,
169182
):
170183

171184
mock_webserver_router = (
172185
mocked_backend_services_apis_for_create_and_delete_solver_job["webserver"]
173186
)
187+
assert mock_webserver_router is not None
174188
callback = mock_webserver_router["create_projects"].side_effect
189+
assert callback is not None
175190

176191
def create_project_side_effect(request: httpx.Request):
192+
# check `hidden` bool
177193
query = dict(elm.split("=") for elm in request.url.query.decode().split("&"))
178194
_hidden = query.get("hidden")
179195
assert _hidden == ("true" if hidden else "false")
196+
197+
# check parent project and node id
198+
if parent_project_id is not None:
199+
assert f"{parent_project_id}" == dict(request.headers).get(
200+
X_SIMCORE_PARENT_PROJECT_UUID.lower()
201+
)
202+
if parent_node_id is not None:
203+
assert f"{parent_node_id}" == dict(request.headers).get(
204+
X_SIMCORE_PARENT_NODE_ID.lower()
205+
)
180206
return callback(request)
181207

182-
mock_webserver_router = (
183-
mocked_backend_services_apis_for_create_and_delete_solver_job["webserver"]
184-
)
185208
mock_webserver_router["create_projects"].side_effect = create_project_side_effect
186209

187210
# create Job
211+
header_dict = {}
212+
if parent_project_id is not None:
213+
header_dict[X_SIMCORE_PARENT_PROJECT_UUID] = f"{parent_project_id}"
214+
if parent_node_id is not None:
215+
header_dict[X_SIMCORE_PARENT_NODE_ID] = f"{parent_node_id}"
188216
resp = await client.post(
189217
f"/v0/solvers/{solver_key}/releases/{solver_version}/jobs",
190218
auth=auth,
191219
params={"hidden": f"{hidden}"},
220+
headers=header_dict,
192221
json=JobInputs(
193222
values={
194223
"x": 3.14,

0 commit comments

Comments
 (0)