Skip to content

Commit b4f6ce1

Browse files
committed
moves schemas to models_library
1 parent a2859ef commit b4f6ce1

File tree

6 files changed

+186
-180
lines changed

6 files changed

+186
-180
lines changed

api/specs/web-server/_users.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
from typing import Annotated
88

99
from fastapi import APIRouter, Depends, status
10-
from models_library.api_schemas_webserver.users import MyProfileGet, MyProfilePatch
10+
from models_library.api_schemas_webserver.users import (
11+
MyProfileGet,
12+
MyProfilePatch,
13+
PreRegisteredUserGet,
14+
SearchQueryParams,
15+
UserGet,
16+
)
1117
from models_library.api_schemas_webserver.users_preferences import PatchRequestBody
1218
from models_library.generics import Envelope
1319
from models_library.user_preferences import PreferenceIdentifier
@@ -20,11 +26,6 @@
2026
from simcore_service_webserver.users._notifications_handlers import (
2127
_NotificationPathParams,
2228
)
23-
from simcore_service_webserver.users._schemas import (
24-
PreUserProfile,
25-
SearchQueryParams,
26-
UserProfile,
27-
)
2829
from simcore_service_webserver.users._tokens_handlers import _TokenPathParams
2930
from simcore_service_webserver.users.schemas import (
3031
PermissionGet,
@@ -66,8 +67,8 @@ async def replace_my_profile(_profile: MyProfilePatch):
6667
status_code=status.HTTP_204_NO_CONTENT,
6768
)
6869
async def set_frontend_preference(
69-
preference_id: PreferenceIdentifier, # noqa: ARG001
70-
body_item: PatchRequestBody, # noqa: ARG001
70+
preference_id: PreferenceIdentifier,
71+
body_item: PatchRequestBody,
7172
):
7273
...
7374

@@ -142,7 +143,7 @@ async def list_user_permissions():
142143

143144
@router.get(
144145
"/users:search",
145-
response_model=Envelope[list[UserProfile]],
146+
response_model=Envelope[list[UserGet]],
146147
tags=[
147148
"po",
148149
],
@@ -154,10 +155,10 @@ async def search_users(_params: Annotated[SearchQueryParams, Depends()]):
154155

155156
@router.post(
156157
"/users:pre-register",
157-
response_model=Envelope[UserProfile],
158+
response_model=Envelope[UserGet],
158159
tags=[
159160
"po",
160161
],
161162
)
162-
async def pre_register_user(_body: PreUserProfile):
163+
async def pre_register_user(_body: PreRegisteredUserGet):
163164
...

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

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
import re
2+
import sys
3+
from contextlib import suppress
24
from datetime import date
35
from enum import Enum
4-
from typing import Annotated, Literal
6+
from typing import Annotated, Any, Final, Literal
57

8+
import pycountry
9+
from models_library.api_schemas_webserver._base import InputSchema, OutputSchema
610
from models_library.api_schemas_webserver.groups import MyGroupsGet
711
from models_library.api_schemas_webserver.users_preferences import AggregatedPreferences
812
from models_library.basic_types import IDStr
913
from models_library.emails import LowerCaseEmailStr
14+
from models_library.products import ProductName
1015
from models_library.users import FirstNameStr, LastNameStr, UserID
11-
from pydantic import BaseModel, ConfigDict, Field, field_validator
16+
from pydantic import (
17+
BaseModel,
18+
ConfigDict,
19+
Field,
20+
ValidationInfo,
21+
field_validator,
22+
model_validator,
23+
)
24+
from simcore_postgres_database.models.users import UserStatus
1225

1326
from ._base import InputSchema, OutputSchema
1427

@@ -128,3 +141,132 @@ def _validate_user_name(cls, value: str):
128141
raise ValueError(msg)
129142

130143
return value
144+
145+
146+
class SearchQueryParams(BaseModel):
147+
email: str = Field(
148+
min_length=3,
149+
max_length=200,
150+
description="complete or glob pattern for an email",
151+
)
152+
153+
154+
class UserGet(OutputSchema):
155+
first_name: str | None
156+
last_name: str | None
157+
email: LowerCaseEmailStr
158+
institution: str | None
159+
phone: str | None
160+
address: str | None
161+
city: str | None
162+
state: str | None = Field(description="State, province, canton, ...")
163+
postal_code: str | None
164+
country: str | None
165+
extras: dict[str, Any] = Field(
166+
default_factory=dict,
167+
description="Keeps extra information provided in the request form",
168+
)
169+
170+
# authorization
171+
invited_by: str | None = Field(default=None)
172+
173+
# user status
174+
registered: bool
175+
status: UserStatus | None
176+
products: list[ProductName] | None = Field(
177+
default=None,
178+
description="List of products this users is included or None if fields is unset",
179+
)
180+
181+
@field_validator("status")
182+
@classmethod
183+
def _consistency_check(cls, v, info: ValidationInfo):
184+
registered = info.data["registered"]
185+
status = v
186+
if not registered and status is not None:
187+
msg = f"{registered=} and {status=} is not allowed"
188+
raise ValueError(msg)
189+
return v
190+
191+
192+
MAX_BYTES_SIZE_EXTRAS: Final[int] = 512
193+
194+
195+
class PreRegisteredUserGet(InputSchema):
196+
first_name: str
197+
last_name: str
198+
email: LowerCaseEmailStr
199+
institution: str | None = Field(
200+
default=None, description="company, university, ..."
201+
)
202+
phone: str | None
203+
# billing details
204+
address: str
205+
city: str
206+
state: str | None = Field(default=None)
207+
postal_code: str
208+
country: str
209+
extras: Annotated[
210+
dict[str, Any],
211+
Field(
212+
default_factory=dict,
213+
description="Keeps extra information provided in the request form. At most MAX_NUM_EXTRAS fields",
214+
),
215+
]
216+
217+
model_config = ConfigDict(str_strip_whitespace=True, str_max_length=200)
218+
219+
@model_validator(mode="before")
220+
@classmethod
221+
def _preprocess_aliases_and_extras(cls, values):
222+
# multiple aliases for "institution"
223+
alias_by_priority = ("companyName", "company", "university", "universityName")
224+
if "institution" not in values:
225+
226+
for alias in alias_by_priority:
227+
if alias in values:
228+
values["institution"] = values.pop(alias)
229+
230+
# collect extras
231+
extra_fields = {}
232+
field_names_and_aliases = (
233+
set(cls.model_fields.keys())
234+
| {f.alias for f in cls.model_fields.values() if f.alias}
235+
| set(alias_by_priority)
236+
)
237+
for key, value in values.items():
238+
if key not in field_names_and_aliases:
239+
extra_fields[key] = value
240+
if sys.getsizeof(extra_fields) > MAX_BYTES_SIZE_EXTRAS:
241+
extra_fields.pop(key)
242+
break
243+
244+
for key in extra_fields:
245+
values.pop(key)
246+
247+
values.setdefault("extras", {})
248+
values["extras"].update(extra_fields)
249+
250+
return values
251+
252+
@field_validator("first_name", "last_name", "institution", mode="before")
253+
@classmethod
254+
def _pre_normalize_given_names(cls, v):
255+
if v:
256+
with suppress(Exception): # skip if funny characters
257+
name = re.sub(r"\s+", " ", v)
258+
return re.sub(r"\b\w+\b", lambda m: m.group(0).capitalize(), name)
259+
return v
260+
261+
@field_validator("country", mode="before")
262+
@classmethod
263+
def _pre_check_and_normalize_country(cls, v):
264+
if v:
265+
try:
266+
return pycountry.countries.lookup(v).name
267+
except LookupError as err:
268+
raise ValueError(v) from err
269+
return v
270+
271+
272+
assert set(PreRegisteredUserGet.model_fields).issubset(UserGet.model_fields) # nosec

services/web/server/src/simcore_service_webserver/users/_schemas.py

Lines changed: 1 addition & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -2,159 +2,14 @@
22
33
"""
44

5-
import re
6-
import sys
7-
from contextlib import suppress
8-
from typing import Annotated, Any, Final
95

10-
import pycountry
11-
from models_library.api_schemas_webserver._base import InputSchema, OutputSchema
12-
from models_library.emails import LowerCaseEmailStr
13-
from models_library.products import ProductName
146
from models_library.users import UserID
15-
from pydantic import (
16-
BaseModel,
17-
ConfigDict,
18-
Field,
19-
ValidationInfo,
20-
field_validator,
21-
model_validator,
22-
)
7+
from pydantic import BaseModel, Field
238
from servicelib.request_keys import RQT_USERID_KEY
24-
from simcore_postgres_database.models.users import UserStatus
259

2610
from .._constants import RQ_PRODUCT_KEY
2711

2812

2913
class UsersRequestContext(BaseModel):
3014
user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required]
3115
product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required]
32-
33-
34-
class SearchQueryParams(BaseModel):
35-
email: str = Field(
36-
min_length=3,
37-
max_length=200,
38-
description="complete or glob pattern for an email",
39-
)
40-
41-
42-
class UserProfile(OutputSchema):
43-
first_name: str | None
44-
last_name: str | None
45-
email: LowerCaseEmailStr
46-
institution: str | None
47-
phone: str | None
48-
address: str | None
49-
city: str | None
50-
state: str | None = Field(description="State, province, canton, ...")
51-
postal_code: str | None
52-
country: str | None
53-
extras: dict[str, Any] = Field(
54-
default_factory=dict,
55-
description="Keeps extra information provided in the request form",
56-
)
57-
58-
# authorization
59-
invited_by: str | None = Field(default=None)
60-
61-
# user status
62-
registered: bool
63-
status: UserStatus | None
64-
products: list[ProductName] | None = Field(
65-
default=None,
66-
description="List of products this users is included or None if fields is unset",
67-
)
68-
69-
@field_validator("status")
70-
@classmethod
71-
def _consistency_check(cls, v, info: ValidationInfo):
72-
registered = info.data["registered"]
73-
status = v
74-
if not registered and status is not None:
75-
msg = f"{registered=} and {status=} is not allowed"
76-
raise ValueError(msg)
77-
return v
78-
79-
80-
MAX_BYTES_SIZE_EXTRAS: Final[int] = 512
81-
82-
83-
class PreUserProfile(InputSchema):
84-
first_name: str
85-
last_name: str
86-
email: LowerCaseEmailStr
87-
institution: str | None = Field(
88-
default=None, description="company, university, ..."
89-
)
90-
phone: str | None
91-
# billing details
92-
address: str
93-
city: str
94-
state: str | None = Field(default=None)
95-
postal_code: str
96-
country: str
97-
extras: Annotated[
98-
dict[str, Any],
99-
Field(
100-
default_factory=dict,
101-
description="Keeps extra information provided in the request form. At most MAX_NUM_EXTRAS fields",
102-
),
103-
]
104-
105-
model_config = ConfigDict(str_strip_whitespace=True, str_max_length=200)
106-
107-
@model_validator(mode="before")
108-
@classmethod
109-
def _preprocess_aliases_and_extras(cls, values):
110-
# multiple aliases for "institution"
111-
alias_by_priority = ("companyName", "company", "university", "universityName")
112-
if "institution" not in values:
113-
114-
for alias in alias_by_priority:
115-
if alias in values:
116-
values["institution"] = values.pop(alias)
117-
118-
# collect extras
119-
extra_fields = {}
120-
field_names_and_aliases = (
121-
set(cls.model_fields.keys())
122-
| {f.alias for f in cls.model_fields.values() if f.alias}
123-
| set(alias_by_priority)
124-
)
125-
for key, value in values.items():
126-
if key not in field_names_and_aliases:
127-
extra_fields[key] = value
128-
if sys.getsizeof(extra_fields) > MAX_BYTES_SIZE_EXTRAS:
129-
extra_fields.pop(key)
130-
break
131-
132-
for key in extra_fields:
133-
values.pop(key)
134-
135-
values.setdefault("extras", {})
136-
values["extras"].update(extra_fields)
137-
138-
return values
139-
140-
@field_validator("first_name", "last_name", "institution", mode="before")
141-
@classmethod
142-
def _pre_normalize_given_names(cls, v):
143-
if v:
144-
with suppress(Exception): # skip if funny characters
145-
name = re.sub(r"\s+", " ", v)
146-
return re.sub(r"\b\w+\b", lambda m: m.group(0).capitalize(), name)
147-
return v
148-
149-
@field_validator("country", mode="before")
150-
@classmethod
151-
def _pre_check_and_normalize_country(cls, v):
152-
if v:
153-
try:
154-
return pycountry.countries.lookup(v).name
155-
except LookupError as err:
156-
raise ValueError(v) from err
157-
return v
158-
159-
160-
assert set(PreUserProfile.model_fields).issubset(UserProfile.model_fields) # nosec

0 commit comments

Comments
 (0)