Skip to content

Commit 2f2b23c

Browse files
Merge remote-tracking branch 'upstream/master' into add-celery-routing-queues
2 parents 3b69bc3 + 11a7e7b commit 2f2b23c

File tree

149 files changed

+3132
-1492
lines changed

Some content is hidden

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

149 files changed

+3132
-1492
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/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/src/common_library/json_serialization.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
""" Helpers for json serialization
2-
- built-in json-like API
3-
- implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance
1+
"""Helpers for json serialization
2+
- built-in json-like API
3+
- implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance
44
"""
55

66
import datetime
@@ -118,6 +118,28 @@ def pydantic_encoder(obj: Any) -> Any:
118118
raise TypeError(msg)
119119

120120

121+
def representation_encoder(obj: Any):
122+
"""
123+
A fallback encoder that uses `pydantic_encoder` to serialize objects.
124+
If serialization fails, it falls back to using `str(obj)`.
125+
126+
This is practical for representation purposes, such as logging or debugging.
127+
128+
Example:
129+
>>> from common_library.json_serialization import json_dumps, representation_encoder
130+
>>> class CustomObject:
131+
... def __str__(self):
132+
... return "CustomObjectRepresentation"
133+
>>> obj = CustomObject()
134+
>>> json_dumps(obj, default=representation_encoder)
135+
'"CustomObjectRepresentation"'
136+
"""
137+
try:
138+
return pydantic_encoder(obj)
139+
except TypeError:
140+
return str(obj)
141+
142+
121143
def json_dumps(
122144
obj: Any,
123145
*,

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/common-library/tests/test_json_serialization.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
SeparatorTuple,
1414
json_dumps,
1515
json_loads,
16+
representation_encoder,
1617
)
1718
from faker import Faker
1819
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, HttpUrl, TypeAdapter
@@ -110,3 +111,25 @@ class M(BaseModel):
110111
http_url=faker.url(),
111112
)
112113
json_dumps(obj)
114+
115+
116+
def test_json_dumps_with_representation_encoder():
117+
class CustomObject:
118+
def __str__(self):
119+
return "CustomObjectRepresentation"
120+
121+
class SomeModel(BaseModel):
122+
x: int
123+
124+
obj = {
125+
"custom": CustomObject(),
126+
"some": SomeModel(x=42),
127+
}
128+
129+
# Using representation_encoder as the default encoder
130+
result = json_dumps(obj, default=representation_encoder, indent=1)
131+
132+
assert (
133+
result
134+
== '{\n "custom": "CustomObjectRepresentation",\n "some": {\n "x": 42\n }\n}'
135+
)

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/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

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

Lines changed: 0 additions & 44 deletions
This file was deleted.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
_logger = logging.getLogger(__name__)
88

99

10-
def create_render_env_from_package(**kwargs):
10+
def create_render_environment_from_notifications_library(**kwargs) -> Environment:
1111
return Environment(
1212
loader=PackageLoader(notifications_library.__name__, "templates"),
1313
autoescape=select_autoescape(["html", "xml"]),
1414
**kwargs
1515
)
1616

1717

18-
def create_render_env_from_folder(top_dir: Path):
18+
def create_render_environment_from_folder(top_dir: Path) -> Environment:
1919
assert top_dir.exists() # nosec
2020
assert top_dir.is_dir() # nosec
2121
return Environment(

0 commit comments

Comments
 (0)