Skip to content

Commit df04839

Browse files
committed
merge master into 7197-add-zipping-endpoints-in-storage
2 parents 8acc41c + dd053ac commit df04839

File tree

16 files changed

+419
-45
lines changed

16 files changed

+419
-45
lines changed

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,13 @@ def from_domain_model(cls, item: LicensedItem) -> Self:
141141
exclude_unset=True,
142142
),
143143
"licensed_resources": [
144-
_ItisVipResourceRestData(**x)
145-
for x in sorted(
146-
item.licensed_resources,
147-
key=lambda x: datetime.strptime(
148-
x["source"]["features"]["date"], "%Y-%m-%d"
149-
),
150-
reverse=True,
151-
)
144+
_ItisVipResourceRestData(**x) for x in item.licensed_resources
152145
],
153146
"category_id": item.licensed_resources[0]["category_id"],
154147
"category_display": item.licensed_resources[0]["category_display"],
148+
"terms_of_use_url": item.licensed_resources[0].get(
149+
"terms_of_use_url", None
150+
),
155151
}
156152
)
157153

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import date, datetime
22
from enum import auto
33
from typing import Annotated, Any, NamedTuple, NewType, NotRequired, TypeAlias, cast
44
from uuid import UUID
@@ -47,7 +47,7 @@ class FeaturesDict(TypedDict):
4747
age: NotRequired[str]
4848
weight: NotRequired[str]
4949
height: NotRequired[str]
50-
date: str
50+
date: date
5151
ethnicity: NotRequired[str]
5252
functionality: NotRequired[str]
5353

@@ -102,6 +102,7 @@ class LicensedResourceDB(BaseModel):
102102
licensed_resource_name: str
103103
licensed_resource_type: LicensedResourceType
104104
licensed_resource_data: dict[str, Any] | None
105+
priority: int
105106

106107
# states
107108
created: datetime
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""licensed_resources add priority column
2+
3+
Revision ID: 5e43b5ec7604
4+
Revises: e8ffc0c96336
5+
Create Date: 2025-02-18 12:24:49.105989+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "5e43b5ec7604"
13+
down_revision = "e8ffc0c96336"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
"licensed_resources",
22+
sa.Column("priority", sa.SmallInteger(), server_default="0", nullable=False),
23+
)
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.drop_column("licensed_resources", "priority")
30+
# ### end Alembic commands ###

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
nullable=True,
4848
doc="Resource metadata. Used for read-only purposes",
4949
),
50+
sa.Column(
51+
"priority",
52+
sa.SmallInteger,
53+
nullable=False,
54+
server_default="0",
55+
doc="Used for sorting 0 (first) > 1 (second) > 2 (third) (ex. if we want to manually adjust how it is presented in the Market)",
56+
),
5057
column_created_datetime(timezone=True),
5158
column_modified_datetime(timezone=True),
5259
column_trashed_datetime("licensed_resources"),

packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ def random_itis_vip_available_download_item(
488488
f"name: {fake.name()} Right Hand," # w/o spaces
489489
f" version: V{fake.pyint()}.0, " # w/ x2 spaces
490490
f"sex: Male, age: 8 years," # w/o spaces
491-
f"date: {fake.date()}, " # w/ x1 spaces
491+
f"date: {fake.date()} , " # w/ x2 spaces prefix, x1 space suffix
492492
f"ethnicity: Caucasian, functionality: {features_functionality} "
493493
"}"
494494
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10109,6 +10109,7 @@ components:
1010910109
title: Height
1011010110
date:
1011110111
type: string
10112+
format: date
1011210113
title: Date
1011310114
ethnicity:
1011410115
type: string

services/web/server/src/simcore_service_webserver/folders/_trash_service.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@
33

44
import arrow
55
from aiohttp import web
6-
from models_library.folders import FolderID
6+
from common_library.pagination_tools import iter_pagination_params
7+
from models_library.access_rights import AccessRights
8+
from models_library.basic_types import IDStr
9+
from models_library.folders import FolderDB, FolderID
710
from models_library.products import ProductName
811
from models_library.projects import ProjectID
12+
from models_library.rest_ordering import OrderBy, OrderDirection
13+
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
914
from models_library.users import UserID
1015
from simcore_postgres_database.utils_repos import transaction_context
1116
from sqlalchemy.ext.asyncio import AsyncConnection
1217

1318
from ..db.plugin import get_asyncpg_engine
1419
from ..projects._trash_service import trash_project, untrash_project
1520
from ..workspaces.api import check_user_workspace_access
16-
from . import _folders_repository
21+
from . import _folders_repository, _folders_service
22+
from .errors import FolderNotTrashedError
1723

1824
_logger = logging.getLogger(__name__)
1925

@@ -186,3 +192,89 @@ async def untrash_folder(
186192
await untrash_project(
187193
app, product_name=product_name, user_id=user_id, project_id=project_id
188194
)
195+
196+
197+
def _can_delete(
198+
folder_db: FolderDB,
199+
my_access_rights: AccessRights,
200+
user_id: UserID,
201+
until_equal_datetime: datetime | None,
202+
) -> bool:
203+
return bool(
204+
folder_db.trashed
205+
and (until_equal_datetime is None or folder_db.trashed < until_equal_datetime)
206+
and my_access_rights.delete
207+
and folder_db.trashed_by == user_id
208+
and folder_db.trashed_explicitly
209+
)
210+
211+
212+
async def list_explicitly_trashed_folders(
213+
app: web.Application,
214+
*,
215+
product_name: ProductName,
216+
user_id: UserID,
217+
until_equal_datetime: datetime | None = None,
218+
) -> list[FolderID]:
219+
trashed_folder_ids: list[FolderID] = []
220+
221+
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
222+
(
223+
folders,
224+
page_params.total_number_of_items,
225+
) = await _folders_service.list_folders_full_depth(
226+
app,
227+
user_id=user_id,
228+
product_name=product_name,
229+
text=None,
230+
trashed=True,
231+
offset=page_params.offset,
232+
limit=page_params.limit,
233+
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
234+
)
235+
236+
# NOTE: Applying POST-FILTERING
237+
trashed_folder_ids.extend(
238+
[
239+
f.folder_db.folder_id
240+
for f in folders
241+
if _can_delete(
242+
f.folder_db,
243+
my_access_rights=f.my_access_rights,
244+
user_id=user_id,
245+
until_equal_datetime=until_equal_datetime,
246+
)
247+
]
248+
)
249+
return trashed_folder_ids
250+
251+
252+
async def delete_trashed_folder(
253+
app: web.Application,
254+
*,
255+
product_name: ProductName,
256+
user_id: UserID,
257+
folder_id: FolderID,
258+
until_equal_datetime: datetime | None = None,
259+
) -> None:
260+
261+
folder = await _folders_service.get_folder(
262+
app, user_id=user_id, folder_id=folder_id, product_name=product_name
263+
)
264+
265+
if not _can_delete(
266+
folder.folder_db,
267+
folder.my_access_rights,
268+
user_id=user_id,
269+
until_equal_datetime=until_equal_datetime,
270+
):
271+
raise FolderNotTrashedError(
272+
folder_id=folder_id,
273+
user_id=user_id,
274+
reason="Cannot delete trashed folder since it does not fit current criteria",
275+
)
276+
277+
# NOTE: this function deletes folder AND its content recursively!
278+
await _folders_service.delete_folder(
279+
app, user_id=user_id, folder_id=folder_id, product_name=product_name
280+
)

services/web/server/src/simcore_service_webserver/folders/errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ class FolderAccessForbiddenError(FoldersValueError):
1919

2020
class FolderGroupNotFoundError(FoldersValueError):
2121
msg_template = "Folder group not found. {reason}"
22+
23+
24+
class FoldersRuntimeError(WebServerBaseError, RuntimeError):
25+
...
26+
27+
28+
class FolderNotTrashedError(FoldersRuntimeError):
29+
msg_template = (
30+
"Cannot delete folder {folder_id} since it was not trashed first: {reason}"
31+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from ._trash_service import delete_trashed_folder, list_explicitly_trashed_folders
2+
3+
__all__: tuple[str, ...] = (
4+
"delete_trashed_folder",
5+
"list_explicitly_trashed_folders",
6+
)
7+
8+
# nopycln: file

services/web/server/src/simcore_service_webserver/licenses/_itis_vip_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@
1919
)
2020

2121

22+
def _clean_dict_data(data_dict):
23+
"""
24+
Strips leading/trailing whitespace from all string values
25+
and removes keys whose stripped value is empty.
26+
"""
27+
cleaned = {}
28+
for k, v in data_dict.items():
29+
if v is not None:
30+
if isinstance(v, str):
31+
v_stripped = v.strip()
32+
# Keep the key only if it's not empty after strip
33+
if v_stripped:
34+
cleaned[k] = v_stripped
35+
else:
36+
# If it's not a string, just copy the value as is
37+
cleaned[k] = v
38+
return cleaned
39+
40+
2241
def _feature_descriptor_to_dict(descriptor: str) -> dict[str, Any]:
2342
# NOTE: this is manually added in the server side so be more robust to errors
2443
descriptor = _max_str_adapter.validate_python(descriptor.strip("{}"))
@@ -34,6 +53,7 @@ class ItisVipData(BaseModel):
3453
thumbnail: Annotated[str, Field(alias="Thumbnail")]
3554
features: Annotated[
3655
FeaturesDict,
56+
BeforeValidator(_clean_dict_data),
3757
BeforeValidator(_feature_descriptor_to_dict),
3858
Field(alias="Features"),
3959
]

0 commit comments

Comments
 (0)