Skip to content

Commit f2c2e62

Browse files
Merge branch 'master' into 8292-create-custom-GenerateJsonSchema-for-resolving-references
2 parents da4863b + 95dde06 commit f2c2e62

File tree

56 files changed

+2839
-1583
lines changed

Some content is hidden

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

56 files changed

+2839
-1583
lines changed

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

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,48 @@ class GroupAccessRights(BaseModel):
5454
)
5555

5656

57-
class GroupGet(OutputSchema):
58-
gid: GroupID = Field(..., description="the group ID")
59-
label: str = Field(..., description="the group name")
60-
description: str = Field(..., description="the group description")
61-
thumbnail: AnyUrl | None = Field(
62-
default=None, description="url to the group thumbnail"
63-
)
64-
access_rights: GroupAccessRights = Field(..., alias="accessRights")
57+
class GroupGetBase(OutputSchema):
58+
gid: Annotated[GroupID, Field(description="the group's unique ID")]
59+
label: Annotated[str, Field(description="the group's display name")]
60+
description: str
61+
thumbnail: Annotated[
62+
AnyUrl | None, Field(description="a link to the group's thumbnail")
63+
] = None
64+
65+
@field_validator("thumbnail", mode="before")
66+
@classmethod
67+
def _sanitize_thumbnail_input(cls, v):
68+
if v:
69+
# Enforces null if thumbnail is not valid URL or empty
70+
with suppress(ValidationError):
71+
return TypeAdapter(AnyHttpUrl).validate_python(v)
72+
return None
73+
74+
@classmethod
75+
def dump_basic_group_data(cls, group: Group) -> dict:
76+
"""Helper function to extract common group data for schema conversion"""
77+
return remap_keys(
78+
group.model_dump(
79+
include={
80+
"gid",
81+
"name",
82+
"description",
83+
"thumbnail",
84+
},
85+
exclude={
86+
"inclusion_rules", # deprecated
87+
},
88+
exclude_unset=True,
89+
by_alias=False,
90+
),
91+
rename={
92+
"name": "label",
93+
},
94+
)
95+
96+
97+
class GroupGet(GroupGetBase):
98+
access_rights: Annotated[GroupAccessRights, Field(alias="accessRights")]
6599

66100
inclusion_rules: Annotated[
67101
dict[str, str],
@@ -77,24 +111,7 @@ def from_domain_model(cls, group: Group, access_rights: AccessRightsDict) -> Sel
77111
# Adapts these domain models into this schema
78112
return cls.model_validate(
79113
{
80-
**remap_keys(
81-
group.model_dump(
82-
include={
83-
"gid",
84-
"name",
85-
"description",
86-
"thumbnail",
87-
},
88-
exclude={
89-
"inclusion_rules", # deprecated
90-
},
91-
exclude_unset=True,
92-
by_alias=False,
93-
),
94-
rename={
95-
"name": "label",
96-
},
97-
),
114+
**cls.dump_basic_group_data(group),
98115
"access_rights": access_rights,
99116
}
100117
)
@@ -136,15 +153,6 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
136153

137154
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
138155

139-
@field_validator("thumbnail", mode="before")
140-
@classmethod
141-
def _sanitize_legacy_data(cls, v):
142-
if v:
143-
# Enforces null if thumbnail is not valid URL or empty
144-
with suppress(ValidationError):
145-
return TypeAdapter(AnyHttpUrl).validate_python(v)
146-
return None
147-
148156

149157
class GroupCreate(InputSchema):
150158
label: str
@@ -187,6 +195,12 @@ class MyGroupsGet(OutputSchema):
187195
organizations: list[GroupGet] | None = None
188196
all: GroupGet
189197
product: GroupGet | None = None
198+
support: Annotated[
199+
GroupGetBase | None,
200+
Field(
201+
description="Group ID of the app support team or None if no support is defined for this product"
202+
),
203+
] = None
190204

191205
model_config = ConfigDict(
192206
json_schema_extra={
@@ -225,6 +239,12 @@ class MyGroupsGet(OutputSchema):
225239
"description": "Open to all users",
226240
"accessRights": {"read": True, "write": False, "delete": False},
227241
},
242+
"support": {
243+
"gid": "2",
244+
"label": "Support Team",
245+
"description": "The support team of the application",
246+
"thumbnail": "https://placekitten.com/15/15",
247+
},
228248
}
229249
}
230250
)
@@ -234,6 +254,7 @@ def from_domain_model(
234254
cls,
235255
groups_by_type: GroupsByTypeTuple,
236256
my_product_group: tuple[Group, AccessRightsDict] | None,
257+
product_support_group: Group | None,
237258
) -> Self:
238259
assert groups_by_type.primary # nosec
239260
assert groups_by_type.everyone # nosec
@@ -249,6 +270,13 @@ def from_domain_model(
249270
if my_product_group
250271
else None
251272
),
273+
support=(
274+
GroupGetBase.model_validate(
275+
GroupGetBase.dump_basic_group_data(product_support_group)
276+
)
277+
if product_support_group
278+
else None
279+
),
252280
)
253281

254282

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ class MyProfilePrivacyPatch(InputSchema):
6262
hide_email: bool | None = None
6363

6464

65+
class MyProfileAddressGet(OutputSchema):
66+
"""Details provided upon registration and used e.g. for invoicing"""
67+
68+
institution: str | None
69+
address: str | None
70+
city: str | None
71+
state: Annotated[str | None, Field(description="State, province, canton, ...")]
72+
postal_code: str | None
73+
country: str | None
74+
75+
6576
class MyProfileRestGet(OutputSchemaWithoutCamelCase):
6677
id: UserID
6778
user_name: Annotated[
@@ -86,6 +97,7 @@ class MyProfileRestGet(OutputSchemaWithoutCamelCase):
8697

8798
privacy: MyProfilePrivacyGet
8899
preferences: AggregatedPreferences
100+
contact: MyProfileAddressGet | None = None
89101

90102
@staticmethod
91103
def _update_json_schema_extra(schema: JsonDict) -> None:
@@ -105,6 +117,25 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
105117
"hide_email": 1,
106118
},
107119
},
120+
{
121+
"id": 1,
122+
"login": "[email protected]",
123+
"userName": "minuser",
124+
"role": "USER",
125+
"preferences": {},
126+
"privacy": {
127+
"hide_username": False,
128+
"hide_fullname": False,
129+
"hide_email": False,
130+
},
131+
"provided": {
132+
"address": "123 Main St",
133+
"city": "Sampleville",
134+
"state": "CA",
135+
"postal_code": "12345",
136+
"country": "Wonderland",
137+
},
138+
},
108139
]
109140
}
110141
)
@@ -132,8 +163,10 @@ def from_domain_model(
132163
my_groups_by_type: GroupsByTypeTuple,
133164
my_product_group: tuple[Group, AccessRightsDict] | None,
134165
my_preferences: AggregatedPreferences,
166+
my_support_group: Group | None,
167+
profile_contact: MyProfileAddressGet | None = None,
135168
) -> Self:
136-
data = remap_keys(
169+
profile_data = remap_keys(
137170
my_profile.model_dump(
138171
include={
139172
"id",
@@ -151,9 +184,12 @@ def from_domain_model(
151184
rename={"email": "login"},
152185
)
153186
return cls(
154-
**data,
155-
groups=MyGroupsGet.from_domain_model(my_groups_by_type, my_product_group),
187+
**profile_data,
188+
groups=MyGroupsGet.from_domain_model(
189+
my_groups_by_type, my_product_group, my_support_group
190+
),
156191
preferences=my_preferences,
192+
contact=profile_contact,
157193
)
158194

159195

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

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -39,38 +39,51 @@ class Group(BaseModel):
3939

4040
@staticmethod
4141
def _update_json_schema_extra(schema: JsonDict) -> None:
42+
everyone: JsonDict = {
43+
"gid": 1,
44+
"name": "Everyone",
45+
"type": "everyone",
46+
"description": "all users",
47+
"thumbnail": None,
48+
}
49+
user: JsonDict = {
50+
"gid": 2,
51+
"name": "User",
52+
"description": "primary group",
53+
"type": "primary",
54+
"thumbnail": None,
55+
}
56+
organization: JsonDict = {
57+
"gid": 3,
58+
"name": "Organization",
59+
"description": "standard group",
60+
"type": "standard",
61+
"thumbnail": None,
62+
"inclusionRules": {},
63+
}
64+
product: JsonDict = {
65+
"gid": 4,
66+
"name": "Product",
67+
"description": "standard group for products",
68+
"type": "standard",
69+
"thumbnail": None,
70+
}
71+
support: JsonDict = {
72+
"gid": 5,
73+
"name": "Support",
74+
"description": "support group",
75+
"type": "standard",
76+
"thumbnail": None,
77+
}
78+
4279
schema.update(
4380
{
4481
"examples": [
45-
{
46-
"gid": 1,
47-
"name": "Everyone",
48-
"type": "everyone",
49-
"description": "all users",
50-
"thumbnail": None,
51-
},
52-
{
53-
"gid": 2,
54-
"name": "User",
55-
"description": "primary group",
56-
"type": "primary",
57-
"thumbnail": None,
58-
},
59-
{
60-
"gid": 3,
61-
"name": "Organization",
62-
"description": "standard group",
63-
"type": "standard",
64-
"thumbnail": None,
65-
"inclusionRules": {},
66-
},
67-
{
68-
"gid": 4,
69-
"name": "Product",
70-
"description": "standard group for products",
71-
"type": "standard",
72-
"thumbnail": None,
73-
},
82+
everyone,
83+
user,
84+
organization,
85+
product,
86+
support,
7487
]
7588
}
7689
)

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ class UserBillingDetails(BaseModel):
7171
institution: str | None
7272
address: str | None
7373
city: str | None
74-
state: str | None = Field(description="State, province, canton, ...")
75-
country: str # Required for taxes
74+
state: Annotated[str | None, Field(description="State, province, canton, ...")]
75+
country: Annotated[
76+
str,
77+
Field(description="Billing country (with standardize name) required for taxes"),
78+
]
7679
postal_code: str | None
7780
phone: str | None
7881

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from models_library.api_schemas_webserver.users import (
23
MyProfileRestGet,
34
)
@@ -7,7 +8,11 @@
78
from pydantic import TypeAdapter
89

910

10-
def test_adapter_from_model_to_schema():
11+
@pytest.mark.parametrize("with_support_group", [True, False])
12+
@pytest.mark.parametrize("with_standard_groups", [True, False])
13+
def test_adapter_from_model_to_schema(
14+
with_support_group: bool, with_standard_groups: bool
15+
):
1116
my_profile = MyProfile.model_validate(MyProfile.model_json_schema()["example"])
1217

1318
groups = TypeAdapter(list[Group]).validate_python(
@@ -17,13 +22,22 @@ def test_adapter_from_model_to_schema():
1722
ar = AccessRightsDict(read=False, write=False, delete=False)
1823

1924
my_groups_by_type = GroupsByTypeTuple(
20-
primary=(groups[1], ar), standard=[(groups[2], ar)], everyone=(groups[0], ar)
25+
primary=(groups[1], ar),
26+
standard=[(groups[2], ar)] if with_standard_groups else [],
27+
everyone=(groups[0], ar),
2128
)
22-
my_product_group = groups[-1], AccessRightsDict(
29+
my_product_group = groups[3], AccessRightsDict(
2330
read=False, write=False, delete=False
2431
)
32+
33+
my_support_group = groups[4]
34+
2535
my_preferences = {"foo": Preference(default_value=3, value=1)}
2636

2737
MyProfileRestGet.from_domain_model(
28-
my_profile, my_groups_by_type, my_product_group, my_preferences
38+
my_profile,
39+
my_groups_by_type,
40+
my_product_group,
41+
my_preferences,
42+
my_support_group if with_support_group else None,
2943
)

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,17 @@ async def is_email_used(
318318
return bool(pre_registered)
319319

320320
async def get_billing_details(
321-
self, connection: AsyncConnection | None = None, *, user_id: int
321+
self,
322+
connection: AsyncConnection | None = None,
323+
*,
324+
product_name: str,
325+
user_id: int,
322326
) -> Any | None:
327+
"""Returns billing details for the specified user and product.
328+
329+
- If the user is registered without a product, returns details for that registration.
330+
- Returns None if no billing details are found.
331+
"""
323332
async with pass_or_acquire_connection(self._engine, connection) as conn:
324333
result = await conn.execute(
325334
sa.select(
@@ -339,7 +348,13 @@ async def get_billing_details(
339348
users.c.id == users_pre_registration_details.c.user_id,
340349
)
341350
)
342-
.where(users.c.id == user_id)
351+
.where(
352+
(users.c.id == user_id)
353+
& (
354+
(users_pre_registration_details.c.product_name == product_name)
355+
| (users_pre_registration_details.c.product_name.is_(None))
356+
)
357+
)
343358
.order_by(users_pre_registration_details.c.created.desc())
344359
.limit(1)
345360
# NOTE: might want to copy billing details to users table??

0 commit comments

Comments
 (0)