Skip to content

Commit f6efa8e

Browse files
♻️ Maintenance/remove legacy db listing code (#7889)
1 parent a5afe8d commit f6efa8e

File tree

8 files changed

+125
-81
lines changed

8 files changed

+125
-81
lines changed

packages/models-library/src/models_library/projects.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
from uuid import UUID
99

1010
from common_library.basic_types import DEFAULT_FACTORY
11-
from models_library.basic_types import ConstrainedStr
12-
from models_library.folders import FolderID
13-
from models_library.workspaces import WorkspaceID
1411
from pydantic import (
1512
BaseModel,
1613
ConfigDict,
@@ -21,8 +18,11 @@
2118
)
2219

2320
from .basic_regex import DATE_RE, UUID_RE_BASE
21+
from .basic_types import ConstrainedStr
2422
from .emails import LowerCaseEmailStr
23+
from .folders import FolderID
2524
from .groups import GroupID
25+
from .products import ProductName
2626
from .projects_access import AccessRights, GroupIDStr
2727
from .projects_nodes import Node
2828
from .projects_nodes_io import NodeIDStr
@@ -33,6 +33,7 @@
3333
none_to_empty_str_pre_validator,
3434
)
3535
from .utils.enums import StrAutoEnum
36+
from .workspaces import WorkspaceID
3637

3738
ProjectID: TypeAlias = UUID
3839
CommitID: TypeAlias = int
@@ -147,6 +148,25 @@ def _convert_sql_alchemy_enum(cls, v):
147148
)
148149

149150

151+
class ProjectListAtDB(BaseProjectModel):
152+
id: int
153+
type: ProjectType
154+
template_type: ProjectTemplateType | None
155+
prj_owner: int | None
156+
ui: dict[str, Any] | None
157+
classifiers: list[ClassifierID] | None
158+
dev: dict[str, Any] | None
159+
quality: dict[str, Any]
160+
published: bool | None
161+
hidden: bool
162+
workspace_id: WorkspaceID | None
163+
trashed: datetime | None
164+
trashed_by: UserID | None
165+
trashed_explicitly: bool
166+
product_name: ProductName
167+
folder_id: FolderID | None
168+
169+
150170
class Project(BaseProjectModel):
151171
# NOTE: This is the pydantic pendant of project-v0.0.1.json used in the API of the webserver/webclient
152172
# NOT for usage with DB!!

packages/models-library/src/models_library/projects_nodes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class Node(BaseModel):
168168
),
169169
] = None
170170

171-
thumbnail: Annotated[
171+
thumbnail: Annotated[ # <-- (DEPRECATED) Can be removed
172172
str | HttpUrl | None,
173173
Field(
174174
description="url of the latest screenshot of the node",
@@ -232,18 +232,18 @@ class Node(BaseModel):
232232
] = DEFAULT_FACTORY
233233

234234
output_node: Annotated[bool | None, Field(deprecated=True, alias="outputNode")] = (
235-
None
235+
None # <-- (DEPRECATED) Can be removed
236236
)
237237

238-
output_nodes: Annotated[
238+
output_nodes: Annotated[ # <-- (DEPRECATED) Can be removed
239239
list[NodeID] | None,
240240
Field(
241241
description="Used in group-nodes. Node IDs of those connected to the output",
242242
alias="outputNodes",
243243
),
244244
] = None
245245

246-
parent: Annotated[
246+
parent: Annotated[ # <-- (DEPRECATED) Can be removed
247247
NodeID | None,
248248
Field(
249249
description="Parent's (group-nodes') node ID s. Used to group",

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

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from aiohttp import web
1212
from models_library.folders import FolderID, FolderQuery, FolderScope
13-
from models_library.projects import ProjectID, ProjectTemplateType
13+
from models_library.projects import ProjectTemplateType
1414
from models_library.rest_ordering import OrderBy
1515
from models_library.users import UserID
1616
from models_library.workspaces import WorkspaceID, WorkspaceQuery, WorkspaceScope
@@ -23,11 +23,13 @@
2323
from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB
2424

2525
from ..folders import _folders_repository
26+
from ..users.api import get_user_email_legacy
2627
from ..workspaces.api import check_user_workspace_access
2728
from . import _projects_service
2829
from ._access_rights_repository import batch_get_project_access_rights
2930
from ._projects_repository import batch_get_trashed_by_primary_gid
3031
from ._projects_repository_legacy import ProjectDBAPI
32+
from ._projects_repository_legacy_utils import convert_to_schema_names
3133
from .models import ProjectDict, ProjectTypeAPI
3234

3335

@@ -53,15 +55,14 @@ async def _aggregate_data_to_projects_from_other_sources(
5355
app: web.Application,
5456
*,
5557
db_projects: list[ProjectDict],
56-
db_project_types: list[ProjectTypeDB],
5758
user_id: UserID,
5859
) -> list[ProjectDict]:
5960
"""
6061
Aggregates data to each project from other sources, first as a batch-update and then as a parallel-update.
6162
"""
6263
# updating `project.trashed_by_primary_gid`
6364
trashed_by_primary_gid_values = await batch_get_trashed_by_primary_gid(
64-
app, projects_uuids=[ProjectID(p["uuid"]) for p in db_projects]
65+
app, projects_uuids=[p["uuid"] for p in db_projects]
6566
)
6667

6768
_batch_update("trashed_by_primary_gid", trashed_by_primary_gid_values, db_projects)
@@ -70,7 +71,7 @@ async def _aggregate_data_to_projects_from_other_sources(
7071
project_to_access_rights = await batch_get_project_access_rights(
7172
app=app,
7273
projects_uuids_with_workspace_id=[
73-
(ProjectID(p["uuid"]), p["workspaceId"]) for p in db_projects
74+
(p["uuid"], p["workspaceId"]) for p in db_projects
7475
],
7576
)
7677

@@ -79,22 +80,40 @@ async def _aggregate_data_to_projects_from_other_sources(
7980
_projects_service.add_project_states_for_user(
8081
user_id=user_id,
8182
project=prj,
82-
is_template=prj_type == ProjectTypeDB.TEMPLATE,
83+
is_template=prj["type"] == ProjectTypeDB.TEMPLATE.value,
8384
app=app,
8485
)
85-
for prj, prj_type in zip(db_projects, db_project_types, strict=False)
86+
for prj in db_projects
8687
]
8788

8889
updated_projects: list[ProjectDict] = await _paralell_update(
8990
*update_state_per_project,
9091
)
9192

9293
for project in updated_projects:
93-
project["accessRights"] = project_to_access_rights[project["uuid"]]
94+
project["accessRights"] = project_to_access_rights[f"{project['uuid']}"]
9495

9596
return updated_projects
9697

9798

99+
async def _legacy_convert_db_projects_to_api_projects(
100+
app: web.Application,
101+
db,
102+
db_projects: list[dict[str, Any]],
103+
) -> list[dict]:
104+
"""
105+
Converts db schema projects to API schema (legacy postprocessing).
106+
"""
107+
api_projects: list[dict] = []
108+
for db_prj in db_projects:
109+
db_prj_dict = db_prj
110+
db_prj_dict.pop("product_name", None)
111+
db_prj_dict["tags"] = await db.get_tags_by_project(project_id=f"{db_prj['id']}")
112+
user_email = await get_user_email_legacy(app, db_prj["prj_owner"])
113+
api_projects.append(convert_to_schema_names(db_prj_dict, user_email))
114+
return api_projects
115+
116+
98117
async def list_projects( # pylint: disable=too-many-arguments
99118
app: web.Application,
100119
user_id: UserID,
@@ -140,7 +159,7 @@ async def list_projects( # pylint: disable=too-many-arguments
140159
workspace_id=workspace_id,
141160
)
142161

143-
db_projects, db_project_types, total_number_projects = await db.list_projects_dicts(
162+
db_projects, total_number_projects = await db.list_projects_dicts(
144163
product_name=product_name,
145164
user_id=user_id,
146165
workspace_query=(
@@ -172,11 +191,15 @@ async def list_projects( # pylint: disable=too-many-arguments
172191
order_by=order_by,
173192
)
174193

175-
projects = await _aggregate_data_to_projects_from_other_sources(
176-
app, db_projects=db_projects, db_project_types=db_project_types, user_id=user_id
194+
api_projects = await _legacy_convert_db_projects_to_api_projects(
195+
app, db, db_projects
177196
)
178197

179-
return projects, total_number_projects
198+
final_projects = await _aggregate_data_to_projects_from_other_sources(
199+
app, db_projects=api_projects, user_id=user_id
200+
)
201+
202+
return final_projects, total_number_projects
180203

181204

182205
async def list_projects_full_depth(
@@ -196,7 +219,7 @@ async def list_projects_full_depth(
196219
) -> tuple[list[ProjectDict], int]:
197220
db = ProjectDBAPI.get_from_app_context(app)
198221

199-
db_projects, db_project_types, total_number_projects = await db.list_projects_dicts(
222+
db_projects, total_number_projects = await db.list_projects_dicts(
200223
product_name=product_name,
201224
user_id=user_id,
202225
workspace_query=WorkspaceQuery(workspace_scope=WorkspaceScope.ALL),
@@ -210,8 +233,12 @@ async def list_projects_full_depth(
210233
order_by=order_by,
211234
)
212235

213-
projects = await _aggregate_data_to_projects_from_other_sources(
214-
app, db_projects=db_projects, db_project_types=db_project_types, user_id=user_id
236+
api_projects = await _legacy_convert_db_projects_to_api_projects(
237+
app, db, db_projects
238+
)
239+
240+
final_projects = await _aggregate_data_to_projects_from_other_sources(
241+
app, db_projects=api_projects, user_id=user_id
215242
)
216243

217-
return projects, total_number_projects
244+
return final_projects, total_number_projects

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
from models_library.folders import FolderQuery, FolderScope
2020
from models_library.groups import GroupID
2121
from models_library.products import ProductName
22-
from models_library.projects import ProjectID, ProjectIDStr
22+
from models_library.projects import (
23+
ProjectID,
24+
ProjectIDStr,
25+
ProjectListAtDB,
26+
)
2327
from models_library.projects_comments import CommentID, ProjectsCommentsDB
2428
from models_library.projects_nodes import Node
2529
from models_library.projects_nodes_io import NodeID, NodeIDStr
@@ -584,7 +588,7 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st
584588
limit: int | None = None,
585589
# order
586590
order_by: OrderBy = DEFAULT_ORDER_BY,
587-
) -> tuple[list[ProjectDict], list[ProjectType], int]:
591+
) -> tuple[list[dict[str, Any]], int]:
588592
async with self.engine.acquire() as conn:
589593
user_groups_proxy: list[RowProxy] = await self._list_user_groups(
590594
conn, user_id
@@ -667,14 +671,18 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st
667671
projects.c.id,
668672
)
669673

670-
prjs, prj_types = await self._execute_without_permission_check(
671-
conn,
672-
select_projects_query=combined_query.offset(offset).limit(limit),
673-
)
674+
prjs_output = []
675+
async for row in conn.execute(combined_query.offset(offset).limit(limit)):
676+
# NOTE: Historically, projects were returned as a dictionary. I have created a model that
677+
# validates the DB row, but this model includes some default values inside the Workbench Node model.
678+
# Therefore, if we use this model, it will return those default values, which is not backward-compatible
679+
# with the frontend. The frontend would need to check and adapt how it handles default values in
680+
# Workbench nodes, which are currently not returned if not set in the DB.
681+
ProjectListAtDB.model_validate(row)
682+
prjs_output.append(dict(row.items()))
674683

675684
return (
676-
prjs,
677-
prj_types,
685+
prjs_output,
678686
cast(int, total_count),
679687
)
680688

@@ -1249,6 +1257,13 @@ async def remove_tag(
12491257
project["tags"].remove(tag_id)
12501258
return convert_to_schema_names(project, user_email)
12511259

1260+
async def get_tags_by_project(self, project_id: str) -> list[int]:
1261+
async with self.engine.acquire() as conn:
1262+
query = sa.select(projects_tags.c.tag_id).where(
1263+
projects_tags.c.project_id == project_id
1264+
)
1265+
return [row.tag_id async for row in conn.execute(query)]
1266+
12521267
#
12531268
# Project Comments
12541269
#

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

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import asyncio
21
import logging
32
from collections.abc import Mapping
43
from copy import deepcopy
@@ -9,23 +8,27 @@
98
import sqlalchemy as sa
109
from aiopg.sa.connection import SAConnection
1110
from aiopg.sa.result import RowProxy
12-
from models_library.projects import ProjectAtDB, ProjectID, ProjectTemplateType
11+
from models_library.projects import ProjectID, ProjectType
1312
from models_library.projects_nodes import Node
1413
from models_library.projects_nodes_io import NodeIDStr
1514
from models_library.utils.change_case import camel_to_snake, snake_to_camel
1615
from pydantic import ValidationError
1716
from simcore_postgres_database.models.project_to_groups import project_to_groups
18-
from simcore_postgres_database.webserver_models import ProjectType, projects
17+
from simcore_postgres_database.webserver_models import (
18+
ProjectTemplateType as ProjectTemplateTypeDB,
19+
)
20+
from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB
21+
from simcore_postgres_database.webserver_models import (
22+
projects,
23+
)
1924
from sqlalchemy.dialects.postgresql import insert as pg_insert
20-
from sqlalchemy.sql.selectable import CompoundSelect, Select
2125

2226
from ..db.models import GroupType, groups, projects_tags, user_to_groups, users
2327
from ..users.exceptions import UserNotFoundError
2428
from ..utils import format_datetime
2529
from ._projects_repository import PROJECT_DB_COLS
2630
from .exceptions import (
2731
NodeNotFoundError,
28-
ProjectInvalidRightsError,
2932
ProjectInvalidUsageError,
3033
ProjectNotFoundError,
3134
)
@@ -91,9 +94,9 @@ def convert_to_schema_names(
9194
if col_name == "prj_owner":
9295
# this entry has to be converted to the owner e-mail address
9396
converted_value = user_email
94-
if col_name == "type" and isinstance(col_value, ProjectType):
97+
if col_name == "type" and isinstance(col_value, ProjectTypeDB):
9598
converted_value = col_value.value
96-
if col_name == "template_type" and isinstance(col_value, ProjectTemplateType):
99+
if col_name == "template_type" and isinstance(col_value, ProjectTemplateTypeDB):
97100
converted_value = col_value.value
98101

99102
if col_name in SCHEMA_NON_NULL_KEYS and col_value is None:
@@ -184,50 +187,6 @@ async def _upsert_tags_in_project(
184187
.on_conflict_do_nothing()
185188
)
186189

187-
async def _execute_without_permission_check(
188-
self,
189-
conn: SAConnection,
190-
*,
191-
select_projects_query: Select | CompoundSelect,
192-
) -> tuple[list[dict[str, Any]], list[ProjectType]]:
193-
api_projects: list[dict] = [] # API model-compatible projects
194-
db_projects: list[dict] = [] # DB model-compatible projects
195-
project_types: list[ProjectType] = []
196-
async for row in conn.execute(select_projects_query):
197-
assert isinstance(row, RowProxy) # nosec
198-
try:
199-
await asyncio.get_event_loop().run_in_executor(
200-
None, ProjectAtDB.model_validate, row
201-
)
202-
203-
except ProjectInvalidRightsError:
204-
continue
205-
206-
except ValidationError as exc:
207-
logger.warning(
208-
"project %s failed validation, please check. error: %s",
209-
f"{row.id=}",
210-
exc,
211-
)
212-
continue
213-
214-
prj: dict[str, Any] = dict(row.items())
215-
prj.pop("product_name", None)
216-
217-
db_projects.append(prj)
218-
219-
# NOTE: DO NOT nest _get_tags_by_project in async loop above !!!
220-
# FIXME: temporary avoids inner async loops issue https://github.com/aio-libs/aiopg/issues/535
221-
for db_prj in db_projects:
222-
db_prj["tags"] = await self._get_tags_by_project(
223-
conn, project_id=db_prj["id"]
224-
)
225-
user_email = await self._get_user_email(conn, db_prj["prj_owner"])
226-
api_projects.append(convert_to_schema_names(db_prj, user_email))
227-
project_types.append(db_prj["type"])
228-
229-
return (api_projects, project_types)
230-
231190
async def _get_project(
232191
self,
233192
connection: SAConnection,

0 commit comments

Comments
 (0)