Skip to content

Commit 1ed4955

Browse files
authored
Merge branch 'master' into feature/on-share-project-email
2 parents ee53285 + ef99643 commit 1ed4955

File tree

177 files changed

+4036
-1722
lines changed

Some content is hidden

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

177 files changed

+4036
-1722
lines changed

api/specs/web-server/_projects_groups.py renamed to api/specs/web-server/_projects_access_rights.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
from typing import Annotated
88

99
from fastapi import APIRouter, Depends, status
10+
from models_library.api_schemas_webserver.projects_access_rights import (
11+
ProjectsGroupsBodyParams,
12+
ProjectsGroupsPathParams,
13+
ProjectShare,
14+
ProjectShareAccepted,
15+
)
1016
from models_library.generics import Envelope
1117
from simcore_service_webserver._meta import API_VTAG
1218
from simcore_service_webserver.projects._controller._rest_schemas import (
1319
ProjectPathParams,
1420
)
15-
from simcore_service_webserver.projects._controller.groups_rest import (
16-
_ProjectsGroupsBodyParams,
17-
_ProjectsGroupsPathParams,
18-
)
1921
from simcore_service_webserver.projects._groups_service import ProjectGroupGet
2022

2123
router = APIRouter(
@@ -24,14 +26,30 @@
2426
)
2527

2628

29+
@router.post(
30+
"/projects/{project_id}:share",
31+
response_model=Envelope[ProjectShareAccepted],
32+
status_code=status.HTTP_202_ACCEPTED,
33+
responses={
34+
status.HTTP_202_ACCEPTED: {
35+
"description": "The request to share the project has been accepted, but the actual sharing process has to be confirmd."
36+
}
37+
},
38+
)
39+
async def share_project(
40+
_path: Annotated[ProjectPathParams, Depends()],
41+
_body: ProjectShare,
42+
): ...
43+
44+
2745
@router.post(
2846
"/projects/{project_id}/groups/{group_id}",
2947
response_model=Envelope[ProjectGroupGet],
3048
status_code=status.HTTP_201_CREATED,
3149
)
3250
async def create_project_group(
33-
_path: Annotated[_ProjectsGroupsPathParams, Depends()],
34-
_body: _ProjectsGroupsBodyParams,
51+
_path: Annotated[ProjectsGroupsPathParams, Depends()],
52+
_body: ProjectsGroupsBodyParams,
3553
): ...
3654

3755

@@ -47,8 +65,8 @@ async def list_project_groups(_path: Annotated[ProjectPathParams, Depends()]): .
4765
response_model=Envelope[ProjectGroupGet],
4866
)
4967
async def replace_project_group(
50-
_path: Annotated[_ProjectsGroupsPathParams, Depends()],
51-
_body: _ProjectsGroupsBodyParams,
68+
_path: Annotated[ProjectsGroupsPathParams, Depends()],
69+
_body: ProjectsGroupsBodyParams,
5270
): ...
5371

5472

@@ -57,5 +75,5 @@ async def replace_project_group(
5775
status_code=status.HTTP_204_NO_CONTENT,
5876
)
5977
async def delete_project_group(
60-
_path: Annotated[_ProjectsGroupsPathParams, Depends()],
78+
_path: Annotated[ProjectsGroupsPathParams, Depends()],
6179
): ...

api/specs/web-server/_storage.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
PresignedLink,
2323
)
2424
from models_library.api_schemas_webserver.storage import (
25+
BatchDeletePathsBodyParams,
2526
DataExportPost,
2627
ListPathsQueryParams,
2728
StorageLocationPathParams,
@@ -80,6 +81,19 @@ async def compute_path_size(_path: Annotated[StoragePathComputeSizeParams, Depen
8081
"""Compute the size of a path"""
8182

8283

84+
@router.post(
85+
"/storage/locations/{location_id}/-/paths:batchDelete",
86+
response_model=Envelope[TaskGet],
87+
status_code=status.HTTP_202_ACCEPTED,
88+
description="Deletes Paths",
89+
)
90+
async def batch_delete_paths(
91+
_path: Annotated[StorageLocationPathParams, Depends()],
92+
_body: Annotated[BatchDeletePathsBodyParams, Depends()],
93+
):
94+
"""deletes files/folders if user has the rights to"""
95+
96+
8397
@router.get(
8498
"/storage/locations/{location_id}/datasets",
8599
response_model=Envelope[list[DatasetMetaData]],

api/specs/web-server/openapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"_nih_sparc",
4343
"_nih_sparc_redirections",
4444
"_projects",
45+
"_projects_access_rights",
4546
"_projects_comments",
4647
"_projects_folders",
47-
"_projects_groups",
4848
"_projects_metadata",
4949
"_projects_nodes",
5050
"_projects_nodes_pricing_unit", # after _projects_nodes

packages/common-library/tests/test_errors_classes.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,15 @@
1313

1414

1515
def test_get_full_class_name():
16-
class A(OsparcErrorMixin):
17-
...
16+
class A(OsparcErrorMixin): ...
1817

19-
class B1(A):
20-
...
18+
class B1(A): ...
2119

22-
class B2(A):
23-
...
20+
class B2(A): ...
2421

25-
class C(B2):
26-
...
22+
class C(B2): ...
2723

28-
class B12(B1, ValueError):
29-
...
24+
class B12(B1, ValueError): ...
3025

3126
assert B1._get_full_class_name() == "A.B1"
3227
assert C._get_full_class_name() == "A.B2.C"

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ class AccessRights(BaseModel):
1010

1111
model_config = ConfigDict(extra="forbid")
1212

13+
def verify_access_integrity(self):
14+
"""Helper function that checks extra constraints in access-rights flags"""
15+
if self.write and not self.read:
16+
msg = "Write access requires read access"
17+
raise ValueError(msg)
18+
if self.delete and not self.write:
19+
msg = "Delete access requires read access"
20+
raise ValueError(msg)
21+
return self
22+
1323

1424
class ExecutableAccessRights(BaseModel):
1525
write: Annotated[bool, Field(description="can change executable settings")]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Annotated, Self
2+
3+
from models_library.groups import GroupID
4+
from models_library.projects import ProjectID
5+
from pydantic import (
6+
BaseModel,
7+
ConfigDict,
8+
EmailStr,
9+
Field,
10+
HttpUrl,
11+
StringConstraints,
12+
model_validator,
13+
)
14+
15+
from ..access_rights import AccessRights
16+
from ._base import InputSchema, OutputSchema
17+
18+
19+
class ProjectsGroupsPathParams(BaseModel):
20+
project_id: ProjectID
21+
group_id: GroupID
22+
23+
model_config = ConfigDict(extra="forbid")
24+
25+
26+
class ProjectsGroupsBodyParams(InputSchema):
27+
read: bool
28+
write: bool
29+
delete: bool
30+
31+
32+
class ProjectShare(InputSchema):
33+
sharee_email: EmailStr
34+
sharer_message: Annotated[
35+
str,
36+
StringConstraints(max_length=500, strip_whitespace=True),
37+
Field(description="An optional message from sharer to sharee"),
38+
] = ""
39+
40+
# Sharing access rights
41+
read: bool
42+
write: bool
43+
delete: bool
44+
45+
@model_validator(mode="after")
46+
def _validate_access_rights(self) -> Self:
47+
AccessRights.model_construct(
48+
read=self.read, write=self.write, delete=self.delete
49+
).verify_access_integrity()
50+
return self
51+
52+
53+
class ProjectShareAccepted(OutputSchema):
54+
sharee_email: EmailStr
55+
confirmation_link: HttpUrl

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ class ListPathsQueryParams(InputSchema, CursorQueryParameters):
3636
] = DEFAULT_NUMBER_OF_PATHS_PER_PAGE
3737

3838

39+
class BatchDeletePathsBodyParams(InputSchema):
40+
paths: set[Path]
41+
42+
3943
class DataExportPost(InputSchema):
4044
paths: list[StorageFileID]
4145

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ def from_key(cls, key: str) -> "DockerLabelKey":
6363
_UNDEFINED_LABEL_VALUE_INT: Final[str] = "0"
6464

6565

66-
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY: Final[
67-
DockerLabelKey
68-
] = TypeAdapter(DockerLabelKey).validate_python("ec2-instance-type")
66+
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY: Final[DockerLabelKey] = (
67+
TypeAdapter(DockerLabelKey).validate_python("ec2-instance-type")
68+
)
6969

7070

7171
def to_simcore_runtime_docker_label_key(key: str) -> DockerLabelKey:
@@ -122,18 +122,24 @@ def _backwards_compatibility(cls, values: dict[str, Any]) -> dict[str, Any]:
122122

123123
mapped_values.setdefault(
124124
f"{_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX}memory-limit",
125-
_UNDEFINED_LABEL_VALUE_INT,
125+
values.get("memory_limit", _UNDEFINED_LABEL_VALUE_INT),
126126
)
127127

128128
def _convert_nano_cpus_to_cpus(nano_cpu: str) -> str:
129129
with contextlib.suppress(ValidationError):
130-
return f"{TypeAdapter(float).validate_python(nano_cpu) / (1.0*10**9):.2f}"
130+
return f"{TypeAdapter(float).validate_python(nano_cpu) / (1.0 * 10**9):.2f}"
131131
return _UNDEFINED_LABEL_VALUE_INT
132132

133133
mapped_values.setdefault(
134134
f"{_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX}cpu-limit",
135-
_convert_nano_cpus_to_cpus(
136-
values.get("nano_cpus_limit", _UNDEFINED_LABEL_VALUE_INT)
135+
values.get(
136+
"cpu_limit",
137+
_convert_nano_cpus_to_cpus(
138+
values.get(
139+
"nano_cpus_limit",
140+
_UNDEFINED_LABEL_VALUE_INT,
141+
)
142+
),
137143
),
138144
)
139145
return mapped_values

packages/models-library/tests/test_docker.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
from typing import Any
7+
from uuid import UUID
78

89
import pytest
910
from faker import Faker
@@ -13,7 +14,7 @@
1314
DockerLabelKey,
1415
StandardSimcoreDockerLabels,
1516
)
16-
from pydantic import TypeAdapter, ValidationError
17+
from pydantic import ByteSize, TypeAdapter, ValidationError
1718

1819
_faker = Faker()
1920

@@ -83,11 +84,11 @@ def test_docker_label_key(label_key: str, valid: bool):
8384
True,
8485
),
8586
(
86-
f"registry:5000/si.m--c_ore/services/1234/jupyter-smash:{'A'*128}",
87+
f"registry:5000/si.m--c_ore/services/1234/jupyter-smash:{'A' * 128}",
8788
True,
8889
),
8990
(
90-
f"registry:5000/si.m--c_ore/services/1234/jupyter-smash:{'A'*129}",
91+
f"registry:5000/si.m--c_ore/services/1234/jupyter-smash:{'A' * 129}",
9192
False,
9293
),
9394
),
@@ -122,3 +123,17 @@ def test_simcore_service_docker_label_keys(obj_data: dict[str, Any]):
122123
).validate_python(exported_dict)
123124
assert re_imported_docker_label_keys
124125
assert simcore_service_docker_label_keys == re_imported_docker_label_keys
126+
127+
128+
def test_simcore_service_docker_label_keys_construction():
129+
simcore_service_docker_label_keys = StandardSimcoreDockerLabels(
130+
user_id=8268,
131+
project_id=UUID("5ea24ce0-0e4d-4ee6-a3f1-e4799752a684"),
132+
node_id=UUID("c17c6279-23c6-412f-8826-867323a7711a"),
133+
product_name="osparc",
134+
simcore_user_agent="oePqmjQbZndJghceKRJR",
135+
swarm_stack_name="UNDEFINED_DOCKER_LABEL", # NOTE: there is currently no need for this label in the comp backend
136+
memory_limit=ByteSize(23424324),
137+
cpu_limit=1.0,
138+
)
139+
assert simcore_service_docker_label_keys.cpu_limit == 1.0

packages/notifications-library/src/notifications_library/_models.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22

33
from models_library.products import ProductName
44

5-
65
#
76
# *Data are models used for rendering
87
#
8+
9+
10+
@dataclass(frozen=True)
11+
class JinjaTemplateDbGet:
12+
product_name: ProductName
13+
name: str
14+
content: str
15+
16+
917
@dataclass(frozen=True)
1018
class UserData:
1119
first_name: str

0 commit comments

Comments
 (0)