Skip to content

Commit d68e4e6

Browse files
authored
Merge branch 'master' into enh/purchase-for-one-year
2 parents e2b2719 + 1353d02 commit d68e4e6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1102
-656
lines changed

packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from datetime import datetime
2-
from typing import NamedTuple
2+
from typing import Annotated, Self
33

4-
from pydantic import ConfigDict, PositiveInt, field_validator
4+
from pydantic import ConfigDict, Field, field_validator
55

66
from ..access_rights import AccessRights
77
from ..basic_types import IDStr
8-
from ..folders import FolderID
8+
from ..folders import FolderDB, FolderID
99
from ..groups import GroupID
1010
from ..utils.common_validators import null_or_none_str_to_none_validator
1111
from ..workspaces import WorkspaceID
@@ -16,17 +16,40 @@ class FolderGet(OutputSchema):
1616
folder_id: FolderID
1717
parent_folder_id: FolderID | None = None
1818
name: str
19+
1920
created_at: datetime
2021
modified_at: datetime
2122
trashed_at: datetime | None
23+
trashed_by: Annotated[
24+
GroupID | None, Field(description="The primary gid of the user who trashed")
25+
]
2226
owner: GroupID
2327
workspace_id: WorkspaceID | None
2428
my_access_rights: AccessRights
2529

30+
@classmethod
31+
def from_domain_model(
32+
cls,
33+
folder_db: FolderDB,
34+
trashed_by_primary_gid: GroupID | None,
35+
user_folder_access_rights: AccessRights,
36+
) -> Self:
37+
if (folder_db.trashed_by is None) ^ (trashed_by_primary_gid is None):
38+
msg = f"Incompatible inputs: {folder_db.trashed_by=} but not {trashed_by_primary_gid=}"
39+
raise ValueError(msg)
2640

27-
class FolderGetPage(NamedTuple):
28-
items: list[FolderGet]
29-
total: PositiveInt
41+
return cls(
42+
folder_id=folder_db.folder_id,
43+
parent_folder_id=folder_db.parent_folder_id,
44+
name=folder_db.name,
45+
created_at=folder_db.created,
46+
modified_at=folder_db.modified,
47+
trashed_at=folder_db.trashed,
48+
trashed_by=trashed_by_primary_gid,
49+
owner=folder_db.created_by_gid,
50+
workspace_id=folder_db.workspace_id,
51+
my_access_rights=user_folder_access_rights,
52+
)
3053

3154

3255
class FolderCreateBodyParams(InputSchema):

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
66
"""
77

8+
import copy
89
from datetime import datetime
910
from typing import Annotated, Any, Literal, Self, TypeAlias
1011

1112
from common_library.dict_tools import remap_keys
12-
from models_library.folders import FolderID
13-
from models_library.utils._original_fastapi_encoders import jsonable_encoder
14-
from models_library.workspaces import WorkspaceID
1513
from pydantic import (
1614
BeforeValidator,
1715
ConfigDict,
@@ -25,10 +23,12 @@
2523
from ..basic_types import LongTruncatedStr, ShortTruncatedStr
2624
from ..emails import LowerCaseEmailStr
2725
from ..folders import FolderID
26+
from ..groups import GroupID
2827
from ..projects import ClassifierID, DateTimeStr, NodesDict, ProjectID
2928
from ..projects_access import AccessRights, GroupIDStr
3029
from ..projects_state import ProjectState
3130
from ..projects_ui import StudyUI
31+
from ..utils._original_fastapi_encoders import jsonable_encoder
3232
from ..utils.common_validators import (
3333
empty_str_to_none_pre_validator,
3434
none_to_empty_str_pre_validator,
@@ -98,6 +98,9 @@ class ProjectGet(OutputSchema):
9898
folder_id: FolderID | None
9999

100100
trashed_at: datetime | None
101+
trashed_by: Annotated[
102+
GroupID | None, Field(description="The primary gid of the user who trashed")
103+
]
101104

102105
_empty_description = field_validator("description", mode="before")(
103106
none_to_empty_str_pre_validator
@@ -107,10 +110,20 @@ class ProjectGet(OutputSchema):
107110

108111
@classmethod
109112
def from_domain_model(cls, project_data: dict[str, Any]) -> Self:
113+
trimmed_data = copy.copy(project_data)
114+
# project_data["trashed_by"] is a UserID
115+
# project_data["trashed_by_primary_gid"] is a GroupID
116+
trimmed_data.pop("trashed_by", None)
117+
trimmed_data.pop("trashedBy", None)
118+
110119
return cls.model_validate(
111120
remap_keys(
112-
project_data,
113-
rename={"trashed": "trashed_at"},
121+
trimmed_data,
122+
rename={
123+
"trashed": "trashed_at",
124+
"trashed_by_primary_gid": "trashed_by",
125+
"trashedByPrimaryGid": "trashedBy",
126+
},
114127
)
115128
)
116129

@@ -127,7 +140,8 @@ class ProjectReplace(InputSchema):
127140
name: ShortTruncatedStr
128141
description: LongTruncatedStr
129142
thumbnail: Annotated[
130-
HttpUrl | None, BeforeValidator(empty_str_to_none_pre_validator)
143+
HttpUrl | None,
144+
BeforeValidator(empty_str_to_none_pre_validator),
131145
] = Field(default=None)
132146
creation_date: DateTimeStr
133147
last_change_date: DateTimeStr

packages/models-library/src/models_library/api_schemas_webserver/workspaces.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from datetime import datetime
2-
from typing import Self
2+
from typing import Annotated, Self
33

4-
from pydantic import ConfigDict
4+
from pydantic import ConfigDict, Field
55

66
from ..access_rights import AccessRights
77
from ..basic_types import IDStr
88
from ..groups import GroupID
9-
from ..users import UserID
109
from ..workspaces import UserWorkspaceWithAccessRights, WorkspaceID
1110
from ._base import InputSchema, OutputSchema
1211

@@ -19,7 +18,9 @@ class WorkspaceGet(OutputSchema):
1918
created_at: datetime
2019
modified_at: datetime
2120
trashed_at: datetime | None
22-
trashed_by: UserID | None
21+
trashed_by: Annotated[
22+
GroupID | None, Field(description="The primary gid of the user who trashed")
23+
]
2324
my_access_rights: AccessRights
2425
access_rights: dict[GroupID, AccessRights]
2526

@@ -33,7 +34,7 @@ def from_domain_model(cls, wks: UserWorkspaceWithAccessRights) -> Self:
3334
created_at=wks.created,
3435
modified_at=wks.modified,
3536
trashed_at=wks.trashed,
36-
trashed_by=wks.trashed_by if wks.trashed else None,
37+
trashed_by=wks.trashed_by_primary_gid if wks.trashed else None,
3738
my_access_rights=wks.my_access_rights,
3839
access_rights=wks.access_rights,
3940
)
Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import TypeAlias
3+
from typing import NamedTuple, TypeAlias
44

5-
from pydantic import (
6-
BaseModel,
7-
ConfigDict,
8-
Field,
9-
PositiveInt,
10-
ValidationInfo,
11-
field_validator,
12-
)
5+
from pydantic import BaseModel, ConfigDict, PositiveInt, ValidationInfo, field_validator
136

147
from .access_rights import AccessRights
158
from .groups import GroupID
@@ -43,38 +36,31 @@ def validate_folder_id(cls, value, info: ValidationInfo):
4336
return value
4437

4538

46-
#
47-
# DB
48-
#
49-
50-
5139
class FolderDB(BaseModel):
5240
folder_id: FolderID
5341
name: str
5442
parent_folder_id: FolderID | None
55-
created_by_gid: GroupID = Field(
56-
...,
57-
description="GID of the group that owns this wallet",
58-
)
59-
created: datetime = Field(
60-
...,
61-
description="Timestamp on creation",
62-
)
63-
modified: datetime = Field(
64-
...,
65-
description="Timestamp of last modification",
66-
)
67-
trashed: datetime | None = Field(
68-
...,
69-
)
70-
71-
user_id: UserID | None
72-
workspace_id: WorkspaceID | None
7343

44+
created_by_gid: GroupID
45+
created: datetime
46+
modified: datetime
47+
48+
trashed: datetime | None
49+
trashed_by: UserID | None
50+
trashed_explicitly: bool
51+
52+
user_id: UserID | None # owner?
53+
workspace_id: WorkspaceID | None
7454
model_config = ConfigDict(from_attributes=True)
7555

7656

77-
class UserFolderAccessRightsDB(FolderDB):
57+
class UserFolder(FolderDB):
7858
my_access_rights: AccessRights
7959

8060
model_config = ConfigDict(from_attributes=True)
61+
62+
63+
class FolderTuple(NamedTuple):
64+
folder_db: FolderDB
65+
trashed_by_primary_gid: GroupID | None
66+
my_access_rights: AccessRights

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from .basic_regex import DATE_RE, UUID_RE_BASE
1717
from .emails import LowerCaseEmailStr
18+
from .groups import GroupID
1819
from .projects_access import AccessRights, GroupIDStr
1920
from .projects_nodes import Node
2021
from .projects_nodes_io import NodeIDStr
@@ -106,7 +107,7 @@ class ProjectAtDB(BaseProjectModel):
106107

107108
@field_validator("project_type", mode="before")
108109
@classmethod
109-
def convert_sql_alchemy_enum(cls, v):
110+
def _convert_sql_alchemy_enum(cls, v):
110111
if isinstance(v, Enum):
111112
return v.value
112113
return v
@@ -185,8 +186,12 @@ class Project(BaseProjectModel):
185186

186187
trashed: datetime | None = None
187188
trashed_by: Annotated[UserID | None, Field(alias="trashedBy")] = None
189+
trashed_by_primary_gid: Annotated[
190+
GroupID | None, Field(alias="trashedByPrimaryGid")
191+
] = None
188192
trashed_explicitly: Annotated[bool, Field(alias="trashedExplicitly")] = False
189193

190194
model_config = ConfigDict(
195+
# NOTE: this is a security measure until we get rid of the ProjectDict variants
191196
extra="forbid",
192197
)

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Workspace(BaseModel):
4747
workspace_id: WorkspaceID
4848
name: str
4949
description: str | None
50-
owner_primary_gid: PositiveInt = Field(
50+
owner_primary_gid: GroupID = Field(
5151
...,
5252
description="GID of the group that owns this wallet",
5353
)
@@ -62,6 +62,14 @@ class Workspace(BaseModel):
6262
)
6363
trashed: datetime | None
6464
trashed_by: UserID | None
65+
trashed_by_primary_gid: GroupID | None = None
66+
67+
model_config = ConfigDict(from_attributes=True)
68+
69+
70+
class UserWorkspaceWithAccessRights(Workspace):
71+
my_access_rights: AccessRights
72+
access_rights: dict[GroupID, AccessRights]
6573

6674
model_config = ConfigDict(from_attributes=True)
6775

@@ -72,10 +80,3 @@ class WorkspaceUpdates(BaseModel):
7280
thumbnail: str | None = None
7381
trashed: datetime | None = None
7482
trashed_by: UserID | None = None
75-
76-
77-
class UserWorkspaceWithAccessRights(Workspace):
78-
my_access_rights: AccessRights
79-
access_rights: dict[GroupID, AccessRights]
80-
81-
model_config = ConfigDict(from_attributes=True)

packages/models-library/tests/test_api_schemas_webserver_projects.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
@pytest.mark.parametrize(
3232
"api_call",
33-
(NEW_PROJECT, CREATE_FROM_SERVICE, CREATE_FROM_TEMPLATE),
33+
[NEW_PROJECT, CREATE_FROM_SERVICE, CREATE_FROM_TEMPLATE],
3434
ids=lambda c: c.name,
3535
)
3636
def test_create_project_schemas(api_call: HttpApiCallCapture):
@@ -45,7 +45,7 @@ def test_create_project_schemas(api_call: HttpApiCallCapture):
4545

4646
@pytest.mark.parametrize(
4747
"api_call",
48-
(LIST_PROJECTS,),
48+
[LIST_PROJECTS],
4949
ids=lambda c: c.name,
5050
)
5151
def test_list_project_schemas(api_call: HttpApiCallCapture):
@@ -59,7 +59,7 @@ def test_list_project_schemas(api_call: HttpApiCallCapture):
5959

6060
@pytest.mark.parametrize(
6161
"api_call",
62-
(GET_PROJECT, CREATE_FROM_TEMPLATE__TASK_RESULT),
62+
[GET_PROJECT, CREATE_FROM_TEMPLATE__TASK_RESULT],
6363
ids=lambda c: c.name,
6464
)
6565
def test_get_project_schemas(api_call: HttpApiCallCapture):
@@ -74,7 +74,7 @@ def test_get_project_schemas(api_call: HttpApiCallCapture):
7474

7575
@pytest.mark.parametrize(
7676
"api_call",
77-
(REPLACE_PROJECT, REPLACE_PROJECT_ON_MODIFIED),
77+
[REPLACE_PROJECT, REPLACE_PROJECT_ON_MODIFIED],
7878
ids=lambda c: c.name,
7979
)
8080
def test_replace_project_schemas(api_call: HttpApiCallCapture):

packages/models-library/tests/test_project_nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_create_minimal_node(minimal_node_data_sample: dict[str, Any]):
2525
# a nice way to see how the simplest node looks like
2626
assert node.inputs == {}
2727
assert node.outputs == {}
28+
assert node.state is not None
2829
assert node.state.current_status == RunningState.NOT_STARTED
2930
assert node.state.modified is True
3031
assert node.state.dependencies == set()

packages/postgres-database/src/simcore_postgres_database/models/projects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class ProjectType(enum.Enum):
9393
JSONB,
9494
nullable=False,
9595
server_default=sa.text("'{}'::jsonb"),
96-
doc="Read/write/delete access rights of each group (gid) on this project",
96+
doc="DEPRECATED: Read/write/delete access rights of each group (gid) on this project",
9797
),
9898
sa.Column(
9999
"workbench",

packages/postgres-database/src/simcore_postgres_database/utils_repos.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
22
from collections.abc import AsyncIterator
33
from contextlib import asynccontextmanager
4+
from typing import TypeVar
45

6+
import sqlalchemy as sa
7+
from pydantic import BaseModel
58
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
69

710
_logger = logging.getLogger(__name__)
@@ -56,3 +59,29 @@ async def transaction_context(
5659
finally:
5760
assert not conn.closed # nosec
5861
assert not conn.in_transaction() # nosec
62+
63+
64+
SQLModel = TypeVar(
65+
# Towards using https://sqlmodel.tiangolo.com/#create-a-sqlmodel-model
66+
"SQLModel",
67+
bound=BaseModel,
68+
)
69+
70+
71+
def get_columns_from_db_model(
72+
table: sa.Table, model_cls: type[SQLModel]
73+
) -> list[sa.Column]:
74+
"""
75+
Usage example:
76+
77+
query = sa.select( get_columns_from_db_model(project, ProjectDB) )
78+
79+
or
80+
81+
query = (
82+
project.insert().
83+
# ...
84+
.returning(*get_columns_from_db_model(project, ProjectDB))
85+
)
86+
"""
87+
return [table.columns[field_name] for field_name in model_cls.model_fields]

0 commit comments

Comments
 (0)