Skip to content

Commit 446bd66

Browse files
committed
Merge branch 'master' into 114-expose-fileids-dirv2-and-logs-in-apiserver
2 parents 7392a35 + 26bc00b commit 446bd66

File tree

49 files changed

+1399
-261
lines changed

Some content is hidden

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

49 files changed

+1399
-261
lines changed

.env-devel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ WEBSERVER_PROJECTS={}
395395
WEBSERVER_PROMETHEUS_API_VERSION=v1
396396
WEBSERVER_PROMETHEUS_URL=http://prometheus:9090
397397
WEBSERVER_PUBLICATIONS=1
398+
WEBSERVER_REALTIME_COLLABORATION='{"RTC_MAX_NUMBER_OF_USERS":3}'
398399
WEBSERVER_SCICRUNCH={}
399400
WEBSERVER_SESSION_SECRET_KEY='REPLACE_ME_with_result__Fernet_generate_key='
400401
WEBSERVER_SOCKETIO=1

api/specs/web-server/_users.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from models_library.api_schemas_webserver.users import (
1111
MyFunctionPermissionsGet,
1212
MyPermissionGet,
13-
MyProfileGet,
14-
MyProfilePatch,
13+
MyPhoneConfirm,
14+
MyPhoneRegister,
15+
MyProfileRestGet,
16+
MyProfileRestPatch,
1517
MyTokenCreate,
1618
MyTokenGet,
1719
TokenPathParams,
@@ -36,7 +38,7 @@
3638

3739
@router.get(
3840
"/me",
39-
response_model=Envelope[MyProfileGet],
41+
response_model=Envelope[MyProfileRestGet],
4042
)
4143
async def get_my_profile(): ...
4244

@@ -45,7 +47,58 @@ async def get_my_profile(): ...
4547
"/me",
4648
status_code=status.HTTP_204_NO_CONTENT,
4749
)
48-
async def update_my_profile(_body: MyProfilePatch): ...
50+
async def update_my_profile(_body: MyProfileRestPatch): ...
51+
52+
53+
@router.post(
54+
"/me/phone:register",
55+
description="Starts the phone registration process",
56+
status_code=status.HTTP_202_ACCEPTED,
57+
responses={
58+
status.HTTP_202_ACCEPTED: {"description": "Phone registration initiated"},
59+
status.HTTP_401_UNAUTHORIZED: {"description": "Authentication required"},
60+
status.HTTP_403_FORBIDDEN: {"description": "Insufficient permissions"},
61+
status.HTTP_422_UNPROCESSABLE_ENTITY: {
62+
"description": "Invalid phone number format"
63+
},
64+
},
65+
)
66+
async def my_phone_register(_body: MyPhoneRegister): ...
67+
68+
69+
@router.post(
70+
"/me/phone:resend",
71+
description="Resends the phone registration code",
72+
status_code=status.HTTP_202_ACCEPTED,
73+
responses={
74+
status.HTTP_202_ACCEPTED: {"description": "Phone code resent"},
75+
status.HTTP_400_BAD_REQUEST: {
76+
"description": "No pending phone registration found"
77+
},
78+
status.HTTP_401_UNAUTHORIZED: {"description": "Authentication required"},
79+
status.HTTP_403_FORBIDDEN: {"description": "Insufficient permissions"},
80+
},
81+
)
82+
async def my_phone_resend(): ...
83+
84+
85+
@router.post(
86+
"/me/phone:confirm",
87+
description="Confirms the phone registration",
88+
status_code=status.HTTP_204_NO_CONTENT,
89+
responses={
90+
status.HTTP_204_NO_CONTENT: {"description": "Phone registration confirmed"},
91+
status.HTTP_400_BAD_REQUEST: {
92+
"description": "No pending registration or invalid code"
93+
},
94+
status.HTTP_401_UNAUTHORIZED: {"description": "Authentication required"},
95+
status.HTTP_403_FORBIDDEN: {"description": "Insufficient permissions"},
96+
status.HTTP_422_UNPROCESSABLE_ENTITY: {
97+
"description": "Invalid confirmation code format"
98+
},
99+
},
100+
)
101+
async def my_phone_confirm(_body: MyPhoneConfirm): ...
49102

50103

51104
@router.patch(

packages/models-library/requirements/_base.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
arrow
88
jsonschema
99
orjson
10-
pydantic[email]
11-
pydantic-settings
1210
pydantic-extra-types
11+
pydantic-settings
12+
pydantic[email]

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,15 @@ class MyProfilePrivacyPatch(InputSchema):
6262
hide_email: bool | None = None
6363

6464

65-
class MyProfileGet(OutputSchemaWithoutCamelCase):
65+
class MyProfileRestGet(OutputSchemaWithoutCamelCase):
6666
id: UserID
6767
user_name: Annotated[
6868
IDStr, Field(description="Unique username identifier", alias="userName")
6969
]
7070
first_name: FirstNameStr | None = None
7171
last_name: LastNameStr | None = None
7272
login: LowerCaseEmailStr
73+
phone: str | None = None
7374

7475
role: Literal["ANONYMOUS", "GUEST", "USER", "TESTER", "PRODUCT_OWNER", "ADMIN"]
7576
groups: MyGroupsGet | None = None
@@ -141,6 +142,7 @@ def from_domain_model(
141142
"last_name",
142143
"email",
143144
"role",
145+
"phone",
144146
"privacy",
145147
"expiration_date",
146148
},
@@ -155,21 +157,19 @@ def from_domain_model(
155157
)
156158

157159

158-
class MyProfilePatch(InputSchemaWithoutCamelCase):
160+
class MyProfileRestPatch(InputSchemaWithoutCamelCase):
159161
first_name: FirstNameStr | None = None
160162
last_name: LastNameStr | None = None
161163
user_name: Annotated[IDStr | None, Field(alias="userName", min_length=4)] = None
164+
# NOTE: phone is updated via a dedicated endpoint!
162165

163166
privacy: MyProfilePrivacyPatch | None = None
164167

165-
model_config = ConfigDict(
166-
json_schema_extra={
167-
"example": {
168-
"first_name": "Pedro",
169-
"last_name": "Crespo",
170-
}
171-
}
172-
)
168+
@staticmethod
169+
def _update_json_schema_extra(schema: JsonDict) -> None:
170+
schema.update({"examples": [{"first_name": "Pedro", "last_name": "Crespo"}]})
171+
172+
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
173173

174174
@field_validator("user_name")
175175
@classmethod
@@ -207,6 +207,27 @@ def _validate_user_name(cls, value: str):
207207
return value
208208

209209

210+
#
211+
# PHONE REGISTRATION
212+
#
213+
214+
215+
class MyPhoneRegister(InputSchema):
216+
phone: Annotated[
217+
str,
218+
StringConstraints(strip_whitespace=True, min_length=1),
219+
Field(description="Phone number to register"),
220+
]
221+
222+
223+
class MyPhoneConfirm(InputSchema):
224+
code: Annotated[
225+
str,
226+
StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9]+$"),
227+
Field(description="Alphanumeric confirmation code"),
228+
]
229+
230+
210231
#
211232
# USER
212233
#

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class MyProfile(BaseModel):
3838
email: LowerCaseEmailStr
3939
role: UserRole
4040
privacy: PrivacyDict
41+
phone: str | None
4142
expiration_date: datetime.date | None = None
4243

4344
@staticmethod
@@ -50,6 +51,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
5051
"user_name": "PtN5Ab0uv",
5152
"first_name": "PtN5Ab0uv",
5253
"last_name": "",
54+
"phone": None,
5355
"role": "GUEST",
5456
"privacy": {
5557
"hide_email": True,

packages/models-library/tests/test_api_schemas_webserver_users.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,74 +8,74 @@
88
import pytest
99
from common_library.users_enums import UserRole
1010
from models_library.api_schemas_webserver.users import (
11-
MyProfileGet,
12-
MyProfilePatch,
11+
MyProfileRestGet,
12+
MyProfileRestPatch,
1313
)
1414
from pydantic import ValidationError
1515

1616

1717
@pytest.mark.parametrize("user_role", [u.name for u in UserRole])
1818
def test_profile_get_role(user_role: str):
19-
for example in MyProfileGet.model_json_schema()["examples"]:
19+
for example in MyProfileRestGet.model_json_schema()["examples"]:
2020
data = deepcopy(example)
2121
data["role"] = user_role
22-
m1 = MyProfileGet(**data)
22+
m1 = MyProfileRestGet(**data)
2323

2424
data["role"] = UserRole(user_role)
25-
m2 = MyProfileGet(**data)
25+
m2 = MyProfileRestGet(**data)
2626
assert m1 == m2
2727

2828

2929
def test_my_profile_patch_username_min_len():
3030
# minimum length username is 4
3131
with pytest.raises(ValidationError) as err_info:
32-
MyProfilePatch.model_validate({"userName": "abc"})
32+
MyProfileRestPatch.model_validate({"userName": "abc"})
3333

3434
assert err_info.value.error_count() == 1
3535
assert err_info.value.errors()[0]["type"] == "too_short"
3636

37-
MyProfilePatch.model_validate({"userName": "abcd"}) # OK
37+
MyProfileRestPatch.model_validate({"userName": "abcd"}) # OK
3838

3939

4040
def test_my_profile_patch_username_valid_characters():
4141
# Ensure valid characters (alphanumeric + . _ -)
4242
with pytest.raises(ValidationError, match="start with a letter") as err_info:
43-
MyProfilePatch.model_validate({"userName": "1234"})
43+
MyProfileRestPatch.model_validate({"userName": "1234"})
4444

4545
assert err_info.value.error_count() == 1
4646
assert err_info.value.errors()[0]["type"] == "value_error"
4747

48-
MyProfilePatch.model_validate({"userName": "u1234"}) # OK
48+
MyProfileRestPatch.model_validate({"userName": "u1234"}) # OK
4949

5050

5151
def test_my_profile_patch_username_special_characters():
5252
# Ensure no consecutive special characters
5353
with pytest.raises(
5454
ValidationError, match="consecutive special characters"
5555
) as err_info:
56-
MyProfilePatch.model_validate({"userName": "u1__234"})
56+
MyProfileRestPatch.model_validate({"userName": "u1__234"})
5757

5858
assert err_info.value.error_count() == 1
5959
assert err_info.value.errors()[0]["type"] == "value_error"
6060

61-
MyProfilePatch.model_validate({"userName": "u1_234"}) # OK
61+
MyProfileRestPatch.model_validate({"userName": "u1_234"}) # OK
6262

6363
# Ensure it doesn't end with a special character
6464
with pytest.raises(ValidationError, match="end with") as err_info:
65-
MyProfilePatch.model_validate({"userName": "u1234_"})
65+
MyProfileRestPatch.model_validate({"userName": "u1234_"})
6666

6767
assert err_info.value.error_count() == 1
6868
assert err_info.value.errors()[0]["type"] == "value_error"
6969

70-
MyProfilePatch.model_validate({"userName": "u1_234"}) # OK
70+
MyProfileRestPatch.model_validate({"userName": "u1_234"}) # OK
7171

7272

7373
def test_my_profile_patch_username_reserved_words():
7474
# Check reserved words (example list; extend as needed)
7575
with pytest.raises(ValidationError, match="cannot be used") as err_info:
76-
MyProfilePatch.model_validate({"userName": "admin"})
76+
MyProfileRestPatch.model_validate({"userName": "admin"})
7777

7878
assert err_info.value.error_count() == 1
7979
assert err_info.value.errors()[0]["type"] == "value_error"
8080

81-
MyProfilePatch.model_validate({"userName": "midas"}) # OK
81+
MyProfileRestPatch.model_validate({"userName": "midas"}) # OK

packages/models-library/tests/test_users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from models_library.api_schemas_webserver.users import MyProfileGet
1+
from models_library.api_schemas_webserver.users import MyProfileRestGet
22
from models_library.api_schemas_webserver.users_preferences import Preference
33
from models_library.groups import AccessRightsDict, Group, GroupsByTypeTuple
44
from models_library.users import MyProfile
@@ -22,6 +22,6 @@ def test_adapter_from_model_to_schema():
2222
)
2323
my_preferences = {"foo": Preference(default_value=3, value=1)}
2424

25-
MyProfileGet.from_domain_model(
25+
MyProfileRestGet.from_domain_model(
2626
my_profile, my_groups_by_type, my_product_group, my_preferences
2727
)

packages/postgres-database/docker/Dockerfile

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,5 @@ COPY entrypoint.bash /home/entrypoint.bash
5555

5656
RUN chmod +x /home/entrypoint.bash
5757

58-
ENV POSTGRES_USER=scu \
59-
POSTGRES_PASSWORD=adminadmin \
60-
POSTGRES_HOST=postgres \
61-
POSTGRES_PORT=5432 \
62-
POSTGRES_DB=simcoredb
63-
6458
ENTRYPOINT [ "/bin/bash", "/home/entrypoint.bash" ]
6559
CMD [ "sc-pg", "upgrade" ]

services/api-server/src/simcore_service_api_server/services_http/webserver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
ProjectInputUpdate,
3131
)
3232
from models_library.api_schemas_webserver.resource_usage import PricingPlanGet
33-
from models_library.api_schemas_webserver.users import MyProfileGet as WebProfileGet
33+
from models_library.api_schemas_webserver.users import MyProfileRestGet as WebProfileGet
3434
from models_library.api_schemas_webserver.users import (
35-
MyProfilePatch as WebProfileUpdate,
35+
MyProfileRestPatch as WebProfileUpdate,
3636
)
3737
from models_library.api_schemas_webserver.wallets import WalletGet
3838
from models_library.generics import Envelope

services/api-server/tests/unit/_with_db/test_api_user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pytest
1010
import respx
1111
from fastapi import FastAPI
12-
from models_library.api_schemas_webserver.users import MyProfileGet as WebProfileGet
12+
from models_library.api_schemas_webserver.users import MyProfileRestGet as WebProfileGet
1313
from pytest_mock import MockType
1414
from respx import MockRouter
1515
from simcore_service_api_server._meta import API_VTAG

0 commit comments

Comments
 (0)