From 5d76e4db54cf5dacae28803a01210f2280d90b1f Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 14:31:41 +0200 Subject: [PATCH 01/15] upgrade reqs --- services/invitations/requirements/_base.txt | 42 +++++++++++--------- services/invitations/requirements/_test.txt | 14 ------- services/invitations/requirements/_tools.txt | 11 ----- 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/services/invitations/requirements/_base.txt b/services/invitations/requirements/_base.txt index 68a381d3f12..413ac0b5d35 100644 --- a/services/invitations/requirements/_base.txt +++ b/services/invitations/requirements/_base.txt @@ -19,6 +19,8 @@ aiormq==6.8.0 # via aio-pika aiosignal==1.3.1 # via aiohttp +annotated-types==0.7.0 + # via pydantic anyio==4.3.0 # via # fast-depends @@ -31,10 +33,6 @@ arrow==1.3.0 # -r requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -async-timeout==4.0.3 - # via - # aiohttp - # redis attrs==23.2.0 # via # aiohttp @@ -69,18 +67,10 @@ dnspython==2.6.1 # via email-validator email-validator==2.1.1 # via pydantic -exceptiongroup==1.2.0 - # via anyio fast-depends==2.4.2 # via faststream -fastapi==0.99.1 +fastapi==0.115.0 # via - # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../requirements/constraints.txt # -r requirements/../../../packages/service-library/requirements/_fastapi.in # -r requirements/_base.in # prometheus-fastapi-instrumentator @@ -149,12 +139,11 @@ prometheus-fastapi-instrumentator==6.1.0 # via -r requirements/../../../packages/service-library/requirements/_fastapi.in pycparser==2.21 # via cffi -pydantic==1.10.14 +pydantic==2.9.2 # via # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt - # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../requirements/constraints.txt @@ -165,6 +154,20 @@ pydantic==1.10.14 # -r requirements/../../../packages/settings-library/requirements/_base.in # fast-depends # fastapi + # pydantic-extra-types + # pydantic-settings +pydantic-core==2.23.4 + # via pydantic +pydantic-extra-types==2.9.0 + # via + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in +pydantic-settings==2.5.2 + # via + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/_base.in pygments==2.17.2 # via rich pyinstrument==4.6.2 @@ -172,7 +175,9 @@ pyinstrument==4.6.2 python-dateutil==2.9.0.post0 # via arrow python-dotenv==1.0.1 - # via uvicorn + # via + # pydantic-settings + # uvicorn pyyaml==6.0.1 # via # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -214,7 +219,7 @@ sniffio==1.3.1 # via # anyio # httpx -starlette==0.27.0 +starlette==0.38.6 # via # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -241,12 +246,11 @@ typing-extensions==4.10.0 # via # aiodebug # aiodocker - # anyio # fastapi # faststream # pydantic + # pydantic-core # typer - # uvicorn uvicorn==0.29.0 # via # -r requirements/../../../packages/service-library/requirements/_fastapi.in diff --git a/services/invitations/requirements/_test.txt b/services/invitations/requirements/_test.txt index 4571dba8ffa..7b029ccb0de 100644 --- a/services/invitations/requirements/_test.txt +++ b/services/invitations/requirements/_test.txt @@ -16,12 +16,6 @@ coverage==7.6.1 # via # -r requirements/_test.in # pytest-cov -exceptiongroup==1.2.0 - # via - # -c requirements/_base.txt - # anyio - # hypothesis - # pytest faker==27.0.0 # via -r requirements/_test.in h11==0.14.0 @@ -90,11 +84,3 @@ sortedcontainers==2.4.0 # via hypothesis termcolor==2.4.0 # via pytest-sugar -tomli==2.0.1 - # via - # coverage - # pytest -typing-extensions==4.10.0 - # via - # -c requirements/_base.txt - # anyio diff --git a/services/invitations/requirements/_tools.txt b/services/invitations/requirements/_tools.txt index b17c8a2c2d8..f3932792cef 100644 --- a/services/invitations/requirements/_tools.txt +++ b/services/invitations/requirements/_tools.txt @@ -70,22 +70,11 @@ ruff==0.6.1 # via -r requirements/../../../requirements/devenv.txt setuptools==73.0.1 # via pip-tools -tomli==2.0.1 - # via - # -c requirements/_test.txt - # black - # build - # mypy - # pip-tools - # pylint tomlkit==0.13.2 # via pylint typing-extensions==4.10.0 # via # -c requirements/_base.txt - # -c requirements/_test.txt - # astroid - # black # mypy virtualenv==20.26.3 # via pre-commit From cafb49f102274ab63eee80257e3aa61d858d38bf Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 14:35:53 +0200 Subject: [PATCH 02/15] fix code --- .../src/simcore_service_invitations/cli.py | 16 +++--- .../core/settings.py | 18 ++++-- .../services/invitations.py | 56 +++++++------------ .../invitations/tests/unit/api/conftest.py | 2 +- .../tests/unit/api/test_api_invitations.py | 6 +- .../tests/unit/api/test_api_meta.py | 2 +- services/invitations/tests/unit/conftest.py | 2 +- .../tests/unit/test__model_examples.py | 2 +- services/invitations/tests/unit/test_cli.py | 10 +++- 9 files changed, 56 insertions(+), 58 deletions(-) diff --git a/services/invitations/src/simcore_service_invitations/cli.py b/services/invitations/src/simcore_service_invitations/cli.py index dffb1dca32f..cfb7cf2717a 100644 --- a/services/invitations/src/simcore_service_invitations/cli.py +++ b/services/invitations/src/simcore_service_invitations/cli.py @@ -5,7 +5,7 @@ from cryptography.fernet import Fernet from models_library.emails import LowerCaseEmailStr from models_library.invitations import InvitationContent, InvitationInputs -from pydantic import EmailStr, HttpUrl, ValidationError, parse_obj_as +from pydantic import EmailStr, HttpUrl, TypeAdapter, ValidationError from rich.console import Console from servicelib.utils_secrets import generate_password from settings_library.utils_cli import ( @@ -96,19 +96,19 @@ def invite( ctx: typer.Context, email: str = typer.Argument( ..., - callback=lambda v: parse_obj_as(LowerCaseEmailStr, v), + callback=lambda v: TypeAdapter(LowerCaseEmailStr).validate_python(v), help="Custom invitation for a given guest", ), issuer: str = typer.Option( - ..., help=InvitationInputs.__fields__["issuer"].field_info.description + ..., help=InvitationInputs.model_fields["issuer"].description ), trial_account_days: int = typer.Option( None, - help=InvitationInputs.__fields__["trial_account_days"].field_info.description, + help=InvitationInputs.model_fields["trial_account_days"].description, ), product: str = typer.Option( None, - help=InvitationInputs.__fields__["product"].field_info.description, + help=InvitationInputs.model_fields["product"].description, ), ): """Creates an invitation link for user with 'email' and issued by 'issuer'""" @@ -117,7 +117,7 @@ def invite( invitation_data = InvitationInputs( issuer=issuer, - guest=parse_obj_as(EmailStr, email), + guest=TypeAdapter(EmailStr).validate_python(email), trial_account_days=trial_account_days, extra_credits_in_usd=None, product=product, @@ -142,14 +142,14 @@ def extract(ctx: typer.Context, invitation_url: str): try: invitation: InvitationContent = extract_invitation_content( invitation_code=extract_invitation_code_from_query( - parse_obj_as(HttpUrl, invitation_url) + TypeAdapter(HttpUrl).validate_python(invitation_url) ), secret_key=settings.INVITATIONS_SECRET_KEY.get_secret_value().encode(), default_product=settings.INVITATIONS_DEFAULT_PRODUCT, ) assert invitation.product is not None # nosec - print(invitation.json(indent=1)) # noqa: T201 + print(invitation.model_dump_json(indent=1)) # noqa: T201 except (InvalidInvitationCodeError, ValidationError): _err_console.print("[bold red]Invalid code[/bold red]") diff --git a/services/invitations/src/simcore_service_invitations/core/settings.py b/services/invitations/src/simcore_service_invitations/core/settings.py index 6af076f4229..29d286f1020 100644 --- a/services/invitations/src/simcore_service_invitations/core/settings.py +++ b/services/invitations/src/simcore_service_invitations/core/settings.py @@ -1,7 +1,14 @@ from functools import cached_property from models_library.products import ProductName -from pydantic import Field, HttpUrl, PositiveInt, SecretStr, validator +from pydantic import ( + AliasChoices, + Field, + HttpUrl, + PositiveInt, + SecretStr, + field_validator, +) from settings_library.base import BaseCustomSettings from settings_library.basic_types import BuildTargetEnum, LogLevel, VersionTag from settings_library.utils_logging import MixinLoggingSettings @@ -38,14 +45,15 @@ class _BaseApplicationSettings(BaseCustomSettings, MixinLoggingSettings): # RUNTIME ----------------------------------------------------------- INVITATIONS_LOGLEVEL: LogLevel = Field( - default=LogLevel.INFO, env=["INVITATIONS_LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"] + default=LogLevel.INFO, + validation_alias=AliasChoices("INVITATIONS_LOGLEVEL", "LOG_LEVEL", "LOGLEVEL"), ) INVITATIONS_LOG_FORMAT_LOCAL_DEV_ENABLED: bool = Field( default=False, - env=[ + validation_alias=AliasChoices( "INVITATIONS_LOG_FORMAT_LOCAL_DEV_ENABLED", "LOG_FORMAT_LOCAL_DEV_ENABLED", - ], + ), description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!", ) @@ -53,7 +61,7 @@ class _BaseApplicationSettings(BaseCustomSettings, MixinLoggingSettings): def LOG_LEVEL(self): return self.INVITATIONS_LOGLEVEL - @validator("INVITATIONS_LOGLEVEL") + @field_validator("INVITATIONS_LOGLEVEL") @classmethod def valid_log_level(cls, value: str) -> str: return cls.validate_log_level(value) diff --git a/services/invitations/src/simcore_service_invitations/services/invitations.py b/services/invitations/src/simcore_service_invitations/services/invitations.py index d5bed8662cf..1cdc33f4085 100644 --- a/services/invitations/src/simcore_service_invitations/services/invitations.py +++ b/services/invitations/src/simcore_service_invitations/services/invitations.py @@ -1,18 +1,22 @@ import base64 import binascii import logging -from typing import Any, ClassVar, cast from urllib import parse +from common_library.pydantic_networks_extension import HttpUrlLegacy from cryptography.fernet import Fernet, InvalidToken from models_library.invitations import InvitationContent, InvitationInputs from models_library.products import ProductName -from pydantic import HttpUrl, ValidationError, parse_obj_as +from pydantic import ConfigDict, HttpUrl, TypeAdapter, ValidationError from starlette.datastructures import URL _logger = logging.getLogger(__name__) +def _to_initial(v: str): + return v[0] + + class InvalidInvitationCodeError(Exception): ... @@ -23,9 +27,9 @@ class _ContentWithShortNames(InvitationContent): @classmethod def serialize(cls, model_obj: InvitationContent) -> str: """Exports to json using *short* aliases and values in order to produce shorter codes""" - model_w_short_aliases_json: str = cls.construct( - **model_obj.dict(exclude_unset=True) - ).json(exclude_unset=True, by_alias=True) + model_w_short_aliases_json: str = cls.model_construct( + **model_obj.model_dump(exclude_unset=True) + ).model_dump_json(exclude_unset=True, by_alias=True) # NOTE: json arguments try to minimize the amount of data # serialized. The CONS is that it relies on models in the code # that might change over time. This might lead to some datasets in codes @@ -35,36 +39,18 @@ def serialize(cls, model_obj: InvitationContent) -> str: @classmethod def deserialize(cls, raw_json: str) -> InvitationContent: """Parses a json string and returns InvitationContent model""" - model_w_short_aliases = cls.parse_raw(raw_json) - return InvitationContent.construct( - **model_w_short_aliases.dict(exclude_unset=True) + model_w_short_aliases = cls.model_validate_json(raw_json) + return InvitationContent.model_construct( + **model_w_short_aliases.model_dump(exclude_unset=True) ) - class Config: - allow_population_by_field_name = True # NOTE: can parse using field names - allow_mutation = False - anystr_strip_whitespace = True + model_config = ConfigDict( # NOTE: Can export with alias: short aliases to minimize the size of serialization artifact - fields: ClassVar[dict[str, Any]] = { - "issuer": { - "alias": "i", - }, - "guest": { - "alias": "g", - }, - "trial_account_days": { - "alias": "t", - }, - "extra_credits_in_usd": { - "alias": "e", - }, - "product": { - "alias": "p", - }, - "created": { - "alias": "c", - }, - } + alias_generator=_to_initial, + populate_by_name=True, # NOTE: can parse using field names + frozen=True, + str_strip_whitespace=True, + ) # @@ -79,9 +65,9 @@ def _build_link( r = URL("/registration").include_query_params(invitation=code_url_safe) # Adds query to fragment - base_url = f"{base_url.rstrip('/')}/" + base_url = f"{base_url}/" url = URL(base_url).replace(fragment=f"{r}") - return cast(HttpUrl, parse_obj_as(HttpUrl, f"{url}")) + return TypeAdapter(HttpUrlLegacy).validate_python(f"{url}") def _fernet_encrypt_as_urlsafe_code( @@ -124,7 +110,7 @@ def create_invitation_link_and_content( code = _create_invitation_code(content, secret_key) # Adds message as the invitation in query link = _build_link( - base_url=base_url, + base_url=f"{base_url}", code_url_safe=code.decode(), ) return link, content diff --git a/services/invitations/tests/unit/api/conftest.py b/services/invitations/tests/unit/api/conftest.py index f4151fcc519..c558ac496ad 100644 --- a/services/invitations/tests/unit/api/conftest.py +++ b/services/invitations/tests/unit/api/conftest.py @@ -18,7 +18,7 @@ def client(app_environment: EnvVarsDict) -> Iterator[TestClient]: print(f"app_environment={json.dumps(app_environment)}") app = create_app() - print("settings:\n", app.state.settings.json(indent=1)) + print("settings:\n", app.state.settings.model_dump_json(indent=1)) with TestClient(app, base_url="http://testserver.test") as client: yield client diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 572f30f8173..87ee9c28800 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -64,7 +64,7 @@ def test_check_invitation( # up ot here, identifcal to above. # Let's use invitation link - invitation_url = ApiInvitationContentAndLink.parse_obj( + invitation_url = ApiInvitationContentAndLink.model_validate( response.json() ).invitation_url @@ -77,7 +77,7 @@ def test_check_invitation( assert response.status_code == 200, f"{response.json()=}" # decrypted invitation should be identical to request above - invitation = InvitationContent.parse_obj(response.json()) + invitation = InvitationContent.model_validate(response.json()) assert invitation.issuer == invitation_data.issuer assert invitation.guest == invitation_data.guest assert invitation.trial_account_days == invitation_data.trial_account_days @@ -106,7 +106,7 @@ def test_check_valid_invitation( assert response.status_code == 200, f"{response.json()=}" # decrypted invitation should be identical to request above - invitation = InvitationContent.parse_obj(response.json()) + invitation = InvitationContent.model_validate(response.json()) assert invitation.issuer == invitation_data.issuer assert invitation.guest == invitation_data.guest diff --git a/services/invitations/tests/unit/api/test_api_meta.py b/services/invitations/tests/unit/api/test_api_meta.py index cee4afd13c9..ff9c3768e5e 100644 --- a/services/invitations/tests/unit/api/test_api_meta.py +++ b/services/invitations/tests/unit/api/test_api_meta.py @@ -19,7 +19,7 @@ def test_healthcheck(client: TestClient): def test_meta(client: TestClient): response = client.get(f"/{API_VTAG}/meta") assert response.status_code == status.HTTP_200_OK - meta = _Meta.parse_obj(response.json()) + meta = _Meta.model_validate(response.json()) response = client.get(meta.docs_url) assert response.status_code == status.HTTP_200_OK diff --git a/services/invitations/tests/unit/conftest.py b/services/invitations/tests/unit/conftest.py index 1b6ea4ee6e9..fde239d5332 100644 --- a/services/invitations/tests/unit/conftest.py +++ b/services/invitations/tests/unit/conftest.py @@ -109,4 +109,4 @@ def invitation_data( if product: kwargs["product"] = product - return InvitationInputs.parse_obj(kwargs) + return InvitationInputs.model_validate(kwargs) diff --git a/services/invitations/tests/unit/test__model_examples.py b/services/invitations/tests/unit/test__model_examples.py index 31ed0dfc603..78dfdd96669 100644 --- a/services/invitations/tests/unit/test__model_examples.py +++ b/services/invitations/tests/unit/test__model_examples.py @@ -26,4 +26,4 @@ def test_model_examples( model_cls: type[BaseModel], example_name: int, example_data: Any ): print(example_name, ":", json.dumps(example_data)) - assert model_cls.parse_obj(example_data) + assert model_cls.model_validate(example_data) diff --git a/services/invitations/tests/unit/test_cli.py b/services/invitations/tests/unit/test_cli.py index e77247115cf..c83623a2f8b 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -8,6 +8,7 @@ import pytest from faker import Faker from models_library.products import ProductName +from pydantic import TypeAdapter from pytest_simcore.helpers.monkeypatch_envs import load_dotenv, setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict from simcore_service_invitations._meta import API_VERSION @@ -71,7 +72,10 @@ def test_invite_user_and_check_invitation( env=environs, ) assert result.exit_code == os.EX_OK, result.output - assert expected == InvitationInputs.parse_raw(result.stdout).dict() + assert ( + expected + == TypeAdapter(InvitationInputs).validate_json(result.stdout).model_dump() + ) def test_echo_dotenv(cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch): @@ -82,7 +86,7 @@ def test_echo_dotenv(cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch): environs = load_dotenv(result.stdout) envs = setenvs_from_dict(monkeypatch, environs) - settings_from_obj = ApplicationSettings.parse_obj(envs) + settings_from_obj = ApplicationSettings.model_validate(envs) settings_from_envs = ApplicationSettings.create_from_envs() assert settings_from_envs == settings_from_obj @@ -93,5 +97,5 @@ def test_list_settings(cli_runner: CliRunner, app_environment: EnvVarsDict): assert result.exit_code == os.EX_OK, result.output print(result.output) - settings = ApplicationSettings.parse_raw(result.output) + settings = TypeAdapter(ApplicationSettings).validate_json(result.output) assert settings == ApplicationSettings.create_from_envs() From c14111895a6c958286e24fa17f6f3e9d3e2a9b02 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 14:45:06 +0200 Subject: [PATCH 03/15] fix mypy --- .../invitations/tests/unit/test_invitations.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/invitations/tests/unit/test_invitations.py b/services/invitations/tests/unit/test_invitations.py index edd9c01556b..a16dd743cc9 100644 --- a/services/invitations/tests/unit/test_invitations.py +++ b/services/invitations/tests/unit/test_invitations.py @@ -28,7 +28,7 @@ def test_all_invitation_fields_have_short_and_unique_aliases(): # all have short alias all_alias = [] - for field in _ContentWithShortNames.__fields__.values(): + for field in _ContentWithShortNames.model_fields.values(): assert field.alias assert field.alias not in all_alias all_alias.append(field.alias) @@ -38,7 +38,7 @@ def test_import_and_export_invitation_alias_by_alias( invitation_data: InvitationInputs, ): expected_content = InvitationContent( - **invitation_data.dict(), + **invitation_data.model_dump(), created=datetime.now(tz=timezone.utc), ) raw_data = _ContentWithShortNames.serialize(expected_content) @@ -51,13 +51,13 @@ def test_export_by_alias_produces_smaller_strings( invitation_data: InvitationInputs, ): content = InvitationContent( - **invitation_data.dict(), + **invitation_data.model_dump(), created=datetime.now(tz=timezone.utc), ) raw_data = _ContentWithShortNames.serialize(content) # export by alias produces smaller strings - assert len(raw_data) < len(content.json()) + assert len(raw_data) < len(content.model_dump_json()) def test_create_and_decrypt_invitation( @@ -85,9 +85,9 @@ def test_create_and_decrypt_invitation( assert isinstance(invitation, InvitationContent) assert invitation.product is not None - expected = invitation_data.dict(exclude_none=True) + expected = invitation_data.model_dump(exclude_none=True) expected.setdefault("product", default_product) - assert invitation.dict(exclude={"created"}, exclude_none=True) == expected + assert invitation.model_dump(exclude={"created"}, exclude_none=True) == expected # @@ -116,9 +116,9 @@ def test_valid_invitation_code( default_product=default_product, ) - expected = invitation_data.dict(exclude_none=True) + expected = invitation_data.model_dump(exclude_none=True) expected.setdefault("product", default_product) - assert invitation.dict(exclude={"created"}, exclude_none=True) == expected + assert invitation.model_dump(exclude={"created"}, exclude_none=True) == expected def test_invalid_invitation_encoding( @@ -176,7 +176,7 @@ class OtherModel(BaseModel): secret = secret_key.encode() other_code = _fernet_encrypt_as_urlsafe_code( - data=OtherModel().json().encode(), secret_key=secret + data=OtherModel().model_dump_json().encode(), secret_key=secret ) with pytest.raises(ValidationError): From 8e49f763ab21a0563da569e8596ac63f8386f3fb Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 14:49:11 +0200 Subject: [PATCH 04/15] fix mypy --- services/invitations/tests/unit/api/test_api_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 87ee9c28800..693333f8aed 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -31,7 +31,7 @@ def test_create_invitation( ): response = client.post( f"/{API_VTAG}/invitations", - json=invitation_input.dict(exclude_none=True), + json=invitation_input.model_dump(exclude_none=True), auth=basic_auth, ) assert response.status_code == status.HTTP_200_OK, f"{response.json()=}" From 90f4556a37325780e011f960e3f27e8a0c5e92ae Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 16:04:27 +0200 Subject: [PATCH 05/15] fix model copy --- packages/models-library/src/models_library/invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/invitations.py b/packages/models-library/src/models_library/invitations.py index b1a8e4154e9..595c09b6012 100644 --- a/packages/models-library/src/models_library/invitations.py +++ b/packages/models-library/src/models_library/invitations.py @@ -50,7 +50,7 @@ class InvitationContent(InvitationInputs): created: datetime = Field(..., description="Timestamp for creation") def as_invitation_inputs(self) -> InvitationInputs: - return self.copy(exclude={"created"}) + return self.model_validate(self.model_dump(exclude={"created"})) # copy excluding "created" @classmethod def create_from_inputs( From 204ecf3b3d0359c07e78a214b7ab0da84441e7d4 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 16:09:19 +0200 Subject: [PATCH 06/15] fix deprecated dict --- .../src/simcore_service_invitations/api/_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/src/simcore_service_invitations/api/_invitations.py b/services/invitations/src/simcore_service_invitations/api/_invitations.py index 4422bda3135..e0fa2b1d0e9 100644 --- a/services/invitations/src/simcore_service_invitations/api/_invitations.py +++ b/services/invitations/src/simcore_service_invitations/api/_invitations.py @@ -48,7 +48,7 @@ async def create_invitation( default_product=settings.INVITATIONS_DEFAULT_PRODUCT, ) invitation = ApiInvitationContentAndLink( - **invitation_content.dict(), + **invitation_content.model_dump(), invitation_url=invitation_link, ) From f70fa05c06e0a93a70181daf5fcd8f78f2126783 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 16:13:35 +0200 Subject: [PATCH 07/15] fix deprecated method --- .../src/simcore_service_invitations/api/_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/src/simcore_service_invitations/api/_invitations.py b/services/invitations/src/simcore_service_invitations/api/_invitations.py index e0fa2b1d0e9..0a680189c61 100644 --- a/services/invitations/src/simcore_service_invitations/api/_invitations.py +++ b/services/invitations/src/simcore_service_invitations/api/_invitations.py @@ -52,7 +52,7 @@ async def create_invitation( invitation_url=invitation_link, ) - _logger.info("New invitation: %s", f"{invitation.json(indent=1)}") + _logger.info("New invitation: %s", f"{invitation.model_dump_json(indent=1)}") return invitation From d781534db93b48bbcd66b8559545c72dc6be54aa Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 16:18:36 +0200 Subject: [PATCH 08/15] fix url type --- .../invitations/src/simcore_service_invitations/api/_meta.py | 5 +++-- services/invitations/tests/unit/api/test_api_meta.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/invitations/src/simcore_service_invitations/api/_meta.py b/services/invitations/src/simcore_service_invitations/api/_meta.py index 621f3b4445e..0b1e6a26fe5 100644 --- a/services/invitations/src/simcore_service_invitations/api/_meta.py +++ b/services/invitations/src/simcore_service_invitations/api/_meta.py @@ -2,7 +2,8 @@ from collections.abc import Callable from fastapi import APIRouter, Depends -from pydantic import BaseModel, HttpUrl +from pydantic import BaseModel +from simcore_service_invitations.services.invitations import HttpUrlLegacy from .._meta import API_VERSION, PROJECT_NAME from ._dependencies import get_reverse_url_mapper @@ -20,7 +21,7 @@ class _Meta(BaseModel): name: str version: str - docs_url: HttpUrl + docs_url: HttpUrlLegacy # diff --git a/services/invitations/tests/unit/api/test_api_meta.py b/services/invitations/tests/unit/api/test_api_meta.py index ff9c3768e5e..4fe4f39b22c 100644 --- a/services/invitations/tests/unit/api/test_api_meta.py +++ b/services/invitations/tests/unit/api/test_api_meta.py @@ -21,5 +21,5 @@ def test_meta(client: TestClient): assert response.status_code == status.HTTP_200_OK meta = _Meta.model_validate(response.json()) - response = client.get(meta.docs_url) + response = client.get(f"{meta.docs_url}") assert response.status_code == status.HTTP_200_OK From 676e65e4501b2a3c84fa0bf7aa501a794a6cba62 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 20:58:01 +0200 Subject: [PATCH 09/15] make openapi.json --- services/invitations/openapi.json | 118 +++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 36 deletions(-) diff --git a/services/invitations/openapi.json b/services/invitations/openapi.json index 96630799e46..508191b0419 100644 --- a/services/invitations/openapi.json +++ b/services/invitations/openapi.json @@ -175,18 +175,32 @@ "description": "Invitee's email. Note that the registration can ONLY be used with this email" }, "trial_account_days": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Trial Account Days", - "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", - "minimum": 0 + "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires" }, "extra_credits_in_usd": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Extra Credits In Usd", - "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD", - "minimum": 0 + "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD" }, "product": { "type": "string", @@ -208,13 +222,12 @@ "created" ], "title": "ApiInvitationContent", - "description": "Data in an invitation", "example": { - "issuer": "issuerid", + "created": "2023-01-11 13:11:47.293595", "guest": "invitedguest@company.com", - "trial_account_days": 2, + "issuer": "issuerid", "product": "osparc", - "created": "2023-01-11 13:11:47.293595" + "trial_account_days": 2 } }, "ApiInvitationContentAndLink": { @@ -233,18 +246,32 @@ "description": "Invitee's email. Note that the registration can ONLY be used with this email" }, "trial_account_days": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Trial Account Days", - "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", - "minimum": 0 + "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires" }, "extra_credits_in_usd": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Extra Credits In Usd", - "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD", - "minimum": 0 + "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD" }, "product": { "type": "string", @@ -275,14 +302,13 @@ "invitation_url" ], "title": "ApiInvitationContentAndLink", - "description": "Data in an invitation", "example": { - "issuer": "issuerid", + "created": "2023-01-11 13:11:47.293595", "guest": "invitedguest@company.com", - "trial_account_days": 2, + "invitation_url": "https://foo.com/#/registration?invitation=1234", + "issuer": "issuerid", "product": "osparc", - "created": "2023-01-11 13:11:47.293595", - "invitation_url": "https://foo.com/#/registration?invitation=1234" + "trial_account_days": 2 } }, "ApiInvitationInputs": { @@ -301,21 +327,42 @@ "description": "Invitee's email. Note that the registration can ONLY be used with this email" }, "trial_account_days": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Trial Account Days", - "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", - "minimum": 0 + "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires" }, "extra_credits_in_usd": { - "type": "integer", - "exclusiveMinimum": true, + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": true, + "minimum": 0 + }, + { + "type": "null" + } + ], "title": "Extra Credits In Usd", - "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD", - "minimum": 0 + "description": "If set, the account's primary wallet will add extra credits corresponding to this ammount in USD" }, "product": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Product", "description": "If None, it will use INVITATIONS_DEFAULT_PRODUCT" } @@ -326,10 +373,9 @@ "guest" ], "title": "ApiInvitationInputs", - "description": "Input data necessary to create an invitation", "example": { - "issuer": "issuerid", "guest": "invitedguest@company.com", + "issuer": "issuerid", "trial_account_days": 2 } }, From 998585d91b315b7a147a0a6e46787e9613cd5b9d Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 21:02:31 +0200 Subject: [PATCH 10/15] fix url --- services/invitations/tests/unit/api/test_api_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 693333f8aed..226b79363c2 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -71,7 +71,7 @@ def test_check_invitation( # check invitation_url response = client.post( f"/{API_VTAG}/invitations:extract", - json={"invitation_url": invitation_url}, + json={"invitation_url": f"{invitation_url}"}, auth=basic_auth, ) assert response.status_code == 200, f"{response.json()=}" From bbd7485ff2616320ddcfa5e0536b9a09fafd52cb Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 21:46:11 +0200 Subject: [PATCH 11/15] fix guest email strategy --- services/invitations/tests/unit/api/test_api_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 226b79363c2..95d5eeeece0 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -23,7 +23,7 @@ @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) -@given(invitation_input=st.builds(InvitationInputs)) +@given(invitation_input=st.builds(InvitationInputs, guest=st.emails())) def test_create_invitation( invitation_input: InvitationInputs, client: TestClient, From 3962f85fda692b577cc689a7b4b88be85e266589 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 21:49:23 +0200 Subject: [PATCH 12/15] fix import --- .../invitations/src/simcore_service_invitations/api/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/invitations/src/simcore_service_invitations/api/_meta.py b/services/invitations/src/simcore_service_invitations/api/_meta.py index 0b1e6a26fe5..49d3f4b48cf 100644 --- a/services/invitations/src/simcore_service_invitations/api/_meta.py +++ b/services/invitations/src/simcore_service_invitations/api/_meta.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel -from simcore_service_invitations.services.invitations import HttpUrlLegacy +from common_library.pydantic_networks_extension import HttpUrlLegacy from .._meta import API_VERSION, PROJECT_NAME from ._dependencies import get_reverse_url_mapper From 15349dd7d32e50e98ceb35d25fb023db990f8487 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 22:09:27 +0200 Subject: [PATCH 13/15] fix Url serialization --- .../common-library/src/common_library/serialization.py | 7 ++++++- services/invitations/tests/unit/test_cli.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/common-library/src/common_library/serialization.py b/packages/common-library/src/common_library/serialization.py index a2178cc3ae6..787e88123eb 100644 --- a/packages/common-library/src/common_library/serialization.py +++ b/packages/common-library/src/common_library/serialization.py @@ -3,6 +3,7 @@ from common_library.pydantic_fields_extension import get_type from pydantic import BaseModel, SecretStr +from pydantic_core import Url def model_dump_with_secrets( @@ -19,11 +20,15 @@ def model_dump_with_secrets( if isinstance(field_data, timedelta): data[field_name] = field_data.total_seconds() - if isinstance(field_data, SecretStr): + elif isinstance(field_data, SecretStr): if show_secrets: data[field_name] = field_data.get_secret_value() else: data[field_name] = str(field_data) + + elif isinstance(field_data, Url): + data[field_name] = str(field_data) + elif isinstance(field_data, dict): field_type = get_type(settings_obj.model_fields[field_name]) if issubclass(field_type, BaseModel): diff --git a/services/invitations/tests/unit/test_cli.py b/services/invitations/tests/unit/test_cli.py index c83623a2f8b..0ca095f1915 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -97,5 +97,5 @@ def test_list_settings(cli_runner: CliRunner, app_environment: EnvVarsDict): assert result.exit_code == os.EX_OK, result.output print(result.output) - settings = TypeAdapter(ApplicationSettings).validate_json(result.output) + settings = ApplicationSettings.model_validate_json(result.output) assert settings == ApplicationSettings.create_from_envs() From 2d79cb059d181a1cda082246f833d51d17ca0e39 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 10 Oct 2024 22:18:06 +0200 Subject: [PATCH 14/15] fix url --- services/invitations/tests/unit/test_invitations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/invitations/tests/unit/test_invitations.py b/services/invitations/tests/unit/test_invitations.py index a16dd743cc9..c90cb310c2e 100644 --- a/services/invitations/tests/unit/test_invitations.py +++ b/services/invitations/tests/unit/test_invitations.py @@ -72,8 +72,8 @@ def test_create_and_decrypt_invitation( base_url=faker.url(), default_product=default_product, ) - assert invitation_link.fragment - query_params = dict(parse.parse_qsl(URL(invitation_link.fragment).query)) + assert URL(invitation_link).fragment + query_params = dict(parse.parse_qsl((URL(URL(invitation_link).fragment).query))) # will raise TokenError or ValidationError invitation = decrypt_invitation( From 7ad1a1d1e5fe9b29714afb1145d6481e99d6299a Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Fri, 11 Oct 2024 11:36:20 +0200 Subject: [PATCH 15/15] add aliases uniqueness test --- services/invitations/tests/unit/test_invitations.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/invitations/tests/unit/test_invitations.py b/services/invitations/tests/unit/test_invitations.py index c90cb310c2e..0e6ea34b18c 100644 --- a/services/invitations/tests/unit/test_invitations.py +++ b/services/invitations/tests/unit/test_invitations.py @@ -5,6 +5,7 @@ import binascii from datetime import datetime, timezone +from typing import Counter from urllib import parse import cryptography.fernet @@ -192,3 +193,7 @@ class OtherModel(BaseModel): secret_key=secret, default_product=default_product, ) + + +def test_aliases_uniqueness(): + assert not [item for item, count in Counter([field.alias for field in _ContentWithShortNames.model_fields.values()]).items() if count > 1] #nosec