From b990f439c0419f8c46a26bac6d5e0229079c8786 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:51:16 +0100 Subject: [PATCH 01/10] updates db --- .../src/models_library/licensed_items.py | 22 ++++----- ...4f31760a63ba_add_data_to_licensed_items.py | 45 +++++++++++++++++++ .../models/licensed_items.py | 24 +++++++--- 3 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py diff --git a/packages/models-library/src/models_library/licensed_items.py b/packages/models-library/src/models_library/licensed_items.py index 79cd4fa87e01..155cb02332ab 100644 --- a/packages/models-library/src/models_library/licensed_items.py +++ b/packages/models-library/src/models_library/licensed_items.py @@ -1,9 +1,9 @@ from datetime import datetime from enum import auto -from typing import TypeAlias +from typing import Any, TypeAlias from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict from .products import ProductName from .resource_tracker import PricingPlanId @@ -24,22 +24,22 @@ class LicensedResourceType(StrAutoEnum): class LicensedItemDB(BaseModel): licensed_item_id: LicensedItemID name: str + license_key: str | None licensed_resource_type: LicensedResourceType + licensed_resource_data: dict[str, Any] | None + pricing_plan_id: PricingPlanId product_name: ProductName - created: datetime = Field( - ..., - description="Timestamp on creation", - ) - modified: datetime = Field( - ..., - description="Timestamp of last modification", - ) - # ---- + + created: datetime # Timestamp upon creation + modified: datetime # Timestamp on last modification + trashed: datetime | None # Marked as trashed + model_config = ConfigDict(from_attributes=True) class LicensedItemUpdateDB(BaseModel): name: str | None = None pricing_plan_id: PricingPlanId | None = None + trash: bool | None = None diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py new file mode 100644 index 000000000000..949d8a21b60a --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py @@ -0,0 +1,45 @@ +"""add data to licensed_items + +Revision ID: 4f31760a63ba +Revises: 1bc517536e0a +Create Date: 2025-01-29 16:51:16.453069+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "4f31760a63ba" +down_revision = "1bc517536e0a" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "licensed_items", + sa.Column( + "licensed_resource_data", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + ), + ) + op.add_column( + "licensed_items", + sa.Column( + "trashed", + sa.DateTime(timezone=True), + nullable=True, + comment="The date and time when the licensed_item was marked as trashed. Null if the licensed_item has not been trashed [default].", + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("licensed_items", "trashed") + op.drop_column("licensed_items", "licensed_resource_data") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py b/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py index a0ea136f4bba..c1d80b1a6c28 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py @@ -4,9 +4,14 @@ import enum import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects import postgresql -from ._common import RefActions, column_created_datetime, column_modified_datetime +from ._common import ( + RefActions, + column_created_datetime, + column_modified_datetime, + column_trashed_datetime, +) from .base import metadata @@ -19,7 +24,7 @@ class LicensedResourceType(str, enum.Enum): metadata, sa.Column( "licensed_item_id", - UUID(as_uuid=True), + postgresql.UUID(as_uuid=True), nullable=False, primary_key=True, server_default=sa.text("gen_random_uuid()"), @@ -28,6 +33,7 @@ class LicensedResourceType(str, enum.Enum): "name", sa.String, nullable=False, + doc="Item Name identifier", ), sa.Column( "licensed_resource_type", @@ -35,6 +41,12 @@ class LicensedResourceType(str, enum.Enum): nullable=False, doc="Item type, ex. VIP_MODEL", ), + sa.Column( + "licensed_resource_data", + postgresql.JSONB, + nullable=True, + doc="Stores metadata related to this licensed resource. Used for read-only purposes", + ), sa.Column( "pricing_plan_id", sa.BigInteger, @@ -56,14 +68,16 @@ class LicensedResourceType(str, enum.Enum): name="fk_resource_tracker_license_packages_product_name", ), nullable=False, - doc="Product name", + doc="Product name identifier. If None, then the item is not exposed", ), sa.Column( "license_key", sa.String, nullable=True, - doc="Purpose: Acts as a mapping key to the internal license server. Usage: The Sim4Life base applications use this key to check out a seat from the internal license server.", + doc="Purpose: Acts as a mapping key to the internal license server." + "Usage: The Sim4Life base applications use this key to check out a seat from the internal license server.", ), column_created_datetime(timezone=True), column_modified_datetime(timezone=True), + column_trashed_datetime("licensed_item"), ) From cd8544456832e14bfdb69cf2914c4843acfeb9d9 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:05:16 +0100 Subject: [PATCH 02/10] migrations --- ...4f31760a63ba_add_data_to_licensed_items.py | 51 +++++++++++++++++++ .../models/licensed_items.py | 12 ++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py index 949d8a21b60a..94acfc1df243 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/4f31760a63ba_add_data_to_licensed_items.py @@ -17,6 +17,25 @@ def upgrade(): + + with op.batch_alter_table("licensed_items") as batch_op: + batch_op.alter_column( + "name", + new_column_name="licensed_resource_name", + existing_type=sa.String(), + nullable=False, + ) + batch_op.alter_column( + "pricing_plan_id", + existing_type=sa.Integer(), + nullable=True, + ) + batch_op.alter_column( + "product_name", + existing_type=sa.String(), + nullable=True, + ) + # ### commands auto generated by Alembic - please adjust! ### op.add_column( "licensed_items", @@ -43,3 +62,35 @@ def downgrade(): op.drop_column("licensed_items", "trashed") op.drop_column("licensed_items", "licensed_resource_data") # ### end Alembic commands ### + + # Delete rows with null values in pricing_plan_id and product_name + op.execute( + sa.DDL( + """ + DELETE FROM licensed_items + WHERE pricing_plan_id IS NULL OR product_name IS NULL; + """ + ) + ) + print( + "Warning: Rows with null values in pricing_plan_id or product_name have been deleted." + ) + + with op.batch_alter_table("licensed_items") as batch_op: + + batch_op.alter_column( + "product_name", + existing_type=sa.String(), + nullable=False, + ) + batch_op.alter_column( + "pricing_plan_id", + existing_type=sa.Integer(), + nullable=False, + ) + batch_op.alter_column( + "licensed_resource_name", + new_column_name="name", + existing_type=sa.String(), + nullable=False, + ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py b/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py index c1d80b1a6c28..feddae5fdfd1 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/licensed_items.py @@ -30,22 +30,22 @@ class LicensedResourceType(str, enum.Enum): server_default=sa.text("gen_random_uuid()"), ), sa.Column( - "name", + "licensed_resource_name", sa.String, nullable=False, - doc="Item Name identifier", + doc="Resource name identifier", ), sa.Column( "licensed_resource_type", sa.Enum(LicensedResourceType), nullable=False, - doc="Item type, ex. VIP_MODEL", + doc="Resource type, ex. VIP_MODEL", ), sa.Column( "licensed_resource_data", postgresql.JSONB, nullable=True, - doc="Stores metadata related to this licensed resource. Used for read-only purposes", + doc="Resource metadata. Used for read-only purposes", ), sa.Column( "pricing_plan_id", @@ -56,7 +56,7 @@ class LicensedResourceType(str, enum.Enum): onupdate=RefActions.CASCADE, ondelete=RefActions.RESTRICT, ), - nullable=False, + nullable=True, ), sa.Column( "product_name", @@ -67,7 +67,7 @@ class LicensedResourceType(str, enum.Enum): ondelete=RefActions.CASCADE, name="fk_resource_tracker_license_packages_product_name", ), - nullable=False, + nullable=True, doc="Product name identifier. If None, then the item is not exposed", ), sa.Column( From a76f9bc630ea972111a9100b445bbcc3f68b8ac7 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:09:31 +0100 Subject: [PATCH 03/10] adapts repository --- .../src/models_library/licensed_items.py | 15 +-- .../licenses/_licensed_items_repository.py | 38 ++++---- .../test_licensed_items_repository.py | 94 ++++++++++++++++++- 3 files changed, 118 insertions(+), 29 deletions(-) diff --git a/packages/models-library/src/models_library/licensed_items.py b/packages/models-library/src/models_library/licensed_items.py index 155cb02332ab..2b561687f801 100644 --- a/packages/models-library/src/models_library/licensed_items.py +++ b/packages/models-library/src/models_library/licensed_items.py @@ -23,18 +23,19 @@ class LicensedResourceType(StrAutoEnum): class LicensedItemDB(BaseModel): licensed_item_id: LicensedItemID - name: str - license_key: str | None + + licensed_resource_name: str licensed_resource_type: LicensedResourceType licensed_resource_data: dict[str, Any] | None - pricing_plan_id: PricingPlanId - product_name: ProductName + pricing_plan_id: PricingPlanId | None + product_name: ProductName | None - created: datetime # Timestamp upon creation - modified: datetime # Timestamp on last modification - trashed: datetime | None # Marked as trashed + # states + created: datetime + modified: datetime + trashed: datetime | None model_config = ConfigDict(from_attributes=True) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py index 578616981619..4c9d41adb06b 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py @@ -5,7 +5,7 @@ """ import logging -from typing import cast +from typing import Literal, cast from aiohttp import web from models_library.licensed_items import ( @@ -20,6 +20,7 @@ from pydantic import NonNegativeInt from simcore_postgres_database.models.licensed_items import licensed_items from simcore_postgres_database.utils_repos import ( + get_columns_from_db_model, pass_or_acquire_connection, transaction_context, ) @@ -33,18 +34,7 @@ _logger = logging.getLogger(__name__) -_SELECTION_ARGS = ( - licensed_items.c.licensed_item_id, - licensed_items.c.name, - licensed_items.c.license_key, - licensed_items.c.licensed_resource_type, - licensed_items.c.pricing_plan_id, - licensed_items.c.product_name, - licensed_items.c.created, - licensed_items.c.modified, -) - -assert set(LicensedItemDB.model_fields) == {c.name for c in _SELECTION_ARGS} # nosec +_SELECTION_ARGS = get_columns_from_db_model(licensed_items, LicensedItemDB) async def create( @@ -57,7 +47,7 @@ async def create( pricing_plan_id: PricingPlanId, ) -> LicensedItemDB: async with transaction_context(get_asyncpg_engine(app), connection) as conn: - result = await conn.stream( + result = await conn.execute( licensed_items.insert() .values( name=name, @@ -69,7 +59,7 @@ async def create( ) .returning(*_SELECTION_ARGS) ) - row = await result.first() + row = result.one() return LicensedItemDB.model_validate(row) @@ -81,6 +71,7 @@ async def list_( offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, + filter_trashed: Literal["exclude", "only", "include"] = "exclude", ) -> tuple[int, list[LicensedItemDB]]: base_query = ( select(*_SELECTION_ARGS) @@ -88,6 +79,12 @@ async def list_( .where(licensed_items.c.product_name == product_name) ) + # Apply trashed filter + if filter_trashed == "exclude": + base_query = base_query.where(licensed_items.c.trashed.is_(None)) + elif filter_trashed == "only": + base_query = base_query.where(licensed_items.c.trashed.is_not(None)) + # Select total count from base_query subquery = base_query.subquery() count_query = select(func.count()).select_from(subquery) @@ -147,11 +144,16 @@ async def update( # NOTE: at least 'touch' if updated_values is empty _updates = { **updates.model_dump(exclude_unset=True), - "modified": func.now(), + licensed_items.c.modified.name: func.now(), } + # trashing + assert "trash" in dict(LicensedItemUpdateDB.model_fields) # nosec + if trash := _updates.pop("trash", None): + _updates[licensed_items.c.trashed.name] = func.now() if trash else None + async with transaction_context(get_asyncpg_engine(app), connection) as conn: - result = await conn.stream( + result = await conn.execute( licensed_items.update() .values(**_updates) .where( @@ -160,7 +162,7 @@ async def update( ) .returning(*_SELECTION_ARGS) ) - row = await result.first() + row = result.one_or_none() if row is None: raise LicensedItemNotFoundError(licensed_item_id=licensed_item_id) return LicensedItemDB.model_validate(row) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py index dfe04e2e0d3a..5720e2082fde 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py @@ -3,8 +3,8 @@ # pylint: disable=unused-variable # pylint: disable=too-many-arguments # pylint: disable=too-many-statements -from http import HTTPStatus +import arrow import pytest from aiohttp.test_utils import TestClient from models_library.licensed_items import ( @@ -14,20 +14,22 @@ ) from models_library.rest_ordering import OrderBy from pytest_simcore.helpers.webserver_login import UserInfoDict -from servicelib.aiohttp import status from simcore_service_webserver.db.models import UserRole from simcore_service_webserver.licenses import _licensed_items_repository from simcore_service_webserver.licenses.errors import LicensedItemNotFoundError from simcore_service_webserver.projects.models import ProjectDict -@pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) +@pytest.fixture +def user_role() -> UserRole: + return UserRole.USER + + async def test_licensed_items_db_crud( client: TestClient, logged_user: UserInfoDict, user_project: ProjectDict, osparc_product_name: str, - expected: HTTPStatus, pricing_plan_id: int, ): assert client.app @@ -92,3 +94,87 @@ async def test_licensed_items_db_crud( licensed_item_id=_licensed_item_id, product_name=osparc_product_name, ) + + +async def test_licensed_items_db_trash( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + osparc_product_name: str, + pricing_plan_id: int, +): + assert client.app + + # Create two licensed items + licensed_item_ids = [] + for name in ["Model A", "Model B"]: + licensed_item_db = await _licensed_items_repository.create( + client.app, + product_name=osparc_product_name, + name=name, + licensed_resource_type=LicensedResourceType.VIP_MODEL, + pricing_plan_id=pricing_plan_id, + ) + licensed_item_ids.append(licensed_item_db.licensed_item_id) + + licensed_item_id1, licensed_item_id2 = licensed_item_ids + + # Trash one licensed item + trashing_at = arrow.now().datetime + trashed_item = await _licensed_items_repository.update( + client.app, + licensed_item_id=licensed_item_id1, + product_name=osparc_product_name, + updates=LicensedItemUpdateDB(trash=True), + ) + + assert trashed_item.licensed_item_id == licensed_item_id1 + assert trashed_item.trashed + assert trashing_at < trashed_item.trashed + assert trashed_item.trashed < arrow.now().datetime + + # List with filter_trashed include + total_count, items = await _licensed_items_repository.list_( + client.app, + product_name=osparc_product_name, + offset=0, + limit=10, + order_by=OrderBy(field="modified"), + filter_trashed="include", + ) + assert total_count == 2 + assert {i.licensed_item_id for i in items} == set(licensed_item_ids) + + # List with filter_trashed exclude + total_count, items = await _licensed_items_repository.list_( + client.app, + product_name=osparc_product_name, + offset=0, + limit=10, + order_by=OrderBy(field="modified"), + filter_trashed="exclude", + ) + assert total_count == 1 + assert items[0].licensed_item_id == licensed_item_id2 + assert items[0].trashed is None + + # List with filter_trashed all + total_count, items = await _licensed_items_repository.list_( + client.app, + product_name=osparc_product_name, + offset=0, + limit=10, + order_by=OrderBy(field="modified"), + filter_trashed="only", + ) + assert total_count == 1 + assert items[0].licensed_item_id == trashed_item.licensed_item_id + assert items[0].trashed + + # Get the trashed licensed item + got = await _licensed_items_repository.get( + client.app, + licensed_item_id=trashed_item.licensed_item_id, + product_name=osparc_product_name, + ) + assert got == trashed_item From 775f36164b16ccbfd60109d5851e5bb1322443fe Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:46:21 +0100 Subject: [PATCH 04/10] adapts repository --- .../api_schemas_webserver/licensed_items.py | 22 +++++++++++-- .../licenses/_licensed_items_repository.py | 19 +++++++++-- .../licenses/_licensed_items_service.py | 32 +++++++------------ .../test_licensed_items_repository.py | 10 +++--- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py index 5dafd9d5804f..9eebb991a74a 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py @@ -1,7 +1,12 @@ from datetime import datetime -from typing import NamedTuple +from typing import NamedTuple, Self -from models_library.licensed_items import LicensedItemID, LicensedResourceType +from common_library.dict_tools import remap_keys +from models_library.licensed_items import ( + LicensedItemDB, + LicensedItemID, + LicensedResourceType, +) from models_library.resource_tracker import PricingPlanId from pydantic import ConfigDict, PositiveInt @@ -32,6 +37,19 @@ class LicensedItemGet(OutputSchema): } ) + @classmethod + def from_domain_model(cls, licensed_item_db: LicensedItemDB) -> Self: + return cls.model_validate( + remap_keys( + licensed_item_db.model_dump(), + { + "licensed_resource_name": "name", + "created": "created_at", + "modified": "modified_at", + }, + ) + ) + class LicensedItemGetPage(NamedTuple): items: list[LicensedItemGet] diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py index 4c9d41adb06b..e79b7c6fc4b7 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py @@ -71,8 +71,10 @@ async def list_( offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, - filter_trashed: Literal["exclude", "only", "include"] = "exclude", + trashed: Literal["exclude", "only", "include"] = "exclude", + inactive: Literal["exclude", "only", "include"] = "exclude", ) -> tuple[int, list[LicensedItemDB]]: + base_query = ( select(*_SELECTION_ARGS) .select_from(licensed_items) @@ -80,11 +82,22 @@ async def list_( ) # Apply trashed filter - if filter_trashed == "exclude": + if trashed == "exclude": base_query = base_query.where(licensed_items.c.trashed.is_(None)) - elif filter_trashed == "only": + elif trashed == "only": base_query = base_query.where(licensed_items.c.trashed.is_not(None)) + if inactive == "exclude": + base_query = base_query.where( + licensed_items.c.product_name.is_(None) + or licensed_items.c.licensed_item_id.is_(None) + ) + elif inactive == "only": + base_query = base_query.where( + licensed_items.c.product_name.is_not(None) + and licensed_items.c.licensed_item_id.is_not(None) + ) + # Select total count from base_query subquery = base_query.subquery() count_query = select(func.count()).select_from(subquery) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_service.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_service.py index bf48a89ca5c5..ec748259385d 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_service.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_service.py @@ -42,15 +42,7 @@ async def get_licensed_item( licensed_item_db = await _licensed_items_repository.get( app, licensed_item_id=licensed_item_id, product_name=product_name ) - return LicensedItemGet( - licensed_item_id=licensed_item_db.licensed_item_id, - name=licensed_item_db.name, - license_key=licensed_item_db.license_key, - licensed_resource_type=licensed_item_db.licensed_resource_type, - pricing_plan_id=licensed_item_db.pricing_plan_id, - created_at=licensed_item_db.created, - modified_at=licensed_item_db.modified, - ) + return LicensedItemGet.from_domain_model(licensed_item_db) async def list_licensed_items( @@ -61,21 +53,19 @@ async def list_licensed_items( limit: int, order_by: OrderBy, ) -> LicensedItemGetPage: - total_count, licensed_item_db_list = await _licensed_items_repository.list_( - app, product_name=product_name, offset=offset, limit=limit, order_by=order_by + total_count, items = await _licensed_items_repository.list_( + app, + product_name=product_name, + offset=offset, + limit=limit, + order_by=order_by, + trashed="exclude", + inactive="exclude", ) return LicensedItemGetPage( items=[ - LicensedItemGet( - licensed_item_id=licensed_item_db.licensed_item_id, - name=licensed_item_db.name, - license_key=licensed_item_db.license_key, - licensed_resource_type=licensed_item_db.licensed_resource_type, - pricing_plan_id=licensed_item_db.pricing_plan_id, - created_at=licensed_item_db.created, - modified_at=licensed_item_db.modified, - ) - for licensed_item_db in licensed_item_db_list + LicensedItemGet.from_domain_model(licensed_item_db) + for licensed_item_db in items ], total=total_count, ) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py index 5720e2082fde..66ead781c86e 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py @@ -66,7 +66,7 @@ async def test_licensed_items_db_crud( licensed_item_id=_licensed_item_id, product_name=osparc_product_name, ) - assert licensed_item_db.name == "Model A" + assert licensed_item_db.licensed_resource_name == "Model A" await _licensed_items_repository.update( client.app, @@ -80,7 +80,7 @@ async def test_licensed_items_db_crud( licensed_item_id=_licensed_item_id, product_name=osparc_product_name, ) - assert licensed_item_db.name == "Model B" + assert licensed_item_db.licensed_resource_name == "Model B" licensed_item_db = await _licensed_items_repository.delete( client.app, @@ -140,7 +140,7 @@ async def test_licensed_items_db_trash( offset=0, limit=10, order_by=OrderBy(field="modified"), - filter_trashed="include", + trashed="include", ) assert total_count == 2 assert {i.licensed_item_id for i in items} == set(licensed_item_ids) @@ -152,7 +152,7 @@ async def test_licensed_items_db_trash( offset=0, limit=10, order_by=OrderBy(field="modified"), - filter_trashed="exclude", + trashed="exclude", ) assert total_count == 1 assert items[0].licensed_item_id == licensed_item_id2 @@ -165,7 +165,7 @@ async def test_licensed_items_db_trash( offset=0, limit=10, order_by=OrderBy(field="modified"), - filter_trashed="only", + trashed="only", ) assert total_count == 1 assert items[0].licensed_item_id == trashed_item.licensed_item_id From 8a0c0a1a49e2a627754115ba248fa20dbd9ca06c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:49:36 +0100 Subject: [PATCH 05/10] fixes tests --- .../api_schemas_webserver/licensed_items.py | 11 ++++++++++- .../licenses/_licensed_items_repository.py | 12 ++++++------ .../04/licenses/test_licensed_items_repository.py | 4 ++-- .../with_dbs/04/licenses/test_licensed_items_rest.py | 4 ++-- .../unit/with_dbs/04/licenses/test_licenses_rpc.py | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py index 9eebb991a74a..d1335f03d4aa 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py @@ -41,7 +41,16 @@ class LicensedItemGet(OutputSchema): def from_domain_model(cls, licensed_item_db: LicensedItemDB) -> Self: return cls.model_validate( remap_keys( - licensed_item_db.model_dump(), + licensed_item_db.model_dump( + include={ + "licensed_item_id", + "licensed_resource_name", + "license_key", + "pricing_plan_id", + "created", + "modified", + } + ), { "licensed_resource_name": "name", "created": "created_at", diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py index e79b7c6fc4b7..9289ededac92 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py @@ -41,16 +41,16 @@ async def create( app: web.Application, connection: AsyncConnection | None = None, *, - product_name: ProductName, - name: str, + licensed_resource_name: str, licensed_resource_type: LicensedResourceType, - pricing_plan_id: PricingPlanId, + product_name: ProductName | None, + pricing_plan_id: PricingPlanId | None, ) -> LicensedItemDB: async with transaction_context(get_asyncpg_engine(app), connection) as conn: result = await conn.execute( licensed_items.insert() .values( - name=name, + licensed_resource_name=licensed_resource_name, licensed_resource_type=licensed_resource_type, pricing_plan_id=pricing_plan_id, product_name=product_name, @@ -90,12 +90,12 @@ async def list_( if inactive == "exclude": base_query = base_query.where( licensed_items.c.product_name.is_(None) - or licensed_items.c.licensed_item_id.is_(None) + | licensed_items.c.licensed_item_id.is_(None) ) elif inactive == "only": base_query = base_query.where( licensed_items.c.product_name.is_not(None) - and licensed_items.c.licensed_item_id.is_not(None) + & licensed_items.c.licensed_item_id.is_not(None) ) # Select total count from base_query diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py index 66ead781c86e..98b7f269bf8a 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py @@ -46,7 +46,7 @@ async def test_licensed_items_db_crud( licensed_item_db = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, - name="Model A", + licensed_resource_name="Model A", licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) @@ -111,7 +111,7 @@ async def test_licensed_items_db_trash( licensed_item_db = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, - name=name, + licensed_resource_name=name, licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_rest.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_rest.py index 67c36f2581ba..491e340bd2f0 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_rest.py @@ -42,7 +42,7 @@ async def test_licensed_items_listing( licensed_item_db = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, - name="Model A", + licensed_resource_name="Model A", licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) @@ -109,7 +109,7 @@ async def test_licensed_items_purchase( licensed_item_db = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, - name="Model A", + licensed_resource_name="Model A", licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py index 836a7fe05e6f..acc32cc43256 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py @@ -135,7 +135,7 @@ async def test_license_checkout_workflow( license_item_db = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, - name="Model A", + licensed_resource_name="Model A", licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) From 93fac3065e11ed86e92d4f13e8d1b3d6b5e4c7f0 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:27:26 +0100 Subject: [PATCH 06/10] fixes name --- .../web/server/src/simcore_service_webserver/licenses/_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_rpc.py b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py index 85cc3a99642d..f63c4e916cfe 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_rpc.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py @@ -43,7 +43,7 @@ async def get_licensed_items( product_name=product_name, offset=offset, limit=limit, - order_by=OrderBy(field=IDStr("name")), + order_by=OrderBy(field=IDStr("licensed_resource_name")), ) ) return licensed_item_get_page From fc51ade5a1f83b793b23b2b60eed7c7c06219fcc Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:36:27 +0100 Subject: [PATCH 07/10] fixes tests --- .../src/models_library/licensed_items.py | 3 +- .../licenses/_licensed_items_repository.py | 5 ++- .../test_licensed_items_repository.py | 43 +++++++++---------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/models-library/src/models_library/licensed_items.py b/packages/models-library/src/models_library/licensed_items.py index 2b561687f801..53220d69c8f5 100644 --- a/packages/models-library/src/models_library/licensed_items.py +++ b/packages/models-library/src/models_library/licensed_items.py @@ -41,6 +41,7 @@ class LicensedItemDB(BaseModel): class LicensedItemUpdateDB(BaseModel): - name: str | None = None + licensed_resource_name: str | None = None pricing_plan_id: PricingPlanId | None = None + trash: bool | None = None diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py index 9289ededac92..45e67629a099 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py @@ -71,6 +71,7 @@ async def list_( offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, + # filters trashed: Literal["exclude", "only", "include"] = "exclude", inactive: Literal["exclude", "only", "include"] = "exclude", ) -> tuple[int, list[LicensedItemDB]]: @@ -87,12 +88,12 @@ async def list_( elif trashed == "only": base_query = base_query.where(licensed_items.c.trashed.is_not(None)) - if inactive == "exclude": + if inactive == "only": base_query = base_query.where( licensed_items.c.product_name.is_(None) | licensed_items.c.licensed_item_id.is_(None) ) - elif inactive == "only": + elif inactive == "exclude": base_query = base_query.where( licensed_items.c.product_name.is_not(None) & licensed_items.c.licensed_item_id.is_not(None) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py index 98b7f269bf8a..222637164a72 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py @@ -7,11 +7,7 @@ import arrow import pytest from aiohttp.test_utils import TestClient -from models_library.licensed_items import ( - LicensedItemDB, - LicensedItemUpdateDB, - LicensedResourceType, -) +from models_library.licensed_items import LicensedItemUpdateDB, LicensedResourceType from models_library.rest_ordering import OrderBy from pytest_simcore.helpers.webserver_login import UserInfoDict from simcore_service_webserver.db.models import UserRole @@ -33,65 +29,66 @@ async def test_licensed_items_db_crud( pricing_plan_id: int, ): assert client.app - - output: tuple[int, list[LicensedItemDB]] = await _licensed_items_repository.list_( + total_count, items = await _licensed_items_repository.list_( client.app, product_name=osparc_product_name, offset=0, limit=10, order_by=OrderBy(field="modified"), ) - assert output[0] == 0 + assert total_count == 0 + assert not items - licensed_item_db = await _licensed_items_repository.create( + got = await _licensed_items_repository.create( client.app, product_name=osparc_product_name, licensed_resource_name="Model A", licensed_resource_type=LicensedResourceType.VIP_MODEL, pricing_plan_id=pricing_plan_id, ) - _licensed_item_id = licensed_item_db.licensed_item_id + licensed_item_id = got.licensed_item_id - output: tuple[int, list[LicensedItemDB]] = await _licensed_items_repository.list_( + total_count, items = await _licensed_items_repository.list_( client.app, product_name=osparc_product_name, offset=0, limit=10, order_by=OrderBy(field="modified"), ) - assert output[0] == 1 + assert total_count == 1 + assert items[0].licensed_item_id == licensed_item_id - licensed_item_db = await _licensed_items_repository.get( + got = await _licensed_items_repository.get( client.app, - licensed_item_id=_licensed_item_id, + licensed_item_id=licensed_item_id, product_name=osparc_product_name, ) - assert licensed_item_db.licensed_resource_name == "Model A" + assert got.licensed_resource_name == "Model A" await _licensed_items_repository.update( client.app, - licensed_item_id=_licensed_item_id, + licensed_item_id=licensed_item_id, product_name=osparc_product_name, - updates=LicensedItemUpdateDB(name="Model B"), + updates=LicensedItemUpdateDB(licensed_resource_name="Model B"), ) - licensed_item_db = await _licensed_items_repository.get( + got = await _licensed_items_repository.get( client.app, - licensed_item_id=_licensed_item_id, + licensed_item_id=licensed_item_id, product_name=osparc_product_name, ) - assert licensed_item_db.licensed_resource_name == "Model B" + assert got.licensed_resource_name == "Model B" - licensed_item_db = await _licensed_items_repository.delete( + got = await _licensed_items_repository.delete( client.app, - licensed_item_id=_licensed_item_id, + licensed_item_id=licensed_item_id, product_name=osparc_product_name, ) with pytest.raises(LicensedItemNotFoundError): await _licensed_items_repository.get( client.app, - licensed_item_id=_licensed_item_id, + licensed_item_id=licensed_item_id, product_name=osparc_product_name, ) From 9ffa4c66eec3d67a3f712f105629a4213005b2f2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:15:12 +0100 Subject: [PATCH 08/10] fixes pylint --- .../src/simcore_service_webserver/application_settings.py | 2 +- .../application_settings_utils.py | 2 +- .../web/server/tests/unit/isolated/test_users_models.py | 2 +- .../04/licenses/test_licensed_items_repository.py | 8 +++----- .../unit/with_dbs/04/wallets/payments/test_payments.py | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/application_settings.py b/services/web/server/src/simcore_service_webserver/application_settings.py index 2e52bba1a07b..fcf8080123cf 100644 --- a/services/web/server/src/simcore_service_webserver/application_settings.py +++ b/services/web/server/src/simcore_service_webserver/application_settings.py @@ -411,7 +411,7 @@ def _enable_only_if_dev_features_allowed(cls, v, info: ValidationInfo): return ( None - if info.field_name and is_nullable(cls.model_fields[info.field_name]) + if info.field_name and is_nullable(dict(cls.model_fields)[info.field_name]) else False ) diff --git a/services/web/server/src/simcore_service_webserver/application_settings_utils.py b/services/web/server/src/simcore_service_webserver/application_settings_utils.py index d5180c071926..ca4e27143f2d 100644 --- a/services/web/server/src/simcore_service_webserver/application_settings_utils.py +++ b/services/web/server/src/simcore_service_webserver/application_settings_utils.py @@ -202,7 +202,7 @@ def convert_to_environ_vars( # noqa: C901, PLR0915, PLR0912 def _set_if_disabled(field_name, section): # Assumes that by default is enabled enabled = section.get("enabled", True) - field = ApplicationSettings.model_fields[field_name] + field = dict(ApplicationSettings.model_fields)[field_name] if not enabled: envs[field_name] = "null" if is_nullable(field) else "0" elif get_type(field) == bool: diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index c7cfeba336e3..e568d0d2ddde 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -89,7 +89,7 @@ def test_auto_compute_gravatar__deprecated(fake_profile_get: MyProfileGet): assert ( "gravatar_id" not in data - ), f"{MyProfileGet.model_fields['gravatar_id'].deprecated=}" + ), f"{dict(MyProfileGet.model_fields)['gravatar_id'].deprecated=}" assert data["id"] == profile.id assert data["first_name"] == profile.first_name assert data["last_name"] == profile.last_name diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py index 222637164a72..7e3a0f4018e3 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_repository.py @@ -114,18 +114,16 @@ async def test_licensed_items_db_trash( ) licensed_item_ids.append(licensed_item_db.licensed_item_id) - licensed_item_id1, licensed_item_id2 = licensed_item_ids - # Trash one licensed item trashing_at = arrow.now().datetime trashed_item = await _licensed_items_repository.update( client.app, - licensed_item_id=licensed_item_id1, + licensed_item_id=licensed_item_ids[0], product_name=osparc_product_name, updates=LicensedItemUpdateDB(trash=True), ) - assert trashed_item.licensed_item_id == licensed_item_id1 + assert trashed_item.licensed_item_id == licensed_item_ids[0] assert trashed_item.trashed assert trashing_at < trashed_item.trashed assert trashed_item.trashed < arrow.now().datetime @@ -152,7 +150,7 @@ async def test_licensed_items_db_trash( trashed="exclude", ) assert total_count == 1 - assert items[0].licensed_item_id == licensed_item_id2 + assert items[0].licensed_item_id == licensed_item_ids[1] assert items[0].trashed is None # List with filter_trashed all diff --git a/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py b/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py index 719eb7e6dc89..4b028a61dd81 100644 --- a/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py +++ b/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py @@ -362,7 +362,7 @@ async def test_payment_not_found( def test_payment_transaction_state_and_literals_are_in_sync(): - state_literals = PaymentTransaction.model_fields["state"].annotation + state_literals = dict(PaymentTransaction.model_fields)["state"].annotation assert ( TypeAdapter(list[state_literals]).validate_python( [f"{s}" for s in PaymentTransactionState] From a42054326cbdd94ae52a0e18e155bd7a6d137b5f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:19:14 +0100 Subject: [PATCH 09/10] fixes tests --- .../models_library/api_schemas_webserver/licensed_items.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py index d1335f03d4aa..0e2ff0186d21 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py @@ -15,12 +15,16 @@ class LicensedItemGet(OutputSchema): licensed_item_id: LicensedItemID + name: str license_key: str | None licensed_resource_type: LicensedResourceType + pricing_plan_id: PricingPlanId + created_at: datetime modified_at: datetime + model_config = ConfigDict( json_schema_extra={ "examples": [ @@ -45,7 +49,7 @@ def from_domain_model(cls, licensed_item_db: LicensedItemDB) -> Self: include={ "licensed_item_id", "licensed_resource_name", - "license_key", + "licensed_resource_type" "license_key", "pricing_plan_id", "created", "modified", From 1771b1ce24893db713b577878265b2052748939e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:20:49 +0100 Subject: [PATCH 10/10] fixes tests --- .../src/models_library/api_schemas_webserver/licensed_items.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py index 0e2ff0186d21..aa20fc113f83 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py @@ -49,7 +49,8 @@ def from_domain_model(cls, licensed_item_db: LicensedItemDB) -> Self: include={ "licensed_item_id", "licensed_resource_name", - "licensed_resource_type" "license_key", + "licensed_resource_type", + "license_key", "pricing_plan_id", "created", "modified",