Skip to content

Commit 4ca666e

Browse files
authored
♻️ Refactor webserver projects: drops request dependency on service layer functions (#7228)
1 parent 5ac65f7 commit 4ca666e

File tree

11 files changed

+164
-132
lines changed

11 files changed

+164
-132
lines changed

.vscode/settings.template.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@
5959
},
6060
"hadolint.hadolintPath": "${workspaceFolder}/scripts/hadolint.bash",
6161
"hadolint.cliOptions": [],
62-
"ruff.lint.args": [
63-
"--config=${workspaceFolder}/.ruff.toml"
64-
],
62+
"ruff.configuration": "${workspaceFolder}/.ruff.toml",
6563
"ruff.path": [
6664
"${workspaceFolder}/.venv/bin/ruff"
6765
],

services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from . import _folders_db as project_to_folders_db
4343
from . import projects_service
4444
from ._metadata_api import set_project_ancestors
45-
from ._permalink_api import update_or_pop_permalink_in_project
45+
from ._permalink_service import update_or_pop_permalink_in_project
4646
from .db import ProjectDBAPI
4747
from .exceptions import (
4848
ParentNodeNotFoundError,

services/web/server/src/simcore_service_webserver/projects/_crud_api_delete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def delete_project(
7878
app: web.Application,
7979
project_uuid: ProjectID,
8080
user_id: UserID,
81-
simcore_user_agent,
81+
simcore_user_agent: str,
8282
remove_project_dynamic_services: RemoveProjectServicesCallable,
8383
) -> None:
8484
"""Stops dynamic services, deletes data and finally deletes project

services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py

Lines changed: 52 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
66
"""
77

8+
from typing import Any, Coroutine
9+
810
from aiohttp import web
911
from models_library.folders import FolderID, FolderQuery, FolderScope
1012
from models_library.projects import ProjectID
@@ -23,51 +25,65 @@
2325
from ..folders import _folders_repository as _folders_repository
2426
from ..workspaces._workspaces_service import check_user_workspace_access
2527
from . import projects_service
26-
from ._permalink_api import update_or_pop_permalink_in_project
2728
from .db import ProjectDBAPI
2829
from .models import ProjectDict, ProjectTypeAPI
2930

3031

31-
async def _update_project_dict(
32-
request: web.Request,
33-
*,
34-
user_id: UserID,
35-
project: ProjectDict,
36-
is_template: bool,
37-
) -> ProjectDict:
38-
# state
39-
await projects_service.add_project_states_for_user(
40-
user_id=user_id,
41-
project=project,
42-
is_template=is_template,
43-
app=request.app,
44-
)
32+
def _batch_update(
33+
key: str,
34+
value_per_object: list[Any],
35+
objects: list[dict[str, Any]],
36+
) -> list[dict[str, Any]]:
37+
for obj, value in zip(objects, value_per_object, strict=True):
38+
obj[key] = value
39+
return objects
4540

46-
# permalink
47-
await update_or_pop_permalink_in_project(request, project)
4841

49-
return project
42+
async def _paralell_update(*update_per_object: Coroutine) -> list[Any]:
43+
return await logged_gather(
44+
*update_per_object,
45+
reraise=True,
46+
max_concurrency=100,
47+
)
5048

5149

52-
async def _batch_update_list_of_project_dict(
53-
app: web.Application, list_of_project_dict: list[ProjectDict]
50+
async def _aggregate_data_to_projects_from_other_sources(
51+
app: web.Application,
52+
*,
53+
db_projects: list[ProjectDict],
54+
db_project_types: list[ProjectTypeDB],
55+
user_id: UserID,
5456
) -> list[ProjectDict]:
55-
56-
# updating `trashed_by_primary_gid`
57+
"""
58+
Aggregates data to each project from other sources, first as a batch-update and then as a parallel-update.
59+
"""
60+
# updating `project.trashed_by_primary_gid`
5761
trashed_by_primary_gid_values = await batch_get_trashed_by_primary_gid(
58-
app, projects_uuids=[ProjectID(p["uuid"]) for p in list_of_project_dict]
62+
app, projects_uuids=[ProjectID(p["uuid"]) for p in db_projects]
5963
)
6064

61-
for project_dict, value in zip(
62-
list_of_project_dict, trashed_by_primary_gid_values, strict=True
63-
):
64-
project_dict.update(trashed_by_primary_gid=value)
65+
_batch_update("trashed_by_primary_gid", trashed_by_primary_gid_values, db_projects)
66+
67+
# udpating `project.state`
68+
update_state_per_project = [
69+
projects_service.add_project_states_for_user(
70+
user_id=user_id,
71+
project=prj,
72+
is_template=prj_type == ProjectTypeDB.TEMPLATE,
73+
app=app,
74+
)
75+
for prj, prj_type in zip(db_projects, db_project_types, strict=False)
76+
]
77+
78+
updated_projects: list[ProjectDict] = await _paralell_update(
79+
*update_state_per_project,
80+
)
6581

66-
return list_of_project_dict
82+
return updated_projects
6783

6884

6985
async def list_projects( # pylint: disable=too-many-arguments
70-
request: web.Request,
86+
app: web.Application,
7187
user_id: UserID,
7288
product_name: str,
7389
*,
@@ -87,7 +103,6 @@ async def list_projects( # pylint: disable=too-many-arguments
87103
# ordering
88104
order_by: OrderBy,
89105
) -> tuple[list[ProjectDict], int]:
90-
app = request.app
91106
db = ProjectDBAPI.get_from_app_context(app)
92107

93108
user_available_services: list[dict] = await get_services_for_user_in_product(
@@ -145,27 +160,15 @@ async def list_projects( # pylint: disable=too-many-arguments
145160
order_by=order_by,
146161
)
147162

148-
db_projects = await _batch_update_list_of_project_dict(app, db_projects)
149-
150-
projects: list[ProjectDict] = await logged_gather(
151-
*(
152-
_update_project_dict(
153-
request,
154-
user_id=user_id,
155-
project=prj,
156-
is_template=prj_type == ProjectTypeDB.TEMPLATE,
157-
)
158-
for prj, prj_type in zip(db_projects, db_project_types, strict=False)
159-
),
160-
reraise=True,
161-
max_concurrency=100,
163+
projects = await _aggregate_data_to_projects_from_other_sources(
164+
app, db_projects=db_projects, db_project_types=db_project_types, user_id=user_id
162165
)
163166

164167
return projects, total_number_projects
165168

166169

167170
async def list_projects_full_depth(
168-
request,
171+
app: web.Application,
169172
*,
170173
user_id: UserID,
171174
product_name: str,
@@ -180,10 +183,10 @@ async def list_projects_full_depth(
180183
search_by_multi_columns: str | None,
181184
search_by_project_name: str | None,
182185
) -> tuple[list[ProjectDict], int]:
183-
db = ProjectDBAPI.get_from_app_context(request.app)
186+
db = ProjectDBAPI.get_from_app_context(app)
184187

185188
user_available_services: list[dict] = await get_services_for_user_in_product(
186-
request.app, user_id, product_name, only_key_versions=True
189+
app, user_id, product_name, only_key_versions=True
187190
)
188191

189192
db_projects, db_project_types, total_number_projects = await db.list_projects_dicts(
@@ -202,30 +205,8 @@ async def list_projects_full_depth(
202205
order_by=order_by,
203206
)
204207

205-
db_projects = await _batch_update_list_of_project_dict(request.app, db_projects)
206-
207-
projects: list[ProjectDict] = await logged_gather(
208-
*(
209-
_update_project_dict(
210-
request,
211-
user_id=user_id,
212-
project=prj,
213-
is_template=prj_type == ProjectTypeDB.TEMPLATE,
214-
)
215-
for prj, prj_type in zip(db_projects, db_project_types, strict=False)
216-
),
217-
reraise=True,
218-
max_concurrency=100,
208+
projects = await _aggregate_data_to_projects_from_other_sources(
209+
app, db_projects=db_projects, db_project_types=db_project_types, user_id=user_id
219210
)
220211

221212
return projects, total_number_projects
222-
223-
224-
async def get_project(
225-
request: web.Request,
226-
user_id: UserID,
227-
product_name: str,
228-
project_uuid: ProjectID,
229-
project_type: ProjectTypeAPI,
230-
):
231-
raise NotImplementedError

services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
1414
ProjectCopyOverride,
1515
ProjectCreateNew,
1616
ProjectGet,
17-
ProjectListItem,
1817
ProjectPatch,
1918
)
2019
from models_library.generics import Envelope
2120
from models_library.projects_state import ProjectLocked
2221
from models_library.rest_ordering import OrderBy
23-
from models_library.rest_pagination import Page
24-
from models_library.rest_pagination_utils import paginate_data
2522
from models_library.utils.fastapi_encoders import jsonable_encoder
2623
from servicelib.aiohttp import status
2724
from servicelib.aiohttp.long_running_tasks.server import start_long_running_task
@@ -36,9 +33,7 @@
3633
UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
3734
X_SIMCORE_USER_AGENT,
3835
)
39-
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
4036
from servicelib.redis import get_project_locked_state
41-
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
4237
from simcore_service_webserver.projects.models import ProjectDict
4338

4439
from .._meta import API_VTAG as VTAG
@@ -51,7 +46,7 @@
5146
from ..security.decorators import permission_required
5247
from ..users.api import get_user_fullname
5348
from ..workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError
54-
from . import _crud_api_create, _crud_api_read, projects_service
49+
from . import _crud_api_create, _crud_api_read, _crud_handlers_utils, projects_service
5550
from ._common.models import ProjectPathParams, RequestContext
5651
from ._crud_handlers_models import (
5752
ProjectActiveQueryParams,
@@ -61,7 +56,7 @@
6156
ProjectsListQueryParams,
6257
ProjectsSearchQueryParams,
6358
)
64-
from ._permalink_api import update_or_pop_permalink_in_project
59+
from ._permalink_service import update_or_pop_permalink_in_project
6560
from .exceptions import (
6661
ProjectDeleteError,
6762
ProjectInvalidRightsError,
@@ -167,27 +162,6 @@ async def create_project(request: web.Request):
167162
)
168163

169164

170-
def _create_page_response(projects, request_url, total, limit, offset) -> web.Response:
171-
page = Page[ProjectListItem].model_validate(
172-
paginate_data(
173-
chunk=[
174-
ProjectListItem.from_domain_model(prj).model_dump(
175-
by_alias=True, exclude_unset=True
176-
)
177-
for prj in projects
178-
],
179-
request_url=request_url,
180-
total=total,
181-
limit=limit,
182-
offset=offset,
183-
)
184-
)
185-
return web.Response(
186-
text=page.model_dump_json(**RESPONSE_MODEL_POLICY),
187-
content_type=MIMETYPE_APPLICATION_JSON,
188-
)
189-
190-
191165
@routes.get(f"/{VTAG}/projects", name="list_projects")
192166
@login_required
193167
@permission_required("project.read")
@@ -213,7 +187,7 @@ async def list_projects(request: web.Request):
213187
assert query_params.filters # nosec
214188

215189
projects, total_number_of_projects = await _crud_api_read.list_projects(
216-
request,
190+
request.app,
217191
user_id=req_ctx.user_id,
218192
product_name=req_ctx.product_name,
219193
project_type=query_params.project_type,
@@ -228,7 +202,11 @@ async def list_projects(request: web.Request):
228202
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
229203
)
230204

231-
return _create_page_response(
205+
projects = await _crud_handlers_utils.aggregate_data_to_projects_from_request(
206+
request, projects
207+
)
208+
209+
return _crud_handlers_utils.create_page_response(
232210
projects=projects,
233211
request_url=request.url,
234212
total=total_number_of_projects,
@@ -252,7 +230,7 @@ async def list_projects_full_search(request: web.Request):
252230
tag_ids_list = query_params.tag_ids_list()
253231

254232
projects, total_number_of_projects = await _crud_api_read.list_projects_full_depth(
255-
request,
233+
request.app,
256234
user_id=req_ctx.user_id,
257235
product_name=req_ctx.product_name,
258236
trashed=query_params.filters.trashed,
@@ -264,7 +242,11 @@ async def list_projects_full_search(request: web.Request):
264242
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
265243
)
266244

267-
return _create_page_response(
245+
projects = await _crud_handlers_utils.aggregate_data_to_projects_from_request(
246+
request, projects
247+
)
248+
249+
return _crud_handlers_utils.create_page_response(
268250
projects=projects,
269251
request_url=request.url,
270252
total=total_number_of_projects,
@@ -486,9 +468,9 @@ async def delete_project(request: web.Request):
486468

487469
await projects_service.submit_delete_project_task(
488470
request.app,
489-
path_params.project_id,
490-
req_ctx.user_id,
491-
request.headers.get(
471+
project_uuid=path_params.project_id,
472+
user_id=req_ctx.user_id,
473+
simcore_user_agent=request.headers.get(
492474
X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
493475
),
494476
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from aiohttp import web
2+
from models_library.api_schemas_webserver.projects import ProjectListItem
3+
from models_library.rest_pagination import Page
4+
from models_library.rest_pagination_utils import paginate_data
5+
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
6+
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
7+
8+
from . import _permalink_service
9+
from ._crud_api_read import _paralell_update
10+
from .models import ProjectDict
11+
12+
13+
async def aggregate_data_to_projects_from_request(
14+
request: web.Request,
15+
projects: list[ProjectDict],
16+
) -> list[ProjectDict]:
17+
18+
update_permalink_per_project = [
19+
# permalink
20+
_permalink_service.aggregate_permalink_in_project(request, project=prj)
21+
for prj in projects
22+
]
23+
24+
updated_projects: list[ProjectDict] = await _paralell_update(
25+
*update_permalink_per_project,
26+
)
27+
return updated_projects
28+
29+
30+
def create_page_response(projects, request_url, total, limit, offset) -> web.Response:
31+
page = Page[ProjectListItem].model_validate(
32+
paginate_data(
33+
chunk=[
34+
ProjectListItem.from_domain_model(prj).model_dump(
35+
by_alias=True, exclude_unset=True
36+
)
37+
for prj in projects
38+
],
39+
request_url=request_url,
40+
total=total,
41+
limit=limit,
42+
offset=offset,
43+
)
44+
)
45+
return web.Response(
46+
text=page.model_dump_json(**RESPONSE_MODEL_POLICY),
47+
content_type=MIMETYPE_APPLICATION_JSON,
48+
)

0 commit comments

Comments
 (0)