Skip to content

Commit c1da03c

Browse files
🎨 make folders optional + improvements ⚠️ (#6155)
1 parent b1416b4 commit c1da03c

File tree

20 files changed

+433
-80
lines changed

20 files changed

+433
-80
lines changed

.env-devel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ WEBSERVER_DIAGNOSTICS={}
318318
WEBSERVER_EMAIL={}
319319
WEBSERVER_EXPORTER={}
320320
WEBSERVER_FRONTEND={}
321+
WEBSERVER_FOLDERS=1
321322
WEBSERVER_GARBAGE_COLLECTOR=null
322323
WEBSERVER_GROUPS=1
323324
WEBSERVER_GUNICORN_CMD_ARGS=--timeout=180

api/specs/web-server/_folders.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
FolderGet,
1616
PutFolderBodyParams,
1717
)
18-
from models_library.api_schemas_webserver.wallets import WalletGet
1918
from models_library.generics import Envelope
2019
from models_library.rest_pagination import PageQueryParameters
2120
from pydantic import Json
@@ -39,7 +38,7 @@
3938

4039
@router.post(
4140
"/folders",
42-
response_model=Envelope[WalletGet],
41+
response_model=Envelope[FolderGet],
4342
status_code=status.HTTP_201_CREATED,
4443
)
4544
async def create_folder(_body: CreateFolderBodyParams):
@@ -55,10 +54,10 @@ async def list_folders(
5554
order_by: Annotated[
5655
Json,
5756
Query(
58-
description="Order by field (name|description) and direction (asc|desc). The default sorting order is ascending.",
57+
description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
5958
example='{"field": "name", "direction": "desc"}',
6059
),
61-
] = '{"field": "name", "direction": "desc"}',
60+
] = '{"field": "modified_at", "direction": "desc"}',
6261
):
6362
...
6463

@@ -83,7 +82,7 @@ async def replace_folder(
8382

8483
@router.delete(
8584
"/folders/{folder_id}",
86-
response_model=Envelope[FolderGet],
85+
status_code=status.HTTP_204_NO_CONTENT,
8786
)
8887
async def delete_folder(_path: Annotated[FoldersPathParams, Depends()]):
8988
...

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from datetime import datetime
2+
from typing import NamedTuple
23

34
from models_library.basic_types import IDStr
45
from models_library.folders import FolderID
56
from models_library.projects_access import AccessRights
67
from models_library.users import GroupID
7-
from pydantic import Extra
8+
from models_library.utils.common_validators import null_or_none_str_to_none_validator
9+
from pydantic import Extra, PositiveInt, validator
810

911
from ._base import InputSchema, OutputSchema
1012

@@ -21,6 +23,11 @@ class FolderGet(OutputSchema):
2123
access_rights: dict[GroupID, AccessRights]
2224

2325

26+
class FolderGetPage(NamedTuple):
27+
items: list[FolderGet]
28+
total: PositiveInt
29+
30+
2431
class CreateFolderBodyParams(InputSchema):
2532
name: IDStr
2633
description: str
@@ -29,6 +36,10 @@ class CreateFolderBodyParams(InputSchema):
2936
class Config:
3037
extra = Extra.forbid
3138

39+
_null_or_none_str_to_none_validator = validator(
40+
"parent_folder_id", allow_reuse=True, pre=True
41+
)(null_or_none_str_to_none_validator)
42+
3243

3344
class PutFolderBodyParams(InputSchema):
3445
name: IDStr

packages/models-library/src/models_library/utils/common_validators.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,9 @@ def ensure_unique_dict_values_validator(dict_data: dict) -> dict:
6363
msg = f"Dictionary values must be unique, provided: {dict_data}"
6464
raise ValueError(msg)
6565
return dict_data
66+
67+
68+
def null_or_none_str_to_none_validator(value: Any):
69+
if isinstance(value, str) and value.lower() in ("null", "none"):
70+
return None
71+
return value

packages/models-library/tests/test_utils_common_validators.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
create_enums_pre_validator,
66
empty_str_to_none_pre_validator,
77
none_to_empty_str_pre_validator,
8+
null_or_none_str_to_none_validator,
89
)
910
from pydantic import BaseModel, ValidationError, validator
1011

@@ -59,3 +60,30 @@ class Model(BaseModel):
5960

6061
model = Model.parse_obj({"message": ""})
6162
assert model == Model.parse_obj({"message": None})
63+
64+
65+
def test_null_or_none_str_to_none_validator():
66+
class Model(BaseModel):
67+
message: str | None
68+
69+
_null_or_none_str_to_none_validator = validator(
70+
"message", allow_reuse=True, pre=True
71+
)(null_or_none_str_to_none_validator)
72+
73+
model = Model.parse_obj({"message": "none"})
74+
assert model == Model.parse_obj({"message": None})
75+
76+
model = Model.parse_obj({"message": "null"})
77+
assert model == Model.parse_obj({"message": None})
78+
79+
model = Model.parse_obj({"message": "NoNe"})
80+
assert model == Model.parse_obj({"message": None})
81+
82+
model = Model.parse_obj({"message": "NuLl"})
83+
assert model == Model.parse_obj({"message": None})
84+
85+
model = Model.parse_obj({"message": None})
86+
assert model == Model.parse_obj({"message": None})
87+
88+
model = Model.parse_obj({"message": ""})
89+
assert model == Model.parse_obj({"message": ""})

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from datetime import datetime
66
from enum import Enum
77
from functools import reduce
8-
from typing import Any, ClassVar, Final, TypeAlias
8+
from typing import Any, ClassVar, Final, TypeAlias, cast
99

1010
import sqlalchemy as sa
1111
from aiopg.sa.connection import SAConnection
@@ -20,12 +20,14 @@
2020
parse_obj_as,
2121
)
2222
from pydantic.errors import PydanticErrorMixin
23+
from simcore_postgres_database.utils_ordering import OrderByDict
2324
from sqlalchemy.dialects import postgresql
2425
from sqlalchemy.sql.elements import ColumnElement
2526
from sqlalchemy.sql.selectable import ScalarSelect
2627

2728
from .models.folders import folders, folders_access_rights, folders_to_projects
2829
from .models.groups import GroupType, groups
30+
from .utils_ordering import OrderDirection
2931

3032
_ProductName: TypeAlias = str
3133
_ProjectID: TypeAlias = uuid.UUID
@@ -986,8 +988,11 @@ async def folder_list(
986988
*,
987989
offset: NonNegativeInt,
988990
limit: NonNegativeInt,
991+
order_by: OrderByDict = OrderByDict(
992+
field="modified", direction=OrderDirection.DESC
993+
),
989994
_required_permissions=_requires(_BasePermissions.LIST_FOLDERS), # noqa: B008
990-
) -> list[FolderEntry]:
995+
) -> tuple[int, list[FolderEntry]]:
991996
"""
992997
Raises:
993998
FolderNotFoundError
@@ -1015,7 +1020,7 @@ async def folder_list(
10151020
access_via_gid = resolved_access_rights.gid
10161021
access_via_folder_id = resolved_access_rights.folder_id
10171022

1018-
query = (
1023+
base_query = (
10191024
sa.select(
10201025
folders,
10211026
folders_access_rights,
@@ -1047,14 +1052,30 @@ async def folder_list(
10471052
if folder_id is None
10481053
else True
10491054
)
1050-
.offset(offset)
1051-
.limit(limit)
1055+
.where(folders.c.product_name == product_name)
10521056
)
10531057

1054-
async for entry in connection.execute(query):
1058+
# Select total count from base_query
1059+
subquery = base_query.subquery()
1060+
count_query = sa.select(sa.func.count()).select_from(subquery)
1061+
count_result = await connection.execute(count_query)
1062+
total_count = await count_result.scalar()
1063+
1064+
# Ordering and pagination
1065+
if order_by["direction"] == OrderDirection.ASC:
1066+
list_query = base_query.order_by(
1067+
sa.asc(getattr(folders.c, order_by["field"]))
1068+
)
1069+
else:
1070+
list_query = base_query.order_by(
1071+
sa.desc(getattr(folders.c, order_by["field"]))
1072+
)
1073+
list_query = list_query.offset(offset).limit(limit)
1074+
1075+
async for entry in connection.execute(list_query):
10551076
results.append(FolderEntry.from_orm(entry)) # noqa: PERF401s
10561077

1057-
return results
1078+
return cast(int, total_count), results
10581079

10591080

10601081
async def folder_get(
@@ -1101,6 +1122,7 @@ async def folder_get(
11011122
if folder_id is None
11021123
else True
11031124
)
1125+
.where(folders.c.product_name == product_name)
11041126
)
11051127

11061128
query_result: RowProxy | None = await (
@@ -1113,3 +1135,6 @@ async def folder_get(
11131135
)
11141136

11151137
return FolderEntry.from_orm(query_result)
1138+
1139+
1140+
__all__ = ["OrderByDict"]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from enum import Enum
2+
from typing import TypedDict
3+
4+
5+
class OrderDirection(str, Enum):
6+
ASC = "asc"
7+
DESC = "desc"
8+
9+
10+
class OrderByDict(TypedDict):
11+
field: str
12+
direction: OrderDirection
13+
14+
15+
# Example usage
16+
order_by_example: OrderByDict = {
17+
"field": "example_field",
18+
"direction": OrderDirection.ASC,
19+
}

packages/postgres-database/tests/test_utils_folders.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1633,9 +1633,10 @@ async def _list_folder_as(
16331633
limit: NonNegativeInt = ALL_IN_ONE_PAGE_LIMIT,
16341634
) -> list[FolderEntry]:
16351635

1636-
return await folder_list(
1636+
total_count, folders_db = await folder_list(
16371637
connection, default_product_name, folder_id, gid, offset=offset, limit=limit
16381638
)
1639+
return folders_db
16391640

16401641

16411642
async def test_folder_list(

services/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ services:
725725
WEBSERVER_TAGS: ${WEBSERVER_TAGS}
726726
WEBSERVER_USERS: ${WEBSERVER_USERS}
727727
WEBSERVER_VERSION_CONTROL: ${WEBSERVER_VERSION_CONTROL}
728+
WEBSERVER_FOLDERS: ${WEBSERVER_FOLDERS}
728729

729730
deploy:
730731
labels:

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,14 +1296,14 @@ paths:
12961296
summary: List Folders
12971297
operationId: list_folders
12981298
parameters:
1299-
- description: Order by field (name|description) and direction (asc|desc). The
1300-
default sorting order is ascending.
1299+
- description: Order by field (modified_at|name|description) and direction (asc|desc).
1300+
The default sorting order is ascending.
13011301
required: false
13021302
schema:
13031303
title: Order By
1304-
description: Order by field (name|description) and direction (asc|desc).
1305-
The default sorting order is ascending.
1306-
default: '{"field": "name", "direction": "desc"}'
1304+
description: Order by field (modified_at|name|description) and direction
1305+
(asc|desc). The default sorting order is ascending.
1306+
default: '{"field": "modified_at", "direction": "desc"}'
13071307
example: '{"field": "name", "direction": "desc"}'
13081308
name: order_by
13091309
in: query
@@ -1349,7 +1349,7 @@ paths:
13491349
content:
13501350
application/json:
13511351
schema:
1352-
$ref: '#/components/schemas/Envelope_WalletGet_'
1352+
$ref: '#/components/schemas/Envelope_FolderGet_'
13531353
/v0/folders/{folder_id}:
13541354
get:
13551355
tags:
@@ -1414,12 +1414,8 @@ paths:
14141414
name: folder_id
14151415
in: path
14161416
responses:
1417-
'200':
1417+
'204':
14181418
description: Successful Response
1419-
content:
1420-
application/json:
1421-
schema:
1422-
$ref: '#/components/schemas/Envelope_FolderGet_'
14231419
/v0/folders/{folder_id}/groups/{group_id}:
14241420
put:
14251421
tags:
@@ -6008,8 +6004,8 @@ components:
60086004
quality: {}
60096005
accessRights:
60106006
'1':
6011-
execute_access: true
6012-
write_access: false
6007+
execute: true
6008+
write: false
60136009
key: simcore/services/comp/itis/sleeper
60146010
version: 2.2.1
60156011
version_display: 2 Xtreme
@@ -10788,6 +10784,7 @@ components:
1078810784
title: Write
1078910785
type: boolean
1079010786
default: false
10787+
additionalProperties: false
1079110788
ServiceInputGet:
1079210789
title: ServiceInputGet
1079310790
required:

0 commit comments

Comments
 (0)