Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5614d4e
adds phone numbers in models library
pcrespov Jul 16, 2025
6ed148b
testing formatting
pcrespov Jul 16, 2025
67e6870
downstream dependency
pcrespov Jul 16, 2025
96d4ece
validates pre-registration phone
pcrespov Jul 16, 2025
bcabc1d
updates
pcrespov Jul 16, 2025
bdd5ae9
registration phone
pcrespov Jul 16, 2025
8ce6af1
cleanup
pcrespov Jul 16, 2025
86100eb
renaming
pcrespov Jul 16, 2025
263f893
cleanup
pcrespov Jul 16, 2025
9b2cfd6
fixes
pcrespov Jul 16, 2025
d3d22b1
updates OAS
pcrespov Jul 16, 2025
5ad0a7b
fixing phone fixture
pcrespov Jul 16, 2025
25da7bb
enhance phone number validation to enforce E.164 format and update tests
pcrespov Jul 16, 2025
073782c
refactor request_product_account to improve error handling and valida…
pcrespov Jul 16, 2025
e942776
update message
pcrespov Jul 16, 2025
9489de7
services/webserver api version: 0.72.0 → 0.73.0
pcrespov Jul 16, 2025
ea4855f
moved logic to webserver
pcrespov Jul 17, 2025
f1270b6
phonumbers only in webserver
pcrespov Jul 17, 2025
9cee9aa
moving to models
pcrespov Jul 17, 2025
83457eb
fixes import
pcrespov Jul 17, 2025
66e59ce
duplicates
pcrespov Jul 17, 2025
9e88807
fixing phone
pcrespov Jul 17, 2025
f3d4d43
fixes pylint
pcrespov Jul 17, 2025
da58d27
f=tests
pcrespov Jul 17, 2025
7a3cae1
fixes tixture
pcrespov Jul 17, 2025
9b56b19
Merge branch 'master' into is280/validate-phone-numbers
pcrespov Jul 17, 2025
92580ab
fixes import
pcrespov Jul 17, 2025
20ae11e
fixes
pcrespov Jul 17, 2025
f882ba8
Merge branch 'master' into is280/validate-phone-numbers
pcrespov Jul 18, 2025
448f37a
fixes phones
pcrespov Jul 18, 2025
d0cfbb8
oas
pcrespov Jul 18, 2025
8bc1c1c
services/webserver api version: 0.73.0 → 0.73.1
pcrespov Jul 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/specs/web-server/_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
from models_library.api_schemas_webserver.users import (
MyFunctionPermissionsGet,
MyPermissionGet,
MyPhoneConfirm,
MyPhoneRegister,
MyProfileRestGet,
MyProfileRestPatch,
MyTokenCreate,
Expand All @@ -32,6 +30,10 @@
UserNotificationCreate,
UserNotificationPatch,
)
from simcore_service_webserver.users._controller.rest._rest_schemas import (
MyPhoneConfirm,
MyPhoneRegister,
)

router = APIRouter(prefix=f"/{API_VTAG}", tags=["users"])

Expand Down
4 changes: 2 additions & 2 deletions api/specs/web-server/_users_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from models_library.generics import Envelope
from models_library.rest_pagination import Page
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.users.schemas import PreRegisteredUserGet
from simcore_service_webserver.users.schemas import UserAccountRestPreRegister

router = APIRouter(prefix=f"/{API_VTAG}", tags=["users"])

Expand Down Expand Up @@ -69,4 +69,4 @@ async def search_user_accounts(
response_model=Envelope[UserAccountGet],
tags=_extra_tags,
)
async def pre_register_user_account(_body: PreRegisteredUserGet): ...
async def pre_register_user_account(_body: UserAccountRestPreRegister): ...
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from models_library.rest_base import RequestParameters
from pydantic import AliasGenerator, ConfigDict, Field, HttpUrl, SecretStr
from pydantic.alias_generators import to_camel
from pydantic.config import JsonDict

from ..emails import LowerCaseEmailStr
from ._base import InputSchema, OutputSchema
Expand All @@ -14,33 +15,39 @@ class AccountRequestInfo(InputSchema):
form: dict[str, Any]
captcha: str

@staticmethod
def _update_json_schema_extra(schema: JsonDict) -> None:
schema.update(
{
"example": {
"form": {
"firstName": "James",
"lastName": "Maxwel",
"email": "[email protected]",
"phone": "+41 44 245 96 96",
"company": "EM Com",
"address": "Infinite Loop",
"city": "Washington",
"postalCode": "98001",
"country": "Switzerland",
"application": "Antenna_Design",
"description": "Description of something",
"hear": "Search_Engine",
"privacyPolicy": True,
"eula": True,
},
"captcha": "A12B34",
}
}
)

model_config = ConfigDict(
str_strip_whitespace=True,
str_max_length=200,
# NOTE: this is just informative. The format of the form is defined
# currently in the front-end and it might change
# SEE image in https://github.com/ITISFoundation/osparc-simcore/pull/5378
json_schema_extra={
"example": {
"form": {
"firstName": "James",
"lastName": "Maxwel",
"email": "[email protected]",
"phone": "+1 123456789",
"company": "EM Com",
"address": "Infinite Loop",
"city": "Washington",
"postalCode": "98001",
"country": "USA",
"application": "Antenna_Design",
"description": "Description of something",
"hear": "Search_Engine",
"privacyPolicy": True,
"eula": True,
},
"captcha": "A12B34",
}
},
json_schema_extra=_update_json_schema_extra,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,6 @@ def _validate_user_name(cls, value: str):
return value


#
# PHONE REGISTRATION
#


class MyPhoneRegister(InputSchema):
phone: Annotated[
str,
StringConstraints(strip_whitespace=True, min_length=1),
Field(description="Phone number to register"),
]


class MyPhoneConfirm(InputSchema):
code: Annotated[
str,
StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9]+$"),
Field(description="Alphanumeric confirmation code"),
]


#
# USER
#
Expand Down
1 change: 1 addition & 0 deletions packages/models-library/src/models_library/rest_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:


class EnvelopedError(Envelope[None]):
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/443
error: ErrorGet

model_config = ConfigDict(
Expand Down
4 changes: 3 additions & 1 deletion packages/models-library/tests/test_users.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from models_library.api_schemas_webserver.users import MyProfileRestGet
from models_library.api_schemas_webserver.users import (
MyProfileRestGet,
)
from models_library.api_schemas_webserver.users_preferences import Preference
from models_library.groups import AccessRightsDict, Group, GroupsByTypeTuple
from models_library.users import MyProfile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
from uuid import uuid4

import arrow
import faker
from faker import Faker

DEFAULT_FAKER: Final = faker.Faker()
DEFAULT_FAKER: Final = Faker()


def random_icon_url(fake: Faker):
Expand All @@ -34,6 +33,15 @@ def random_thumbnail_url(fake: Faker):
return fake.image_url(width=32, height=32)


def random_phone_number(fake: Faker = DEFAULT_FAKER) -> str:
# NOTE: faker.phone_number() does not validate with `phonenumbers` library.
phone = fake.random_element(
["+41763456789", "+19104630364", "+13013044567", "+34 950 453 837"]
)
tail = f"{fake.pyint(100, 999)}"
return phone[: -len(tail)] + tail # ensure phone keeps its length


def _compute_hash(password: str) -> str:
try:
# 'passlib' will be used only if already installed.
Expand Down Expand Up @@ -105,7 +113,7 @@ def random_pre_registration_details(
"pre_first_name": fake.first_name(),
"pre_last_name": fake.last_name(),
"pre_email": fake.email(),
"pre_phone": fake.phone_number(),
"pre_phone": random_phone_number(fake),
"institution": fake.company(),
"address": fake.address().replace("\n", ", "),
"city": fake.city(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from typing import TypeVar

from aiohttp import web
from common_library.json_serialization import json_dumps
from common_library.user_messages import user_message
from models_library.rest_error import EnvelopedError
from pydantic import BaseModel, TypeAdapter, ValidationError

from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
Expand All @@ -25,14 +26,13 @@

@contextmanager
def handle_validation_as_http_error(
*, error_msg_template: str, resource_name: str, use_error_v1: bool
*, error_msg_template: str, resource_name: str
) -> Iterator[None]:
"""Context manager to handle ValidationError and reraise them as HTTPUnprocessableEntity error

Arguments:
error_msg_template -- _description_
resource_name --
use_error_v1 -- If True, it uses new error response

Raises:
web.HTTPUnprocessableEntity: (422) raised from a ValidationError
Expand All @@ -43,49 +43,37 @@ def handle_validation_as_http_error(
yield

except ValidationError as err:
details = [
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/443
_details = [
{
"loc": ".".join(map(str, e["loc"])),
"loc": ".".join(map(str, e["loc"])), # e.g. "body.name"
"msg": e["msg"],
"type": e["type"],
}
for e in err.errors()
]
user_error_message = error_msg_template.format(
failed=", ".join(d["loc"] for d in details)
)

if use_error_v1:
# NOTE: keeps backwards compatibility until ligher error response is implemented in the entire API
# Implements servicelib.aiohttp.rest_responses.ErrorItemType
errors = [
{
"code": e["type"],
"message": e["msg"],
"resource": resource_name,
"field": e["loc"],
}
for e in details
]
error_json_str = json_dumps(
{
"error": {
"status": status.HTTP_422_UNPROCESSABLE_ENTITY,
"errors": errors,
}
}
)
else:
# NEW proposed error for https://github.com/ITISFoundation/osparc-simcore/issues/443
error_json_str = json_dumps(
{
"error": {
"msg": user_error_message,
"resource": resource_name, # optional
"details": details, # optional
}

errors_details = [
{
"code": e["type"],
"message": e["msg"],
"resource": resource_name,
"field": e["loc"],
}
for e in _details
]

error_json_str = EnvelopedError.model_validate(
{
"error": {
"message": error_msg_template.format(
failed=", ".join(e["field"] for e in errors_details)
),
"status": status.HTTP_422_UNPROCESSABLE_ENTITY,
"errors": errors_details,
}
)
}
).model_dump_json(exclude_unset=True, exclude_none=True)

raise web.HTTPUnprocessableEntity( # 422
text=error_json_str,
Expand All @@ -104,15 +92,10 @@ def handle_validation_as_http_error(
def parse_request_path_parameters_as(
parameters_schema_cls: type[ModelClass],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelClass:
"""Parses path parameters from 'request' and validates against 'parameters_schema'


Keyword Arguments:
use_enveloped_error_v1 -- new enveloped error model (default: {True})

Raises:
web.HTTPUnprocessableEntity: (422) if validation of parameters fail

Expand All @@ -121,9 +104,10 @@ def parse_request_path_parameters_as(
"""

with handle_validation_as_http_error(
error_msg_template="Invalid parameter/s '{failed}' in request path",
error_msg_template=user_message(
"Invalid parameter/s '{failed}' in request path"
),
resource_name=request.rel_url.path,
use_error_v1=use_enveloped_error_v1,
):
data = dict(request.match_info)
return parameters_schema_cls.model_validate(data)
Expand All @@ -132,15 +116,10 @@ def parse_request_path_parameters_as(
def parse_request_query_parameters_as(
parameters_schema_cls: type[ModelClass],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelClass:
"""Parses query parameters from 'request' and validates against 'parameters_schema'


Keyword Arguments:
use_enveloped_error_v1 -- new enveloped error model (default: {True})

Raises:
web.HTTPUnprocessableEntity: (422) if validation of parameters fail

Expand All @@ -149,9 +128,10 @@ def parse_request_query_parameters_as(
"""

with handle_validation_as_http_error(
error_msg_template="Invalid parameter/s '{failed}' in request query",
error_msg_template=user_message(
"Invalid parameter/s '{failed}' in request query"
),
resource_name=request.rel_url.path,
use_error_v1=use_enveloped_error_v1,
):
# NOTE: Currently, this does not take into consideration cases where there are multiple
# query parameters with the same key. However, we are not using such cases anywhere at the moment.
Expand All @@ -166,13 +146,12 @@ def parse_request_query_parameters_as(
def parse_request_headers_as(
parameters_schema_cls: type[ModelClass],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelClass:
with handle_validation_as_http_error(
error_msg_template="Invalid parameter/s '{failed}' in request headers",
error_msg_template=user_message(
"Invalid parameter/s '{failed}' in request headers"
),
resource_name=request.rel_url.path,
use_error_v1=use_enveloped_error_v1,
):
data = dict(request.headers)
return parameters_schema_cls.model_validate(data)
Expand All @@ -181,8 +160,6 @@ def parse_request_headers_as(
async def parse_request_body_as(
model_schema_cls: type[ModelOrListOrDictType],
request: web.Request,
*,
use_enveloped_error_v1: bool = True,
) -> ModelOrListOrDictType:
"""Parses and validates request body against schema

Expand All @@ -197,9 +174,8 @@ async def parse_request_body_as(
Validated model of request body
"""
with handle_validation_as_http_error(
error_msg_template="Invalid field/s '{failed}' in request body",
error_msg_template=user_message("Invalid field/s '{failed}' in request body"),
resource_name=request.rel_url.path,
use_error_v1=use_enveloped_error_v1,
):
if not request.can_read_body:
# requests w/o body e.g. when model-schema is fully optional
Expand Down
Loading
Loading