From 829eae9e3e4d1b6a49c4e5554a48e7c37dcf4c69 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 9 Dec 2024 16:42:39 +0100 Subject: [PATCH 01/51] db layer licensed items purchase --- .env-devel | 2 +- ...source_tracker_licensed_items_purchases.py | 4 + ...source_tracker_licensed_items_purchases.py | 18 ++- .../exceptions/errors.py | 7 + .../models/licensed_items_purchases.py | 45 ++++++ .../modules/db/licensed_items_purchases.py | 140 ++++++++++++++++++ .../tests/unit/isolated/test_tracing.py | 6 +- 7 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py create mode 100644 services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py create mode 100644 services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py diff --git a/.env-devel b/.env-devel index cc6609460da..1683e998509 100644 --- a/.env-devel +++ b/.env-devel @@ -389,6 +389,6 @@ WEBSERVER_SOCKETIO=1 WEBSERVER_STATICWEB={} WEBSERVER_STUDIES_DISPATCHER={} WEBSERVER_TAGS=1 -WEBSERVER_TRACING=null +WEBSERVER_TRACING={} WEBSERVER_USERS={} WEBSERVER_VERSION_CONTROL=1 diff --git a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py new file mode 100644 index 00000000000..e5394b019d4 --- /dev/null +++ b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py @@ -0,0 +1,4 @@ +from typing import TypeAlias +from uuid import UUID + +LicensedItemPurchaseID: TypeAlias = UUID diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py index 2a13e3d718e..43cf052eb7b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py @@ -5,7 +5,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import UUID -from ._common import column_modified_datetime +from ._common import NUMERIC_KWARGS, column_modified_datetime from .base import metadata resource_tracker_licensed_items_purchases = sa.Table( @@ -34,6 +34,22 @@ sa.BigInteger, nullable=False, ), + sa.Column( + "wallet_name", + sa.String, + nullable=False, + ), + sa.Column( + "pricing_unit_cost_id", + sa.BigInteger, + nullable=False, + ), + sa.Column( + "pricing_unit_cost", + sa.Numeric(**NUMERIC_KWARGS), # type: ignore + nullable=True, + doc="Pricing unit cost used for billing purposes", + ), sa.Column( "start_at", sa.DateTime(timezone=True), diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/exceptions/errors.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/exceptions/errors.py index fe620d99c62..55fde04b0f6 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/exceptions/errors.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/exceptions/errors.py @@ -1,4 +1,7 @@ from common_library.errors_classes import OsparcErrorMixin +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) class ResourceUsageTrackerBaseError(OsparcErrorMixin, Exception): @@ -68,3 +71,7 @@ class PricingPlanNotFoundForServiceError(RutNotFoundError): msg_template = ( "Pricing plan not found for service key {service_key} version {service_version}" ) + + +class LicensedItemPurchaseNotFoundError(RutNotFoundError): + licensed_item_purchase_id: LicensedItemPurchaseID diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py new file mode 100644 index 00000000000..f42690ef347 --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py @@ -0,0 +1,45 @@ +from datetime import datetime +from decimal import Decimal + +from models_library.licensed_items import LicensedItemID +from models_library.products import ProductName +from models_library.resource_tracker import PricingUnitCostId +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) +from models_library.users import UserID +from models_library.wallets import WalletID +from pydantic import BaseModel, ConfigDict + + +class LicensedItemsPurchasesDB(BaseModel): + licensed_item_purchase_id: LicensedItemPurchaseID + product_name: ProductName + licensed_item_id: LicensedItemID + wallet_id: WalletID | None + wallet_name: str | None + pricing_unit_cost_id: PricingUnitCostId + pricing_unit_cost: Decimal + start_at: datetime + expire_at: datetime | None + purchased_by_user: UserID + purchased_at: datetime + modified: datetime + + model_config = ConfigDict(from_attributes=True) + + +class CreateLicensedItemsPurchasesDB(BaseModel): + product_name: ProductName + licensed_item_id: LicensedItemID + wallet_id: WalletID | None + wallet_name: str | None + pricing_unit_cost_id: PricingUnitCostId + pricing_unit_cost: Decimal + start_at: datetime + expire_at: datetime | None + purchased_by_user: UserID + purchased_at: datetime + modified: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py new file mode 100644 index 00000000000..b84f7311fe0 --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py @@ -0,0 +1,140 @@ +from typing import cast + +import sqlalchemy as sa +from models_library.products import ProductName +from models_library.rest_ordering import OrderBy, OrderDirection +from pydantic import NonNegativeInt +from simcore_postgres_database.models.resource_tracker_licensed_items_purchases import ( + resource_tracker_licensed_items_purchases, +) +from simcore_postgres_database.utils_repos import ( + pass_or_acquire_connection, + transaction_context, +) +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine + +from ....exceptions.errors import LicensedItemPurchaseNotFoundError +from ....models.licensed_items_purchases import ( + CreateLicensedItemsPurchasesDB, + LicensedItemPurchaseID, + LicensedItemsPurchasesDB, +) + +_SELECTION_ARGS = ( + resource_tracker_licensed_items_purchases.c.licensed_item_purchase_id, + resource_tracker_licensed_items_purchases.c.product_name, + resource_tracker_licensed_items_purchases.c.licensed_item_id, + resource_tracker_licensed_items_purchases.c.wallet_id, + resource_tracker_licensed_items_purchases.c.wallet_name, + resource_tracker_licensed_items_purchases.c.pricing_unit_cost_id, + resource_tracker_licensed_items_purchases.c.pricing_unit_cost, + resource_tracker_licensed_items_purchases.c.start_at, + resource_tracker_licensed_items_purchases.c.expire_at, + resource_tracker_licensed_items_purchases.c.purchased_by_user, + resource_tracker_licensed_items_purchases.c.purchased_at, + resource_tracker_licensed_items_purchases.c.modified, +) + +assert set(LicensedItemsPurchasesDB.model_fields) == { + c.name for c in _SELECTION_ARGS +} # nosec + + +async def create( + engine: AsyncEngine, + connection: AsyncConnection | None = None, + *, + data: CreateLicensedItemsPurchasesDB, +) -> LicensedItemsPurchasesDB: + async with transaction_context(engine, connection) as conn: + result = await conn.stream( + resource_tracker_licensed_items_purchases.insert() + .values( + product_name=data.product_name, + licensed_item_id=data.licensed_item_id, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_unit_cost_id=data.pricing_unit_cost_id, + pricing_unit_cost=data.pricing_unit_cost, + start_at=data.start_at, + expire_at=data.expire_at, + purchased_by_user=data.purchased_by_user, + purchased_at=data.purchased_at, + modified=sa.func.now(), + ) + .returning(*_SELECTION_ARGS) + ) + row = await result.first() + return LicensedItemsPurchasesDB.model_validate(row) + + +async def list_( + engine: AsyncEngine, + connection: AsyncConnection | None = None, + *, + product_name: ProductName, + offset: NonNegativeInt, + limit: NonNegativeInt, + order_by: OrderBy, +) -> tuple[int, list[LicensedItemsPurchasesDB]]: + base_query = ( + sa.select(*_SELECTION_ARGS) + .select_from(resource_tracker_licensed_items_purchases) + .where(resource_tracker_licensed_items_purchases.c.product_name == product_name) + ) + + # Select total count from base_query + subquery = base_query.subquery() + count_query = sa.select(sa.func.count()).select_from(subquery) + + # Ordering and pagination + if order_by.direction == OrderDirection.ASC: + list_query = base_query.order_by( + sa.asc(getattr(resource_tracker_licensed_items_purchases.c, order_by.field)) + ) + else: + list_query = base_query.order_by( + sa.desc( + getattr(resource_tracker_licensed_items_purchases.c, order_by.field) + ) + ) + list_query = list_query.offset(offset).limit(limit) + + async with pass_or_acquire_connection(engine, connection) as conn: + total_count = await conn.scalar(count_query) + + result = await conn.stream(list_query) + items: list[LicensedItemsPurchasesDB] = [ + LicensedItemsPurchasesDB.model_validate(row) async for row in result + ] + + return cast(int, total_count), items + + +async def get( + engine: AsyncEngine, + connection: AsyncConnection | None = None, + *, + licensed_item_purchase_id: LicensedItemPurchaseID, + product_name: ProductName, +) -> LicensedItemsPurchasesDB: + base_query = ( + sa.select(*_SELECTION_ARGS) + .select_from(resource_tracker_licensed_items_purchases) + .where( + ( + resource_tracker_licensed_items_purchases.c.licensed_item_purchase_id + == licensed_item_purchase_id + ) + & (resource_tracker_licensed_items_purchases.c.product_name == product_name) + ) + ) + + async with pass_or_acquire_connection(engine, connection) as conn: + result = await conn.stream(base_query) + row = await result.first() + if row is None: + raise LicensedItemPurchaseNotFoundError( + licensed_item_purchase_id=licensed_item_purchase_id + ) + return LicensedItemsPurchasesDB.model_validate(row) diff --git a/services/web/server/tests/unit/isolated/test_tracing.py b/services/web/server/tests/unit/isolated/test_tracing.py index ddec0d10422..bac02e74a8c 100644 --- a/services/web/server/tests/unit/isolated/test_tracing.py +++ b/services/web/server/tests/unit/isolated/test_tracing.py @@ -18,7 +18,7 @@ def mock_webserver_service_environment( monkeypatch: pytest.MonkeyPatch, mock_webserver_service_environment: EnvVarsDict ) -> EnvVarsDict: - return mock_webserver_service_environment | setenvs_from_dict( + envs = mock_webserver_service_environment | setenvs_from_dict( monkeypatch, { "TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT": "http://opentelemetry-collector", @@ -26,6 +26,10 @@ def mock_webserver_service_environment( }, ) + envs.pop("WEBSERVER_TRACING") + + return envs + def test_middleware_restrictions_opentelemetry_is_second_middleware( mock_webserver_service_environment: EnvVarsDict, From a975a63369800b40d5303878f91136f8d4c1e6b6 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 08:32:58 +0100 Subject: [PATCH 02/51] rut part --- .env-devel | 2 +- .../licensed_items_purchases.py | 55 +++++++ ...source_tracker_licensed_items_purchases.py | 26 ++++ ...7_add_cols_to_licensed_items_purchases_.py | 45 ++++++ ...source_tracker_licensed_items_purchases.py | 5 + .../api/rpc/_licensed_items.py | 60 ++++++++ .../api/rpc/routes.py | 3 +- .../models/licensed_items_purchases.py | 15 +- .../services/licensed_items_purchases.py | 134 ++++++++++++++++++ ...ases.py => licensed_items_purchases_db.py} | 12 +- .../catalog/licenses/_models.py | 2 +- 11 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py create mode 100644 services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py create mode 100644 services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py rename services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/{licensed_items_purchases.py => licensed_items_purchases_db.py} (92%) diff --git a/.env-devel b/.env-devel index 1683e998509..cc6609460da 100644 --- a/.env-devel +++ b/.env-devel @@ -389,6 +389,6 @@ WEBSERVER_SOCKETIO=1 WEBSERVER_STATICWEB={} WEBSERVER_STUDIES_DISPATCHER={} WEBSERVER_TAGS=1 -WEBSERVER_TRACING={} +WEBSERVER_TRACING=null WEBSERVER_USERS={} WEBSERVER_VERSION_CONTROL=1 diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py new file mode 100644 index 00000000000..69c3c64c440 --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py @@ -0,0 +1,55 @@ +from datetime import datetime +from decimal import Decimal + +from models_library.licensed_items import LicensedItemID +from models_library.products import ProductName +from models_library.resource_tracker import PricingUnitCostId +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) +from models_library.users import UserID +from models_library.wallets import WalletID +from pydantic import BaseModel, ConfigDict, PositiveInt + + +class LicensedItemPurchaseGet(BaseModel): + licensed_item_purchase_id: LicensedItemPurchaseID + product_name: ProductName + licensed_item_id: LicensedItemID + wallet_id: WalletID | None + wallet_name: str | None + pricing_unit_cost_id: PricingUnitCostId + pricing_unit_cost: Decimal + start_at: datetime + expire_at: datetime + num_of_seats: int + purchased_by_user: UserID + purchased_at: datetime + modified: datetime + + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + "licensed_item_purchase_id": 1, + "product_name": "osparc", + "licensed_item_id": "Special Pricing Plan for Sleeper", + "wallet_id": 1, + "wallet_name": "My Wallet", + "pricing_unit_cost_id": 1, + "pricing_unit_cost": Decimal(10), + "start_at": "2023-01-11 13:11:47.293595", + "expire_at": "2023-01-11 13:11:47.293595", + "num_of_seats": 1, + "purchased_by_user": 1, + "purchased_at": "2023-01-11 13:11:47.293595", + "modified": "2023-01-11 13:11:47.293595", + } # type: ignore[index,union-attr] + ] + } + ) + + +class LicensedItemsPurchasesPage(NamedTuple): + items: list[LicensedItemPurchaseGet] + total: PositiveInt diff --git a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py index e5394b019d4..d1ab2d88dc8 100644 --- a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py +++ b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py @@ -1,4 +1,30 @@ +from datetime import datetime +from decimal import Decimal from typing import TypeAlias from uuid import UUID +from pydantic import BaseModel, ConfigDict + +from .licensed_items import LicensedItemID +from .products import ProductName +from .resource_tracker import PricingUnitCostId +from .users import UserID +from .wallets import WalletID + LicensedItemPurchaseID: TypeAlias = UUID + + +class LicensedItemsPurchasesCreate(BaseModel): + product_name: ProductName + licensed_item_id: LicensedItemID + wallet_id: WalletID + wallet_name: str + pricing_unit_cost_id: PricingUnitCostId + pricing_unit_cost: Decimal + start_at: datetime + expire_at: datetime + num_of_seats: int + purchased_by_user: UserID + purchased_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py new file mode 100644 index 00000000000..39f2ba32ea3 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py @@ -0,0 +1,45 @@ +"""add cols to licensed_items_purchases table + +Revision ID: 8fa15c4c3977 +Revises: 38c9ac332c58 +Create Date: 2024-12-10 06:42:23.319239+00:00 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8fa15c4c3977" +down_revision = "38c9ac332c58" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "resource_tracker_licensed_items_purchases", + sa.Column("wallet_name", sa.String(), nullable=False), + ) + op.add_column( + "resource_tracker_licensed_items_purchases", + sa.Column("pricing_unit_cost_id", sa.BigInteger(), nullable=False), + ) + op.add_column( + "resource_tracker_licensed_items_purchases", + sa.Column("pricing_unit_cost", sa.Numeric(scale=2), nullable=True), + ) + op.add_column( + "resource_tracker_licensed_items_purchases", + sa.Column("num_of_seats", sa.SmallInteger(), nullable=False), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("resource_tracker_licensed_items_purchases", "num_of_seats") + op.drop_column("resource_tracker_licensed_items_purchases", "pricing_unit_cost") + op.drop_column("resource_tracker_licensed_items_purchases", "pricing_unit_cost_id") + op.drop_column("resource_tracker_licensed_items_purchases", "wallet_name") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py index 43cf052eb7b..c5c3e2b57ec 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py @@ -62,6 +62,11 @@ nullable=False, server_default=sa.sql.func.now(), ), + sa.Column( + "num_of_seats", + sa.SmallInteger, + nullable=False, + ), sa.Column( "purchased_by_user", sa.BigInteger, diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py new file mode 100644 index 00000000000..c835848b219 --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py @@ -0,0 +1,60 @@ +from fastapi import FastAPI +from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( + LicensedItemPurchaseGet, + LicensedItemsPurchasesPage, +) +from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, + LicensedItemsPurchasesCreate, +) +from models_library.rest_ordering import OrderBy +from models_library.wallets import WalletID +from servicelib.rabbitmq import RPCRouter + +from ...services import licensed_items_purchases + +router = RPCRouter() + + +@router.expose(reraise_if_error_type=()) +async def get_licensed_items_purchases_page( + app: FastAPI, + *, + product_name: ProductName, + wallet_id: WalletID, + offset: int = 0, + limit: int = 20, + order_by: OrderBy = OrderBy(field="purchased_at"), +) -> LicensedItemsPurchasesPage: + return await licensed_items_purchases.list_licensed_items_purchases( + db_engine=app.state.engine, + product_name=product_name, + offset=offset, + limit=limit, + filter_wallet_id=wallet_id, + order_by=order_by, + ) + + +@router.expose(reraise_if_error_type=()) +async def get_licensed_item_purchase( + app: FastAPI, + *, + product_name: ProductName, + licensed_item_purchase_id: LicensedItemPurchaseID, +) -> LicensedItemPurchaseGet: + return await licensed_items_purchases.get_licensed_item_purchase( + db_engine=app.state.engine, + product_name=product_name, + licensed_item_purchase_id=licensed_item_purchase_id, + ) + + +@router.expose(reraise_if_error_type=()) +async def create_licensed_item_purchase( + app: FastAPI, *, data: LicensedItemsPurchasesCreate +) -> LicensedItemPurchaseGet: + return await licensed_items_purchases.create_licensed_item_purchase( + db_engine=app.state.engine, data=data + ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py index ff2e1cdb0bb..349f86f4a01 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py @@ -8,13 +8,14 @@ from servicelib.rabbitmq import RPCRouter from ...services.modules.rabbitmq import get_rabbitmq_rpc_server -from . import _resource_tracker +from . import _licensed_items, _resource_tracker _logger = logging.getLogger(__name__) ROUTERS: list[RPCRouter] = [ _resource_tracker.router, + _licensed_items.router, ] diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py index f42690ef347..4458bd2c258 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/licensed_items_purchases.py @@ -16,12 +16,13 @@ class LicensedItemsPurchasesDB(BaseModel): licensed_item_purchase_id: LicensedItemPurchaseID product_name: ProductName licensed_item_id: LicensedItemID - wallet_id: WalletID | None - wallet_name: str | None + wallet_id: WalletID + wallet_name: str pricing_unit_cost_id: PricingUnitCostId pricing_unit_cost: Decimal start_at: datetime - expire_at: datetime | None + expire_at: datetime + num_of_seats: int purchased_by_user: UserID purchased_at: datetime modified: datetime @@ -32,14 +33,14 @@ class LicensedItemsPurchasesDB(BaseModel): class CreateLicensedItemsPurchasesDB(BaseModel): product_name: ProductName licensed_item_id: LicensedItemID - wallet_id: WalletID | None - wallet_name: str | None + wallet_id: WalletID + wallet_name: str pricing_unit_cost_id: PricingUnitCostId pricing_unit_cost: Decimal start_at: datetime - expire_at: datetime | None + expire_at: datetime + num_of_seats: int purchased_by_user: UserID purchased_at: datetime - modified: datetime model_config = ConfigDict(from_attributes=True) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py new file mode 100644 index 00000000000..3e106559b9e --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py @@ -0,0 +1,134 @@ +from typing import Annotated + +from fastapi import Depends +from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( + LicensedItemPurchaseGet, + LicensedItemsPurchasesPage, +) +from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, + LicensedItemsPurchasesCreate, +) +from models_library.rest_ordering import OrderBy +from models_library.wallets import WalletID +from sqlalchemy.ext.asyncio import AsyncEngine + +from ..api.rest.dependencies import get_resource_tracker_db_engine +from ..models.licensed_items_purchases import ( + CreateLicensedItemsPurchasesDB, + LicensedItemsPurchasesDB, +) +from .modules.db import licensed_items_purchases_db + + +async def list_licensed_items_purchases( + db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)], + *, + product_name: ProductName, + filter_wallet_id: WalletID, + offset: int = 0, + limit: int = 20, + order_by: OrderBy, +) -> LicensedItemsPurchasesPage: + total, licensed_items_purchases_list_db = await licensed_items_purchases_db.list_( + db_engine, + product_name=product_name, + filter_wallet_id=filter_wallet_id, + offset=offset, + limit=limit, + order_by=order_by, + ) + return LicensedItemsPurchasesPage( + total=total, + items=[ + LicensedItemPurchaseGet( + licensed_item_purchase_id=licensed_item_purchase_db.licensed_item_purchase_id, + product_name=licensed_item_purchase_db.product_name, + licensed_item_id=licensed_item_purchase_db.licensed_item_id, + wallet_id=licensed_item_purchase_db.wallet_id, + wallet_name=licensed_item_purchase_db.wallet_name, + pricing_unit_cost_id=licensed_item_purchase_db.pricing_unit_cost_id, + pricing_unit_cost=licensed_item_purchase_db.pricing_unit_cost, + start_at=licensed_item_purchase_db.start_at, + expire_at=licensed_item_purchase_db.expire_at, + num_of_seats=licensed_item_purchase_db.num_of_seats, + purchased_by_user=licensed_item_purchase_db.purchased_by_user, + purchased_at=licensed_item_purchase_db.purchased_at, + modified=licensed_item_purchase_db.modified, + ) + for licensed_item_purchase_db in licensed_items_purchases_list_db + ], + ) + + +async def get_licensed_item_purchase( + db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)], + *, + product_name: ProductName, + licensed_item_purchase_id: LicensedItemPurchaseID, +) -> LicensedItemPurchaseGet: + licensed_item_purchase_db: LicensedItemsPurchasesDB = ( + await licensed_items_purchases_db.get( + db_engine, + product_name=product_name, + licensed_item_purchase_id=licensed_item_purchase_id, + ) + ) + + return LicensedItemPurchaseGet( + licensed_item_purchase_id=licensed_item_purchase_db.licensed_item_purchase_id, + product_name=licensed_item_purchase_db.product_name, + licensed_item_id=licensed_item_purchase_db.licensed_item_id, + wallet_id=licensed_item_purchase_db.wallet_id, + wallet_name=licensed_item_purchase_db.wallet_name, + pricing_unit_cost_id=licensed_item_purchase_db.pricing_unit_cost_id, + pricing_unit_cost=licensed_item_purchase_db.pricing_unit_cost, + start_at=licensed_item_purchase_db.start_at, + expire_at=licensed_item_purchase_db.expire_at, + num_of_seats=licensed_item_purchase_db.num_of_seats, + purchased_by_user=licensed_item_purchase_db.purchased_by_user, + purchased_at=licensed_item_purchase_db.purchased_at, + modified=licensed_item_purchase_db.modified, + ) + + +async def create_licensed_item_purchase( + db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)], + *, + data: LicensedItemsPurchasesCreate, +) -> LicensedItemPurchaseGet: + + _create_db_data = CreateLicensedItemsPurchasesDB( + product_name=data.product_name, + licensed_item_id=data.licensed_item_id, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_unit_cost_id=data.pricing_unit_cost_id, + pricing_unit_cost=data.pricing_unit_cost, + start_at=data.start_at, + expire_at=data.expire_at, + num_of_seats=data.num_of_seats, + purchased_by_user=data.purchased_by_user, + purchased_at=data.purchased_at, + ) + + licensed_item_purchase_db: LicensedItemsPurchasesDB = ( + await licensed_items_purchases_db.create(db_engine, data=_create_db_data) + ) + + return LicensedItemPurchaseGet( + licensed_item_purchase_id=licensed_item_purchase_db.licensed_item_purchase_id, + product_name=licensed_item_purchase_db.product_name, + licensed_item_id=licensed_item_purchase_db.licensed_item_id, + wallet_id=licensed_item_purchase_db.wallet_id, + wallet_name=licensed_item_purchase_db.wallet_name, + pricing_unit_cost_id=licensed_item_purchase_db.pricing_unit_cost_id, + pricing_unit_cost=licensed_item_purchase_db.pricing_unit_cost, + start_at=licensed_item_purchase_db.start_at, + expire_at=licensed_item_purchase_db.expire_at, + num_of_seats=licensed_item_purchase_db.num_of_seats, + purchased_by_user=licensed_item_purchase_db.purchased_by_user, + purchased_at=licensed_item_purchase_db.purchased_at, + modified=licensed_item_purchase_db.modified, + ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py similarity index 92% rename from services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py rename to services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py index b84f7311fe0..e507da9f7e2 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py @@ -3,6 +3,7 @@ import sqlalchemy as sa from models_library.products import ProductName from models_library.rest_ordering import OrderBy, OrderDirection +from models_library.wallets import WalletID from pydantic import NonNegativeInt from simcore_postgres_database.models.resource_tracker_licensed_items_purchases import ( resource_tracker_licensed_items_purchases, @@ -30,6 +31,7 @@ resource_tracker_licensed_items_purchases.c.pricing_unit_cost, resource_tracker_licensed_items_purchases.c.start_at, resource_tracker_licensed_items_purchases.c.expire_at, + resource_tracker_licensed_items_purchases.c.num_of_seats, resource_tracker_licensed_items_purchases.c.purchased_by_user, resource_tracker_licensed_items_purchases.c.purchased_at, resource_tracker_licensed_items_purchases.c.modified, @@ -58,6 +60,7 @@ async def create( pricing_unit_cost=data.pricing_unit_cost, start_at=data.start_at, expire_at=data.expire_at, + num_of_seats=data.num_of_seats, purchased_by_user=data.purchased_by_user, purchased_at=data.purchased_at, modified=sa.func.now(), @@ -73,6 +76,7 @@ async def list_( connection: AsyncConnection | None = None, *, product_name: ProductName, + filter_wallet_id: WalletID, offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, @@ -80,7 +84,13 @@ async def list_( base_query = ( sa.select(*_SELECTION_ARGS) .select_from(resource_tracker_licensed_items_purchases) - .where(resource_tracker_licensed_items_purchases.c.product_name == product_name) + .where( + (resource_tracker_licensed_items_purchases.c.product_name == product_name) + & ( + resource_tracker_licensed_items_purchases.c.wallet_id + == filter_wallet_id + ) + ) ) # Select total count from base_query diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py b/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py index 40d287faa92..884cc291431 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py +++ b/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py @@ -49,6 +49,6 @@ class LicensedItemsListQueryParams( class LicensedItemsBodyParams(BaseModel): wallet_id: WalletID - num_of_seeds: int + num_of_seats: int model_config = ConfigDict(extra="forbid") From 78ef2ccdeb371bfd5b392550ee6ec3f78b285107 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 08:44:32 +0100 Subject: [PATCH 03/51] renaming tests --- ...ker_credit_transactions.py => test_api_credit_transactions.py} | 0 ...esource_tracker_pricing_plans.py => test_api_pricing_plans.py} | 0 ...tracker_pricing_plans_rpc.py => test_api_pricing_plans_rpc.py} | 0 ...r_service_runs__export.py => test_api_service_runs__export.py} | 0 ...usages.py => test_api_service_runs__list_aggregated_usages.py} | 0 ...__list_billable.py => test_api_service_runs__list_billable.py} | 0 ..._with_wallet.py => test_api_service_runs__list_with_wallet.py} | 0 ...ut_wallet.py => test_api_service_runs__list_without_wallet.py} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_credit_transactions.py => test_api_credit_transactions.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_pricing_plans.py => test_api_pricing_plans.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_pricing_plans_rpc.py => test_api_pricing_plans_rpc.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_service_runs__export.py => test_api_service_runs__export.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_service_runs__list_aggregated_usages.py => test_api_service_runs__list_aggregated_usages.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_service_runs__list_billable.py => test_api_service_runs__list_billable.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_service_runs__list_with_wallet.py => test_api_service_runs__list_with_wallet.py} (100%) rename services/resource-usage-tracker/tests/unit/with_dbs/{test_api_resource_tracker_service_runs__list_without_wallet.py => test_api_service_runs__list_without_wallet.py} (100%) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_credit_transactions.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_credit_transactions.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_credit_transactions.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_credit_transactions.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_pricing_plans.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_pricing_plans.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_pricing_plans.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_pricing_plans.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_pricing_plans_rpc.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_pricing_plans_rpc.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_pricing_plans_rpc.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_pricing_plans_rpc.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__export.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__export.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__export.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__export.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_aggregated_usages.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_aggregated_usages.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_aggregated_usages.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_aggregated_usages.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_billable.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_billable.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_billable.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_billable.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_with_wallet.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_with_wallet.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_with_wallet.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_with_wallet.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_without_wallet.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_without_wallet.py similarity index 100% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_without_wallet.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_api_service_runs__list_without_wallet.py From a6f49cd3a52241e4289f7ba92816e683deecd521 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 08:55:44 +0100 Subject: [PATCH 04/51] rpc interface --- .../licensed_items_purchases.py | 86 +++++++++++++++++++ ..._items.py => _licensed_items_purchases.py} | 0 .../test_api_licensed_items_purchases.py | 80 +++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py rename services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/{_licensed_items.py => _licensed_items_purchases.py} (100%) create mode 100644 services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py new file mode 100644 index 00000000000..b77c586f3cf --- /dev/null +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py @@ -0,0 +1,86 @@ +import logging +from typing import Final + +from models_library.api_schemas_resource_usage_tracker import ( + RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, +) +from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( + LicensedItemPurchaseGet, +) +from models_library.api_schemas_resource_usage_tracker.service_runs import ( + ServiceRunPage, +) +from models_library.products import ProductName +from models_library.rabbitmq_basic_types import RPCMethodName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemsPurchasesCreate, +) +from models_library.rest_ordering import OrderBy +from models_library.wallets import WalletID +from pydantic import AnyUrl, NonNegativeInt, TypeAdapter + +from ....logging_utils import log_decorator +from ....rabbitmq import RabbitMQRPCClient + +_logger = logging.getLogger(__name__) + + +_DEFAULT_TIMEOUT_S: Final[NonNegativeInt] = 30 + +_RPC_METHOD_NAME_ADAPTER: TypeAdapter[RPCMethodName] = TypeAdapter(RPCMethodName) + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_licensed_items_purchases_page( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + product_name: ProductName, + wallet_id: WalletID, + offset: int = 0, + limit: int = 20, + order_by: OrderBy = OrderBy(field="purchased_at"), +) -> ServiceRunPage: + result = await rabbitmq_rpc_client.request( + RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, + _RPC_METHOD_NAME_ADAPTER.validate_python("get_licensed_items_purchases_page"), + product_name=product_name, + wallet_id=wallet_id, + limit=limit, + offset=offset, + order_by=order_by, + timeout_s=_DEFAULT_TIMEOUT_S, + ) + assert isinstance(result, ServiceRunPage) # nosec + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_licensed_item_purchase( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + product_name: ProductName, + wallet_id: WalletID, +) -> LicensedItemPurchaseGet: + result = await rabbitmq_rpc_client.request( + RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, + _RPC_METHOD_NAME_ADAPTER.validate_python("get_licensed_item_purchase"), + product_name=product_name, + wallet_id=wallet_id, + timeout_s=_DEFAULT_TIMEOUT_S, + ) + assert isinstance(result, LicensedItemPurchaseGet) # nosec + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def create_licensed_item_purchase( + rabbitmq_rpc_client: RabbitMQRPCClient, *, data: LicensedItemsPurchasesCreate +) -> LicensedItemPurchaseGet: + result: AnyUrl = await rabbitmq_rpc_client.request( + RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, + _RPC_METHOD_NAME_ADAPTER.validate_python("create_licensed_item_purchase"), + data=data, + timeout_s=_DEFAULT_TIMEOUT_S, + ) + assert isinstance(result, LicensedItemPurchaseGet) # nosec + return result diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py similarity index 100% rename from services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items.py rename to services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py new file mode 100644 index 00000000000..44a6ce56016 --- /dev/null +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -0,0 +1,80 @@ +# pylint:disable=unused-variable +# pylint:disable=unused-argument +# pylint:disable=redefined-outer-name +# pylint:disable=too-many-arguments + +import os +from unittest.mock import AsyncMock, Mock + +import pytest +import sqlalchemy as sa +from moto.server import ThreadedMotoServer +from pydantic import AnyUrl, TypeAdapter +from pytest_mock import MockerFixture +from pytest_simcore.helpers.typing_env import EnvVarsDict +from servicelib.rabbitmq import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import service_runs +from settings_library.s3 import S3Settings +from types_aiobotocore_s3 import S3Client + +pytest_simcore_core_services_selection = [ + "postgres", + "rabbit", +] +pytest_simcore_ops_services_selection = [ + "adminer", +] + +_USER_ID = 1 + + +@pytest.fixture +async def mocked_export(mocker: MockerFixture) -> AsyncMock: + return mocker.patch( + "simcore_service_resource_usage_tracker.services.service_runs.service_runs_db.export_service_runs_table_to_s3", + autospec=True, + ) + + +@pytest.fixture +async def mocked_presigned_link(mocker: MockerFixture) -> AsyncMock: + return mocker.patch( + "simcore_service_resource_usage_tracker.services.service_runs.SimcoreS3API.create_single_presigned_download_link", + return_value=TypeAdapter(AnyUrl).validate_python("https://www.testing.com/"), + ) + + +@pytest.fixture +async def enable_resource_usage_tracker_s3( + mock_env: EnvVarsDict, + mocked_aws_server: ThreadedMotoServer, + mocked_s3_server_envs: EnvVarsDict, + mocked_s3_server_settings: S3Settings, + s3_client: S3Client, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Create bucket + await s3_client.create_bucket(Bucket=mocked_s3_server_settings.S3_BUCKET_NAME) + + # Remove the environment variable + if "RESOURCE_USAGE_TRACKER_S3" in os.environ: + monkeypatch.delenv("RESOURCE_USAGE_TRACKER_S3") + + +@pytest.mark.rpc_test() +async def test_rpc_list_service_runs_which_was_billed( + enable_resource_usage_tracker_s3: None, + mocked_redis_server: None, + postgres_db: sa.engine.Engine, + rpc_client: RabbitMQRPCClient, + mocked_export: Mock, + mocked_presigned_link: Mock, +): + download_url = await service_runs.export_service_runs( + rpc_client, + user_id=_USER_ID, + product_name="osparc", + ) + assert isinstance(download_url, AnyUrl) # nosec + assert mocked_export.called + assert mocked_presigned_link.called From f5de79b0d343a436ff877762972fa3279e0af291 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 11:08:11 +0100 Subject: [PATCH 05/51] RUT unit tests --- .../licensed_items_purchases.py | 1 + .../licensed_items_purchases.py | 13 +- .../api/rpc/routes.py | 4 +- .../test_api_licensed_items_purchases.py | 121 +++++++++++------- 4 files changed, 83 insertions(+), 56 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py index 69c3c64c440..6afe928e88d 100644 --- a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py @@ -1,5 +1,6 @@ from datetime import datetime from decimal import Decimal +from typing import NamedTuple from models_library.licensed_items import LicensedItemID from models_library.products import ProductName diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py index b77c586f3cf..95c002df7cb 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py @@ -6,9 +6,8 @@ ) from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( LicensedItemPurchaseGet, -) -from models_library.api_schemas_resource_usage_tracker.service_runs import ( - ServiceRunPage, + LicensedItemPurchaseID, + LicensedItemsPurchasesPage, ) from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName @@ -39,7 +38,7 @@ async def get_licensed_items_purchases_page( offset: int = 0, limit: int = 20, order_by: OrderBy = OrderBy(field="purchased_at"), -) -> ServiceRunPage: +) -> LicensedItemsPurchasesPage: result = await rabbitmq_rpc_client.request( RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, _RPC_METHOD_NAME_ADAPTER.validate_python("get_licensed_items_purchases_page"), @@ -50,7 +49,7 @@ async def get_licensed_items_purchases_page( order_by=order_by, timeout_s=_DEFAULT_TIMEOUT_S, ) - assert isinstance(result, ServiceRunPage) # nosec + assert isinstance(result, LicensedItemsPurchasesPage) # nosec return result @@ -59,13 +58,13 @@ async def get_licensed_item_purchase( rabbitmq_rpc_client: RabbitMQRPCClient, *, product_name: ProductName, - wallet_id: WalletID, + licensed_item_purchase_id: LicensedItemPurchaseID, ) -> LicensedItemPurchaseGet: result = await rabbitmq_rpc_client.request( RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, _RPC_METHOD_NAME_ADAPTER.validate_python("get_licensed_item_purchase"), product_name=product_name, - wallet_id=wallet_id, + licensed_item_purchase_id=licensed_item_purchase_id, timeout_s=_DEFAULT_TIMEOUT_S, ) assert isinstance(result, LicensedItemPurchaseGet) # nosec diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py index 349f86f4a01..f1fd1276161 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/routes.py @@ -8,14 +8,14 @@ from servicelib.rabbitmq import RPCRouter from ...services.modules.rabbitmq import get_rabbitmq_rpc_server -from . import _licensed_items, _resource_tracker +from . import _licensed_items_purchases, _resource_tracker _logger = logging.getLogger(__name__) ROUTERS: list[RPCRouter] = [ _resource_tracker.router, - _licensed_items.router, + _licensed_items_purchases.router, ] diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py index 44a6ce56016..915b86db7c2 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -3,19 +3,23 @@ # pylint:disable=redefined-outer-name # pylint:disable=too-many-arguments -import os -from unittest.mock import AsyncMock, Mock +from datetime import datetime, timezone +from decimal import Decimal -import pytest +# # Remove the environment variable +# if "RESOURCE_USAGE_TRACKER_S3" in os.environ: +# monkeypatch.delenv("RESOURCE_USAGE_TRACKER_S3") import sqlalchemy as sa -from moto.server import ThreadedMotoServer -from pydantic import AnyUrl, TypeAdapter -from pytest_mock import MockerFixture -from pytest_simcore.helpers.typing_env import EnvVarsDict +from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( + LicensedItemPurchaseGet, +) +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemsPurchasesCreate, +) from servicelib.rabbitmq import RabbitMQRPCClient -from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import service_runs -from settings_library.s3 import S3Settings -from types_aiobotocore_s3 import S3Client +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import ( + licensed_items_purchases, +) pytest_simcore_core_services_selection = [ "postgres", @@ -25,56 +29,79 @@ "adminer", ] -_USER_ID = 1 +_USER_ID = 1 -@pytest.fixture -async def mocked_export(mocker: MockerFixture) -> AsyncMock: - return mocker.patch( - "simcore_service_resource_usage_tracker.services.service_runs.service_runs_db.export_service_runs_table_to_s3", - autospec=True, - ) +# @pytest.fixture +# async def mocked_export(mocker: MockerFixture) -> AsyncMock: +# return mocker.patch( +# "simcore_service_resource_usage_tracker.services.service_runs.service_runs_db.export_service_runs_table_to_s3", +# autospec=True, +# ) -@pytest.fixture -async def mocked_presigned_link(mocker: MockerFixture) -> AsyncMock: - return mocker.patch( - "simcore_service_resource_usage_tracker.services.service_runs.SimcoreS3API.create_single_presigned_download_link", - return_value=TypeAdapter(AnyUrl).validate_python("https://www.testing.com/"), - ) +# @pytest.fixture +# async def mocked_presigned_link(mocker: MockerFixture) -> AsyncMock: +# return mocker.patch( +# "simcore_service_resource_usage_tracker.services.service_runs.SimcoreS3API.create_single_presigned_download_link", +# return_value=TypeAdapter(AnyUrl).validate_python("https://www.testing.com/"), +# ) -@pytest.fixture -async def enable_resource_usage_tracker_s3( - mock_env: EnvVarsDict, - mocked_aws_server: ThreadedMotoServer, - mocked_s3_server_envs: EnvVarsDict, - mocked_s3_server_settings: S3Settings, - s3_client: S3Client, - monkeypatch: pytest.MonkeyPatch, -) -> None: - # Create bucket - await s3_client.create_bucket(Bucket=mocked_s3_server_settings.S3_BUCKET_NAME) - # Remove the environment variable - if "RESOURCE_USAGE_TRACKER_S3" in os.environ: - monkeypatch.delenv("RESOURCE_USAGE_TRACKER_S3") +# @pytest.fixture +# async def enable_resource_usage_tracker_s3( +# mock_env: EnvVarsDict, +# mocked_aws_server: ThreadedMotoServer, +# mocked_s3_server_envs: EnvVarsDict, +# mocked_s3_server_settings: S3Settings, +# s3_client: S3Client, +# monkeypatch: pytest.MonkeyPatch, +# ) -> None: +# # Create bucket +# await s3_client.create_bucket(Bucket=mocked_s3_server_settings.S3_BUCKET_NAME) -@pytest.mark.rpc_test() -async def test_rpc_list_service_runs_which_was_billed( - enable_resource_usage_tracker_s3: None, +async def test_rpc_licensed_items_purchases_workflow( + # enable_resource_usage_tracker_s3: None, mocked_redis_server: None, postgres_db: sa.engine.Engine, rpc_client: RabbitMQRPCClient, - mocked_export: Mock, - mocked_presigned_link: Mock, + # mocked_export: Mock, + # mocked_presigned_link: Mock, ): - download_url = await service_runs.export_service_runs( + result = await licensed_items_purchases.get_licensed_items_purchases_page( + rpc_client, product_name="osparc", wallet_id=1 + ) + assert isinstance(result, list) # nosec + + _create_data = LicensedItemsPurchasesCreate( + product_name="osparc", + licensed_item_id="beb16d18-d57d-44aa-a638-9727fa4a72ef", + wallet_id=1, + wallet_name="My Wallet", + pricing_unit_cost_id=1, + pricing_unit_cost=Decimal(10), + start_at=datetime.now(tz=timezone.utc), + expire_at=datetime.now(tz=timezone.utc), + num_of_seats=1, + purchased_by_user=1, + purchased_at=datetime.now(tz=timezone.utc), + ) + + result = await licensed_items_purchases.create_licensed_item_purchase( + rpc_client, data=_create_data + ) + assert isinstance(result, LicensedItemPurchaseGet) # nosec + + result = await licensed_items_purchases.get_licensed_item_purchase( rpc_client, - user_id=_USER_ID, product_name="osparc", + licensed_item_purchase_id=result.licensed_item_purchase_id, + ) + assert isinstance(result, LicensedItemPurchaseGet) # nosec + + result = await licensed_items_purchases.get_licensed_items_purchases_page( + rpc_client, product_name="osparc", wallet_id=_create_data.wallet_id ) - assert isinstance(download_url, AnyUrl) # nosec - assert mocked_export.called - assert mocked_presigned_link.called + assert isinstance(result, list) # nosec From acd4c881a0c32ea40c9409fc42f29cfff5bc4519 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 11:37:40 +0100 Subject: [PATCH 06/51] RUT unit tests --- ...7_add_cols_to_licensed_items_purchases_.py | 4 +-- ...b_add_cols_to_licensed_items_purchases_.py | 28 +++++++++++++++++++ ...source_tracker_licensed_items_purchases.py | 2 +- .../modules/db/licensed_items_purchases_db.py | 4 +-- .../test_api_licensed_items_purchases.py | 22 +++++++++------ 5 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/d68b8128c23b_add_cols_to_licensed_items_purchases_.py diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py index 39f2ba32ea3..ee47dcb5d4a 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py @@ -1,7 +1,7 @@ """add cols to licensed_items_purchases table Revision ID: 8fa15c4c3977 -Revises: 38c9ac332c58 +Revises: 4d007819e61a Create Date: 2024-12-10 06:42:23.319239+00:00 """ @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "8fa15c4c3977" -down_revision = "38c9ac332c58" +down_revision = "4d007819e61a" branch_labels = None depends_on = None diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/d68b8128c23b_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/d68b8128c23b_add_cols_to_licensed_items_purchases_.py new file mode 100644 index 00000000000..da729aec544 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/d68b8128c23b_add_cols_to_licensed_items_purchases_.py @@ -0,0 +1,28 @@ +"""add cols to licensed_items_purchases table 2 + +Revision ID: d68b8128c23b +Revises: 8fa15c4c3977 +Create Date: 2024-12-10 10:24:28.071216+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "d68b8128c23b" +down_revision = "8fa15c4c3977" +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_column("resource_tracker_licensed_items_purchases", "licensed_item_id") + op.add_column( + "resource_tracker_licensed_items_purchases", + sa.Column("licensed_item_id", postgresql.UUID(as_uuid=True), nullable=False), + ) + + +def downgrade(): + ... diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py index c5c3e2b57ec..bfcca3b52e8 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_licensed_items_purchases.py @@ -26,7 +26,7 @@ ), sa.Column( "licensed_item_id", - sa.BigInteger, + UUID(as_uuid=True), nullable=False, ), sa.Column( diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py index e507da9f7e2..67950b7b73d 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py @@ -49,7 +49,7 @@ async def create( data: CreateLicensedItemsPurchasesDB, ) -> LicensedItemsPurchasesDB: async with transaction_context(engine, connection) as conn: - result = await conn.stream( + result = await conn.execute( resource_tracker_licensed_items_purchases.insert() .values( product_name=data.product_name, @@ -67,7 +67,7 @@ async def create( ) .returning(*_SELECTION_ARGS) ) - row = await result.first() + row = result.first() return LicensedItemsPurchasesDB.model_validate(row) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py index 915b86db7c2..aaf235351c3 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -3,7 +3,7 @@ # pylint:disable=redefined-outer-name # pylint:disable=too-many-arguments -from datetime import datetime, timezone +from datetime import UTC, datetime from decimal import Decimal # # Remove the environment variable @@ -12,6 +12,7 @@ import sqlalchemy as sa from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( LicensedItemPurchaseGet, + LicensedItemsPurchasesPage, ) from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemsPurchasesCreate, @@ -73,7 +74,9 @@ async def test_rpc_licensed_items_purchases_workflow( result = await licensed_items_purchases.get_licensed_items_purchases_page( rpc_client, product_name="osparc", wallet_id=1 ) - assert isinstance(result, list) # nosec + assert isinstance(result, LicensedItemsPurchasesPage) # nosec + assert result.items == [] + assert result.total == 0 _create_data = LicensedItemsPurchasesCreate( product_name="osparc", @@ -82,14 +85,14 @@ async def test_rpc_licensed_items_purchases_workflow( wallet_name="My Wallet", pricing_unit_cost_id=1, pricing_unit_cost=Decimal(10), - start_at=datetime.now(tz=timezone.utc), - expire_at=datetime.now(tz=timezone.utc), + start_at=datetime.now(tz=UTC), + expire_at=datetime.now(tz=UTC), num_of_seats=1, purchased_by_user=1, - purchased_at=datetime.now(tz=timezone.utc), + purchased_at=datetime.now(tz=UTC), ) - result = await licensed_items_purchases.create_licensed_item_purchase( + created_item = await licensed_items_purchases.create_licensed_item_purchase( rpc_client, data=_create_data ) assert isinstance(result, LicensedItemPurchaseGet) # nosec @@ -97,11 +100,14 @@ async def test_rpc_licensed_items_purchases_workflow( result = await licensed_items_purchases.get_licensed_item_purchase( rpc_client, product_name="osparc", - licensed_item_purchase_id=result.licensed_item_purchase_id, + licensed_item_purchase_id=created_item.licensed_item_purchase_id, ) assert isinstance(result, LicensedItemPurchaseGet) # nosec + assert result.licensed_item_purchase_id == created_item.licensed_item_purchase_id result = await licensed_items_purchases.get_licensed_items_purchases_page( rpc_client, product_name="osparc", wallet_id=_create_data.wallet_id ) - assert isinstance(result, list) # nosec + assert isinstance(result, LicensedItemsPurchasesPage) # nosec + assert len(result.items) == 1 + assert result.total == 1 From 317cf1dd8ae20e5ebbb35a5779f4ec7e54cb9001 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 11:38:17 +0100 Subject: [PATCH 07/51] RUT unit tests --- .../test_api_licensed_items_purchases.py | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py index aaf235351c3..aad656d1728 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -6,9 +6,6 @@ from datetime import UTC, datetime from decimal import Decimal -# # Remove the environment variable -# if "RESOURCE_USAGE_TRACKER_S3" in os.environ: -# monkeypatch.delenv("RESOURCE_USAGE_TRACKER_S3") import sqlalchemy as sa from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( LicensedItemPurchaseGet, @@ -31,45 +28,10 @@ ] -_USER_ID = 1 - - -# @pytest.fixture -# async def mocked_export(mocker: MockerFixture) -> AsyncMock: -# return mocker.patch( -# "simcore_service_resource_usage_tracker.services.service_runs.service_runs_db.export_service_runs_table_to_s3", -# autospec=True, -# ) - - -# @pytest.fixture -# async def mocked_presigned_link(mocker: MockerFixture) -> AsyncMock: -# return mocker.patch( -# "simcore_service_resource_usage_tracker.services.service_runs.SimcoreS3API.create_single_presigned_download_link", -# return_value=TypeAdapter(AnyUrl).validate_python("https://www.testing.com/"), -# ) - - -# @pytest.fixture -# async def enable_resource_usage_tracker_s3( -# mock_env: EnvVarsDict, -# mocked_aws_server: ThreadedMotoServer, -# mocked_s3_server_envs: EnvVarsDict, -# mocked_s3_server_settings: S3Settings, -# s3_client: S3Client, -# monkeypatch: pytest.MonkeyPatch, -# ) -> None: -# # Create bucket -# await s3_client.create_bucket(Bucket=mocked_s3_server_settings.S3_BUCKET_NAME) - - async def test_rpc_licensed_items_purchases_workflow( - # enable_resource_usage_tracker_s3: None, mocked_redis_server: None, postgres_db: sa.engine.Engine, rpc_client: RabbitMQRPCClient, - # mocked_export: Mock, - # mocked_presigned_link: Mock, ): result = await licensed_items_purchases.get_licensed_items_purchases_page( rpc_client, product_name="osparc", wallet_id=1 From 932fb00f89d107501501a4afdef226fa65e5cdb2 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 13:40:47 +0100 Subject: [PATCH 08/51] webserver part --- .../licensed_items_purchases.py | 4 +- .../licensed_items_purchases.py | 35 +++++++ .../simcore_service_webserver/application.py | 4 + .../catalog/plugin.py | 3 - .../{catalog => }/licenses/__init__.py | 0 .../licenses/_exceptions_handlers.py | 2 +- .../licenses/_licensed_items_api.py | 0 .../licenses/_licensed_items_db.py | 2 +- .../licenses/_licensed_items_handlers.py | 12 +-- .../licenses/_licensed_items_purchases_api.py | 92 +++++++++++++++++++ .../_licensed_items_purchases_handlers.py | 91 ++++++++++++++++++ .../{catalog => }/licenses/_models.py | 4 + .../{catalog => }/licenses/api.py | 0 .../{catalog => }/licenses/errors.py | 0 .../{catalog => }/licenses/plugin.py | 0 15 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/__init__.py (100%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/_exceptions_handlers.py (94%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/_licensed_items_api.py (100%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/_licensed_items_db.py (99%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/_licensed_items_handlers.py (92%) create mode 100644 services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py create mode 100644 services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/_models.py (92%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/api.py (100%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/errors.py (100%) rename services/web/server/src/simcore_service_webserver/{catalog => }/licenses/plugin.py (100%) diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py index 6afe928e88d..c147b411465 100644 --- a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py @@ -17,8 +17,8 @@ class LicensedItemPurchaseGet(BaseModel): licensed_item_purchase_id: LicensedItemPurchaseID product_name: ProductName licensed_item_id: LicensedItemID - wallet_id: WalletID | None - wallet_name: str | None + wallet_id: WalletID + wallet_name: str pricing_unit_cost_id: PricingUnitCostId pricing_unit_cost: Decimal start_at: datetime diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py new file mode 100644 index 00000000000..1005019c58d --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py @@ -0,0 +1,35 @@ +from datetime import datetime +from decimal import Decimal +from typing import NamedTuple + +from models_library.licensed_items import LicensedItemID +from models_library.products import ProductName +from models_library.resource_tracker import PricingUnitCostId +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) +from models_library.users import UserID +from models_library.wallets import WalletID +from pydantic import PositiveInt + +from ._base import OutputSchema + + +class LicensedItemPurchaseGet(OutputSchema): + licensed_item_purchase_id: LicensedItemPurchaseID + product_name: ProductName + licensed_item_id: LicensedItemID + wallet_id: WalletID + pricing_unit_cost_id: PricingUnitCostId + pricing_unit_cost: Decimal + start_at: datetime + expire_at: datetime + num_of_seats: int + purchased_by_user: UserID + purchased_at: datetime + modified: datetime + + +class LicensedItemPurchaseGetPage(NamedTuple): + items: list[LicensedItemPurchaseGet] + total: PositiveInt diff --git a/services/web/server/src/simcore_service_webserver/application.py b/services/web/server/src/simcore_service_webserver/application.py index 79477051ddb..a868e345380 100644 --- a/services/web/server/src/simcore_service_webserver/application.py +++ b/services/web/server/src/simcore_service_webserver/application.py @@ -26,6 +26,7 @@ from .garbage_collector.plugin import setup_garbage_collector from .groups.plugin import setup_groups from .invitations.plugin import setup_invitations +from .licenses.plugin import setup_licenses from .login.plugin import setup_login from .long_running_tasks import setup_long_running_tasks from .meta_modeling.plugin import setup_meta_modeling @@ -139,6 +140,9 @@ def create_application() -> web.Application: setup_version_control(app) setup_meta_modeling(app) + # licenses + setup_licenses(app) + # tagging setup_scicrunch(app) setup_tags(app) diff --git a/services/web/server/src/simcore_service_webserver/catalog/plugin.py b/services/web/server/src/simcore_service_webserver/catalog/plugin.py index 74c36bcbcb4..2af8da917f0 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/plugin.py +++ b/services/web/server/src/simcore_service_webserver/catalog/plugin.py @@ -9,7 +9,6 @@ from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup from . import _handlers, _tags_handlers -from .licenses.plugin import setup_licenses _logger = logging.getLogger(__name__) @@ -28,8 +27,6 @@ def setup_catalog(app: web.Application): for route_def in _handlers.routes ) - setup_licenses(app) - app.add_routes(_handlers.routes) app.add_routes(_tags_handlers.routes) diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/__init__.py b/services/web/server/src/simcore_service_webserver/licenses/__init__.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/__init__.py rename to services/web/server/src/simcore_service_webserver/licenses/__init__.py diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py similarity index 94% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/_exceptions_handlers.py rename to services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py index 0abb7671b16..a4b5ada8925 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/licenses/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py @@ -2,7 +2,7 @@ from servicelib.aiohttp import status -from ...exception_handling import ( +from ..exception_handling import ( ExceptionToHttpErrorMap, HttpErrorInfo, exception_handling_decorator, diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_api.py rename to services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_db.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_db.py similarity index 99% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_db.py rename to services/web/server/src/simcore_service_webserver/licenses/_licensed_items_db.py index fc14221ff91..e468c10f55d 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_db.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_db.py @@ -27,7 +27,7 @@ from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.sql import select -from ...db.plugin import get_asyncpg_engine +from ..db.plugin import get_asyncpg_engine from .errors import LicensedItemNotFoundError _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_handlers.py similarity index 92% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_handlers.py rename to services/web/server/src/simcore_service_webserver/licenses/_licensed_items_handlers.py index 6ed227500e5..355d9658ebb 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/licenses/_licensed_items_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_handlers.py @@ -17,10 +17,10 @@ from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.rest_constants import RESPONSE_MODEL_POLICY -from ..._meta import API_VTAG as VTAG -from ...login.decorators import login_required -from ...security.decorators import permission_required -from ...utils_aiohttp import envelope_json_response +from .._meta import API_VTAG as VTAG +from ..login.decorators import login_required +from ..security.decorators import permission_required +from ..utils_aiohttp import envelope_json_response from . import _licensed_items_api from ._exceptions_handlers import handle_plugin_requests_exceptions from ._models import ( @@ -40,7 +40,7 @@ @login_required @permission_required("catalog/licensed-items.*") @handle_plugin_requests_exceptions -async def list_workspaces(request: web.Request): +async def list_licensed_items(request: web.Request): req_ctx = LicensedItemsRequestContext.model_validate(request) query_params: LicensedItemsListQueryParams = parse_request_query_parameters_as( LicensedItemsListQueryParams, request @@ -77,7 +77,7 @@ async def list_workspaces(request: web.Request): @login_required @permission_required("catalog/licensed-items.*") @handle_plugin_requests_exceptions -async def get_workspace(request: web.Request): +async def get_licensed_item(request: web.Request): req_ctx = LicensedItemsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(LicensedItemsPathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py new file mode 100644 index 00000000000..b4e3b16a0ef --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py @@ -0,0 +1,92 @@ +import logging + +from aiohttp import web +from models_library.api_schemas_resource_usage_tracker import ( + licensed_items_purchases as rut_licensed_items_purchases, +) +from models_library.api_schemas_webserver import ( + licensed_items_purchases as webserver_licensed_items_purchases, +) +from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) +from models_library.rest_ordering import OrderBy +from models_library.wallets import WalletID +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import ( + licensed_items_purchases, +) + +from ..rabbitmq import get_rabbitmq_rpc_client + +_logger = logging.getLogger(__name__) + + +async def list_licensed_items_purchases( + app: web.Application, + product_name: ProductName, + wallet_id: WalletID, + offset: int, + limit: int, + order_by: OrderBy, +) -> webserver_licensed_items_purchases.LicensedItemPurchaseGetPage: + rpc_client = get_rabbitmq_rpc_client(app) + result: rut_licensed_items_purchases.LicensedItemsPurchasesPage = ( + await licensed_items_purchases.get_licensed_items_purchases_page( + rpc_client, + product_name=product_name, + wallet_id=wallet_id, + offset=offset, + limit=limit, + order_by=order_by, + ) + ) + return webserver_licensed_items_purchases.LicensedItemPurchaseGetPage( + total=result.total, + items=[ + webserver_licensed_items_purchases.LicensedItemPurchaseGet( + licensed_item_purchase_id=item.licensed_item_purchase_id, + product_name=item.product_name, + licensed_item_id=item.licensed_item_id, + wallet_id=item.wallet_id, + pricing_unit_cost_id=item.pricing_unit_cost_id, + pricing_unit_cost=item.pricing_unit_cost, + start_at=item.start_at, + expire_at=item.expire_at, + num_of_seats=item.num_of_seats, + purchased_by_user=item.purchased_by_user, + purchased_at=item.purchased_at, + modified=item.modified, + ) + for item in result.items + ], + ) + + +async def get_licensed_item_purchase( + app: web.Application, + product_name: ProductName, + licensed_item_purchase_id: LicensedItemPurchaseID, +) -> webserver_licensed_items_purchases.LicensedItemPurchaseGet: + rpc_client = get_rabbitmq_rpc_client(app) + licensed_item_get: rut_licensed_items_purchases.LicensedItemPurchaseGet = ( + await licensed_items_purchases.get_licensed_item_purchase( + rpc_client, + product_name=product_name, + licensed_item_purchase_id=licensed_item_purchase_id, + ) + ) + return webserver_licensed_items_purchases.LicensedItemPurchaseGet( + licensed_item_purchase_id=licensed_item_get.licensed_item_purchase_id, + product_name=licensed_item_get.product_name, + licensed_item_id=licensed_item_get.licensed_item_id, + wallet_id=licensed_item_get.wallet_id, + pricing_unit_cost_id=licensed_item_get.pricing_unit_cost_id, + pricing_unit_cost=licensed_item_get.pricing_unit_cost, + start_at=licensed_item_get.start_at, + expire_at=licensed_item_get.expire_at, + num_of_seats=licensed_item_get.num_of_seats, + purchased_by_user=licensed_item_get.purchased_by_user, + purchased_at=licensed_item_get.purchased_at, + modified=licensed_item_get.modified, + ) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py new file mode 100644 index 00000000000..990942b883c --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py @@ -0,0 +1,91 @@ +import logging + +from aiohttp import web +from models_library.api_schemas_webserver.licensed_items import LicensedItemGet +from models_library.api_schemas_webserver.licensed_items_purchases import ( + LicensedItemPurchaseGet, + LicensedItemPurchaseGetPage, +) +from models_library.rest_ordering import OrderBy +from models_library.rest_pagination import Page +from models_library.rest_pagination_utils import paginate_data +from servicelib.aiohttp.requests_validation import ( + parse_request_path_parameters_as, + parse_request_query_parameters_as, +) +from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON +from servicelib.rest_constants import RESPONSE_MODEL_POLICY + +from .._meta import API_VTAG as VTAG +from ..login.decorators import login_required +from ..security.decorators import permission_required +from ..utils_aiohttp import envelope_json_response +from . import _licensed_items_purchases_api +from ._exceptions_handlers import handle_plugin_requests_exceptions +from ._models import LicensedItemsPurchasesPathParams, LicensedItemsRequestContext + +_logger = logging.getLogger(__name__) + + +routes = web.RouteTableDef() + + +@routes.get( + f"/{VTAG}/catalog/licensed-items-purchases", name="list_licensed_items_purchases" +) +@login_required +@permission_required("catalog/licensed-items.*") +@handle_plugin_requests_exceptions +async def list_licensed_items_purchases(request: web.Request): + req_ctx = LicensedItemsRequestContext.model_validate(request) + query_params: LicensedItemsListQueryParams = parse_request_query_parameters_as( + LicensedItemsListQueryParams, request + ) + + licensed_item_purchase_get_page: LicensedItemPurchaseGetPage = ( + await _licensed_items_purchases_api.list_licensed_items_purchases( + app=request.app, + product_name=req_ctx.product_name, + offset=query_params.offset, + limit=query_params.limit, + order_by=OrderBy.model_construct(**query_params.order_by.model_dump()), + ) + ) + + page = Page[LicensedItemGet].model_validate( + paginate_data( + chunk=licensed_item_purchase_get_page.items, + request_url=request.url, + total=licensed_item_purchase_get_page.total, + limit=query_params.limit, + offset=query_params.offset, + ) + ) + return web.Response( + text=page.model_dump_json(**RESPONSE_MODEL_POLICY), + content_type=MIMETYPE_APPLICATION_JSON, + ) + + +@routes.get( + f"/{VTAG}/catalog/licensed-items-purchases/{{licensed_item_purchase_id}}", + name="get_licensed_item_purchase", +) +@login_required +@permission_required("catalog/licensed-items.*") +@handle_plugin_requests_exceptions +async def get_licensed_item_purchase(request: web.Request): + req_ctx = LicensedItemsRequestContext.model_validate(request) + path_params = parse_request_path_parameters_as( + LicensedItemsPurchasesPathParams, request + ) + + licensed_item_purchase_get: LicensedItemPurchaseGet = ( + await _licensed_items_purchases_api.get_licensed_item_purchase( + app=request.app, + product_name=req_ctx.product_name, + licensed_item_purchase_id=path_params.licensed_item_purchase_id, + ) + ) + + return envelope_json_response(licensed_item_purchase_get) diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py b/services/web/server/src/simcore_service_webserver/licenses/_models.py similarity index 92% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py rename to services/web/server/src/simcore_service_webserver/licenses/_models.py index 884cc291431..5c91caa45e0 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/licenses/_models.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_models.py @@ -52,3 +52,7 @@ class LicensedItemsBodyParams(BaseModel): num_of_seats: int model_config = ConfigDict(extra="forbid") + + +class LicensedItemsPurchasesPathParams(StrictRequestParameters): + licensed_item_purchase_id: LicensedItemPurchaseID diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/api.py b/services/web/server/src/simcore_service_webserver/licenses/api.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/api.py rename to services/web/server/src/simcore_service_webserver/licenses/api.py diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/errors.py b/services/web/server/src/simcore_service_webserver/licenses/errors.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/errors.py rename to services/web/server/src/simcore_service_webserver/licenses/errors.py diff --git a/services/web/server/src/simcore_service_webserver/catalog/licenses/plugin.py b/services/web/server/src/simcore_service_webserver/licenses/plugin.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/catalog/licenses/plugin.py rename to services/web/server/src/simcore_service_webserver/licenses/plugin.py From 06f5743883958abc7f1804b77aaf90dc28b0ccb0 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 14:12:00 +0100 Subject: [PATCH 09/51] webserver part --- .../licensed_items_purchases.py | 2 +- .../licenses/_licensed_items_purchases_api.py | 23 +++++- .../_licensed_items_purchases_handlers.py | 72 +++++++++++-------- .../licenses/_models.py | 25 ++++++- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py index 1005019c58d..0264e713256 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items_purchases.py @@ -27,7 +27,7 @@ class LicensedItemPurchaseGet(OutputSchema): num_of_seats: int purchased_by_user: UserID purchased_at: datetime - modified: datetime + modified_at: datetime class LicensedItemPurchaseGetPage(NamedTuple): diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py index b4e3b16a0ef..4aae82ae768 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_api.py @@ -12,12 +12,14 @@ LicensedItemPurchaseID, ) from models_library.rest_ordering import OrderBy +from models_library.users import UserID from models_library.wallets import WalletID from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import ( licensed_items_purchases, ) from ..rabbitmq import get_rabbitmq_rpc_client +from ..wallets.api import get_wallet_by_user _logger = logging.getLogger(__name__) @@ -25,11 +27,18 @@ async def list_licensed_items_purchases( app: web.Application, product_name: ProductName, + user_id: UserID, wallet_id: WalletID, offset: int, limit: int, order_by: OrderBy, ) -> webserver_licensed_items_purchases.LicensedItemPurchaseGetPage: + + # Check whether user has access to the wallet + await get_wallet_by_user( + app, user_id=user_id, wallet_id=wallet_id, product_name=product_name + ) + rpc_client = get_rabbitmq_rpc_client(app) result: rut_licensed_items_purchases.LicensedItemsPurchasesPage = ( await licensed_items_purchases.get_licensed_items_purchases_page( @@ -56,7 +65,7 @@ async def list_licensed_items_purchases( num_of_seats=item.num_of_seats, purchased_by_user=item.purchased_by_user, purchased_at=item.purchased_at, - modified=item.modified, + modified_at=item.modified, ) for item in result.items ], @@ -66,6 +75,7 @@ async def list_licensed_items_purchases( async def get_licensed_item_purchase( app: web.Application, product_name: ProductName, + user_id: UserID, licensed_item_purchase_id: LicensedItemPurchaseID, ) -> webserver_licensed_items_purchases.LicensedItemPurchaseGet: rpc_client = get_rabbitmq_rpc_client(app) @@ -76,6 +86,15 @@ async def get_licensed_item_purchase( licensed_item_purchase_id=licensed_item_purchase_id, ) ) + + # Check whether user has access to the wallet + await get_wallet_by_user( + app, + user_id=user_id, + wallet_id=licensed_item_get.wallet_id, + product_name=product_name, + ) + return webserver_licensed_items_purchases.LicensedItemPurchaseGet( licensed_item_purchase_id=licensed_item_get.licensed_item_purchase_id, product_name=licensed_item_get.product_name, @@ -88,5 +107,5 @@ async def get_licensed_item_purchase( num_of_seats=licensed_item_get.num_of_seats, purchased_by_user=licensed_item_get.purchased_by_user, purchased_at=licensed_item_get.purchased_at, - modified=licensed_item_get.modified, + modified_at=licensed_item_get.modified, ) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py index 990942b883c..bd1c0197d09 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py @@ -1,7 +1,6 @@ import logging from aiohttp import web -from models_library.api_schemas_webserver.licensed_items import LicensedItemGet from models_library.api_schemas_webserver.licensed_items_purchases import ( LicensedItemPurchaseGet, LicensedItemPurchaseGetPage, @@ -20,39 +19,74 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response +from ..wallets._handlers import WalletsPathParams from . import _licensed_items_purchases_api from ._exceptions_handlers import handle_plugin_requests_exceptions -from ._models import LicensedItemsPurchasesPathParams, LicensedItemsRequestContext +from ._models import ( + LicensedItemsPurchasesListQueryParams, + LicensedItemsPurchasesPathParams, + LicensedItemsRequestContext, +) _logger = logging.getLogger(__name__) - routes = web.RouteTableDef() @routes.get( - f"/{VTAG}/catalog/licensed-items-purchases", name="list_licensed_items_purchases" + f"/{VTAG}/catalog/licensed-items-purchases/{{licensed_item_purchase_id}}", + name="get_licensed_item_purchase", +) +@login_required +@permission_required("catalog/licensed-items.*") +@handle_plugin_requests_exceptions +async def get_licensed_item_purchase(request: web.Request): + req_ctx = LicensedItemsRequestContext.model_validate(request) + path_params = parse_request_path_parameters_as( + LicensedItemsPurchasesPathParams, request + ) + + licensed_item_purchase_get: LicensedItemPurchaseGet = ( + await _licensed_items_purchases_api.get_licensed_item_purchase( + app=request.app, + product_name=req_ctx.product_name, + user_id=req_ctx.user_id, + licensed_item_purchase_id=path_params.licensed_item_purchase_id, + ) + ) + + return envelope_json_response(licensed_item_purchase_get) + + +@routes.get( + f"/{VTAG}/wallets/{{wallet_id}}/licensed-items-purchases", + name="list_wallet_licensed_items_purchases", ) @login_required @permission_required("catalog/licensed-items.*") @handle_plugin_requests_exceptions async def list_licensed_items_purchases(request: web.Request): req_ctx = LicensedItemsRequestContext.model_validate(request) - query_params: LicensedItemsListQueryParams = parse_request_query_parameters_as( - LicensedItemsListQueryParams, request + path_params = parse_request_path_parameters_as(WalletsPathParams, request) + query_params: LicensedItemsPurchasesListQueryParams = ( + parse_request_query_parameters_as( + LicensedItemsPurchasesListQueryParams, request + ) ) licensed_item_purchase_get_page: LicensedItemPurchaseGetPage = ( await _licensed_items_purchases_api.list_licensed_items_purchases( app=request.app, product_name=req_ctx.product_name, + user_id=req_ctx.user_id, + wallet_id=path_params.wallet_id, offset=query_params.offset, limit=query_params.limit, order_by=OrderBy.model_construct(**query_params.order_by.model_dump()), ) ) - page = Page[LicensedItemGet].model_validate( + page = Page[LicensedItemPurchaseGet].model_validate( paginate_data( chunk=licensed_item_purchase_get_page.items, request_url=request.url, @@ -65,27 +99,3 @@ async def list_licensed_items_purchases(request: web.Request): text=page.model_dump_json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, ) - - -@routes.get( - f"/{VTAG}/catalog/licensed-items-purchases/{{licensed_item_purchase_id}}", - name="get_licensed_item_purchase", -) -@login_required -@permission_required("catalog/licensed-items.*") -@handle_plugin_requests_exceptions -async def get_licensed_item_purchase(request: web.Request): - req_ctx = LicensedItemsRequestContext.model_validate(request) - path_params = parse_request_path_parameters_as( - LicensedItemsPurchasesPathParams, request - ) - - licensed_item_purchase_get: LicensedItemPurchaseGet = ( - await _licensed_items_purchases_api.get_licensed_item_purchase( - app=request.app, - product_name=req_ctx.product_name, - licensed_item_purchase_id=path_params.licensed_item_purchase_id, - ) - ) - - return envelope_json_response(licensed_item_purchase_get) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_models.py b/services/web/server/src/simcore_service_webserver/licenses/_models.py index 5c91caa45e0..2d8514e28e9 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_models.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_models.py @@ -2,6 +2,9 @@ from models_library.basic_types import IDStr from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) from models_library.rest_base import RequestParameters, StrictRequestParameters from models_library.rest_ordering import ( OrderBy, @@ -14,7 +17,7 @@ from pydantic import BaseModel, ConfigDict, Field from servicelib.request_keys import RQT_USERID_KEY -from ..._constants import RQ_PRODUCT_KEY +from .._constants import RQ_PRODUCT_KEY _logger = logging.getLogger(__name__) @@ -56,3 +59,23 @@ class LicensedItemsBodyParams(BaseModel): class LicensedItemsPurchasesPathParams(StrictRequestParameters): licensed_item_purchase_id: LicensedItemPurchaseID + + +_LicensedItemsPurchasesListOrderQueryParams: type[ + RequestParameters +] = create_ordering_query_model_class( + ordering_fields={ + "purchased_at", + "modified_at", + "name", + }, + default=OrderBy(field=IDStr("purchased_at"), direction=OrderDirection.DESC), + ordering_fields_api_to_column_map={"modified_at": "modified"}, +) + + +class LicensedItemsPurchasesListQueryParams( + PageQueryParameters, + _LicensedItemsPurchasesListOrderQueryParams, # type: ignore[misc, valid-type] +): + ... From dbd1ffe7f3c7ae16de8cb00d377f1db2dcaa7a0e Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 14:25:59 +0100 Subject: [PATCH 10/51] open api specs --- ...g_licensed_items.py => _licensed_items.py} | 6 +- .../web-server/_licensed_items_purchases.py | 55 +++ api/specs/web-server/openapi.py | 3 +- .../api/v0/openapi.yaml | 382 +++++++++++++----- .../_licensed_items_purchases_handlers.py | 4 +- .../licenses/errors.py | 2 +- 6 files changed, 339 insertions(+), 113 deletions(-) rename api/specs/web-server/{_catalog_licensed_items.py => _licensed_items.py} (90%) create mode 100644 api/specs/web-server/_licensed_items_purchases.py diff --git a/api/specs/web-server/_catalog_licensed_items.py b/api/specs/web-server/_licensed_items.py similarity index 90% rename from api/specs/web-server/_catalog_licensed_items.py rename to api/specs/web-server/_licensed_items.py index 29b39853c95..377d6b9ab94 100644 --- a/api/specs/web-server/_catalog_licensed_items.py +++ b/api/specs/web-server/_licensed_items.py @@ -14,10 +14,8 @@ from models_library.generics import Envelope from models_library.rest_error import EnvelopedError from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.catalog.licenses._exceptions_handlers import ( - _TO_HTTP_ERROR_MAP, -) -from simcore_service_webserver.catalog.licenses._models import ( +from simcore_service_webserver.licenses._exceptions_handlers import _TO_HTTP_ERROR_MAP +from simcore_service_webserver.licenses._models import ( LicensedItemsBodyParams, LicensedItemsListQueryParams, LicensedItemsPathParams, diff --git a/api/specs/web-server/_licensed_items_purchases.py b/api/specs/web-server/_licensed_items_purchases.py new file mode 100644 index 00000000000..1b3f4b7cf71 --- /dev/null +++ b/api/specs/web-server/_licensed_items_purchases.py @@ -0,0 +1,55 @@ +""" Helper script to generate OAS automatically +""" + +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments + +from typing import Annotated + +from _common import as_query +from fastapi import APIRouter, Depends +from models_library.api_schemas_webserver.licensed_items_purchases import ( + LicensedItemPurchaseGet, +) +from models_library.generics import Envelope +from models_library.rest_error import EnvelopedError +from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.licenses._exceptions_handlers import _TO_HTTP_ERROR_MAP +from simcore_service_webserver.licenses._models import ( + LicensedItemsPurchasesListQueryParams, + LicensedItemsPurchasesPathParams, +) +from simcore_service_webserver.wallets._handlers import WalletsPathParams + +router = APIRouter( + prefix=f"/{API_VTAG}", + tags=[ + "licenses", + ], + responses={ + i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values() + }, +) + + +@router.get( + "/wallets/{wallet_id}/licensed-items-purchases", + response_model=Envelope[list[LicensedItemPurchaseGet]], +) +async def list_wallet_licensed_items_purchases( + _path: Annotated[WalletsPathParams, Depends()], + _query: Annotated[as_query(LicensedItemsPurchasesListQueryParams), Depends()], +): + ... + + +@router.get( + "/licensed-items-purchases/{licensed_item_purchase_id}", + response_model=Envelope[LicensedItemPurchaseGet], +) +async def get_licensed_item_purchase( + _path: Annotated[LicensedItemsPurchasesPathParams, Depends()], +): + ... diff --git a/api/specs/web-server/openapi.py b/api/specs/web-server/openapi.py index 77e656efdaa..54b0aa4361e 100644 --- a/api/specs/web-server/openapi.py +++ b/api/specs/web-server/openapi.py @@ -31,11 +31,12 @@ "_announcements", "_catalog", "_catalog_tags", # MUST BE after _catalog - "_catalog_licensed_items", "_computations", "_exporter", "_folders", "_long_running_tasks", + "_licensed_items", + "_licensed_items_purchases", "_metamodeling", "_nih_sparc", "_nih_sparc_redirections", diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 0ab1f87a5c1..890d5b77af7 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -2357,108 +2357,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_CatalogServiceGet_' - /v0/catalog/licensed-items: - get: - tags: - - licenses - - catalog - summary: List Licensed Items - operationId: list_licensed_items - parameters: - - name: order_by - in: query - required: false - schema: - type: string - contentMediaType: application/json - contentSchema: {} - default: '{"field":"modified","direction":"desc"}' - title: Order By - - name: limit - in: query - required: false - schema: - type: integer - default: 20 - title: Limit - - name: offset - in: query - required: false - schema: - type: integer - default: 0 - title: Offset - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_list_LicensedItemGet__' - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/EnvelopedError' - description: Not Found - /v0/catalog/licensed-items/{licensed_item_id}: - get: - tags: - - licenses - - catalog - summary: Get Licensed Item - operationId: get_licensed_item - parameters: - - name: licensed_item_id - in: path - required: true - schema: - type: string - format: uuid - title: Licensed Item Id - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_LicensedItemGet_' - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/EnvelopedError' - description: Not Found - /v0/catalog/licensed-items/{licensed_item_id}:purchase: - post: - tags: - - licenses - - catalog - summary: Purchase Licensed Item - operationId: purchase_licensed_item - parameters: - - name: licensed_item_id - in: path - required: true - schema: - type: string - format: uuid - title: Licensed Item Id - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LicensedItemsBodyParams' - responses: - '204': - description: Successful Response - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/EnvelopedError' - description: Not Found /v0/computations/{project_id}: get: tags: @@ -3027,6 +2925,186 @@ paths: content: application/json: schema: {} + /v0/catalog/licensed-items: + get: + tags: + - licenses + - catalog + summary: List Licensed Items + operationId: list_licensed_items + parameters: + - name: order_by + in: query + required: false + schema: + type: string + contentMediaType: application/json + contentSchema: {} + default: '{"field":"modified","direction":"desc"}' + title: Order By + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_LicensedItemGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + /v0/catalog/licensed-items/{licensed_item_id}: + get: + tags: + - licenses + - catalog + summary: Get Licensed Item + operationId: get_licensed_item + parameters: + - name: licensed_item_id + in: path + required: true + schema: + type: string + format: uuid + title: Licensed Item Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_LicensedItemGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + /v0/catalog/licensed-items/{licensed_item_id}:purchase: + post: + tags: + - licenses + - catalog + summary: Purchase Licensed Item + operationId: purchase_licensed_item + parameters: + - name: licensed_item_id + in: path + required: true + schema: + type: string + format: uuid + title: Licensed Item Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LicensedItemsBodyParams' + responses: + '204': + description: Successful Response + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + /v0/wallets/{wallet_id}/licensed-items-purchases: + get: + tags: + - licenses + summary: List Wallet Licensed Items Purchases + operationId: list_wallet_licensed_items_purchases + parameters: + - name: wallet_id + in: path + required: true + schema: + type: integer + exclusiveMinimum: true + title: Wallet Id + minimum: 0 + - name: order_by + in: query + required: false + schema: + type: string + contentMediaType: application/json + contentSchema: {} + default: '{"field":"purchased_at","direction":"desc"}' + title: Order By + - name: limit + in: query + required: false + schema: + type: integer + default: 20 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_list_LicensedItemPurchaseGet__' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + /v0/licensed-items-purchases/{licensed_item_purchase_id}: + get: + tags: + - licenses + summary: Get Licensed Item Purchase + operationId: get_licensed_item_purchase + parameters: + - name: licensed_item_purchase_id + in: path + required: true + schema: + type: string + format: uuid + title: Licensed Item Purchase Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_LicensedItemPurchaseGet_' + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found /v0/projects/{project_uuid}/checkpoint/{ref_id}/iterations: get: tags: @@ -7835,6 +7913,19 @@ components: title: Error type: object title: Envelope[LicensedItemGet] + Envelope_LicensedItemPurchaseGet_: + properties: + data: + anyOf: + - $ref: '#/components/schemas/LicensedItemPurchaseGet' + - type: 'null' + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[LicensedItemPurchaseGet] Envelope_Log_: properties: data: @@ -8616,6 +8707,22 @@ components: title: Error type: object title: Envelope[list[LicensedItemGet]] + Envelope_list_LicensedItemPurchaseGet__: + properties: + data: + anyOf: + - items: + $ref: '#/components/schemas/LicensedItemPurchaseGet' + type: array + - type: 'null' + title: Data + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[list[LicensedItemPurchaseGet]] Envelope_list_OsparcCreditsAggregatedByServiceGet__: properties: data: @@ -10029,6 +10136,71 @@ components: - createdAt - modifiedAt title: LicensedItemGet + LicensedItemPurchaseGet: + properties: + licensedItemPurchaseId: + type: string + format: uuid + title: Licenseditempurchaseid + productName: + type: string + title: Productname + licensedItemId: + type: string + format: uuid + title: Licenseditemid + walletId: + type: integer + exclusiveMinimum: true + title: Walletid + minimum: 0 + pricingUnitCostId: + type: integer + exclusiveMinimum: true + title: Pricingunitcostid + minimum: 0 + pricingUnitCost: + type: string + title: Pricingunitcost + startAt: + type: string + format: date-time + title: Startat + expireAt: + type: string + format: date-time + title: Expireat + numOfSeats: + type: integer + title: Numofseats + purchasedByUser: + type: integer + exclusiveMinimum: true + title: Purchasedbyuser + minimum: 0 + purchasedAt: + type: string + format: date-time + title: Purchasedat + modifiedAt: + type: string + format: date-time + title: Modifiedat + type: object + required: + - licensedItemPurchaseId + - productName + - licensedItemId + - walletId + - pricingUnitCostId + - pricingUnitCost + - startAt + - expireAt + - numOfSeats + - purchasedByUser + - purchasedAt + - modifiedAt + title: LicensedItemPurchaseGet LicensedItemsBodyParams: properties: wallet_id: @@ -10036,14 +10208,14 @@ components: exclusiveMinimum: true title: Wallet Id minimum: 0 - num_of_seeds: + num_of_seats: type: integer - title: Num Of Seeds + title: Num Of Seats additionalProperties: false type: object required: - wallet_id - - num_of_seeds + - num_of_seats title: LicensedItemsBodyParams LicensedResourceType: type: string diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py index bd1c0197d09..95f48ebbd0e 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_purchases_handlers.py @@ -34,7 +34,7 @@ @routes.get( - f"/{VTAG}/catalog/licensed-items-purchases/{{licensed_item_purchase_id}}", + f"/{VTAG}/licensed-items-purchases/{{licensed_item_purchase_id}}", name="get_licensed_item_purchase", ) @login_required @@ -65,7 +65,7 @@ async def get_licensed_item_purchase(request: web.Request): @login_required @permission_required("catalog/licensed-items.*") @handle_plugin_requests_exceptions -async def list_licensed_items_purchases(request: web.Request): +async def list_wallet_licensed_items_purchases(request: web.Request): req_ctx = LicensedItemsRequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) query_params: LicensedItemsPurchasesListQueryParams = ( diff --git a/services/web/server/src/simcore_service_webserver/licenses/errors.py b/services/web/server/src/simcore_service_webserver/licenses/errors.py index 0c8bae69b03..0313499429e 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/errors.py +++ b/services/web/server/src/simcore_service_webserver/licenses/errors.py @@ -1,4 +1,4 @@ -from ...errors import WebServerBaseError +from ..errors import WebServerBaseError class LicensesValueError(WebServerBaseError, ValueError): From 6bec5fff3a54de8beeab8b9fddfbf92d0a12569e Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:15:28 +0100 Subject: [PATCH 11/51] webserver part tests --- .../licensed_items_purchases.py | 4 +- .../licenses/_exceptions_handlers.py | 7 +- .../licenses/plugin.py | 3 +- .../04/licenses/test_licensed_items_db.py | 4 +- .../licenses/test_licensed_items_handlers.py | 4 +- .../test_licensed_items_purchases_handlers.py | 101 ++++++++++++++++++ 6 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_purchases_handlers.py diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py index c147b411465..c755d4954b3 100644 --- a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py @@ -32,9 +32,9 @@ class LicensedItemPurchaseGet(BaseModel): json_schema_extra={ "examples": [ { - "licensed_item_purchase_id": 1, + "licensed_item_purchase_id": "beb16d18-d57d-44aa-a638-9727fa4a72ef", "product_name": "osparc", - "licensed_item_id": "Special Pricing Plan for Sleeper", + "licensed_item_id": "303942ef-6d31-4ba8-afbe-dbb1fce2a953", "wallet_id": 1, "wallet_name": "My Wallet", "pricing_unit_cost_id": 1, diff --git a/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py index a4b5ada8925..720e7611671 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py @@ -1,6 +1,7 @@ import logging from servicelib.aiohttp import status +from simcore_service_webserver.wallets.errors import WalletAccessForbiddenError from ..exception_handling import ( ExceptionToHttpErrorMap, @@ -17,7 +18,11 @@ LicensedItemNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, "Market item {licensed_item_id} not found.", - ) + ), + WalletAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Wallet {wallet_id} forbidden.", + ), } diff --git a/services/web/server/src/simcore_service_webserver/licenses/plugin.py b/services/web/server/src/simcore_service_webserver/licenses/plugin.py index ef124c69fad..6c2ea7ce0d9 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/plugin.py +++ b/services/web/server/src/simcore_service_webserver/licenses/plugin.py @@ -7,7 +7,7 @@ from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup -from . import _licensed_items_handlers +from . import _licensed_items_handlers, _licensed_items_purchases_handlers _logger = logging.getLogger(__name__) @@ -24,3 +24,4 @@ def setup_licenses(app: web.Application): # routes app.router.add_routes(_licensed_items_handlers.routes) + app.router.add_routes(_licensed_items_purchases_handlers.routes) diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_db.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_db.py index 5455c280cd7..910e1bdf3f4 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_db.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_db.py @@ -15,9 +15,9 @@ from models_library.rest_ordering import OrderBy from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status -from simcore_service_webserver.catalog.licenses import _licensed_items_db -from simcore_service_webserver.catalog.licenses.errors import LicensedItemNotFoundError from simcore_service_webserver.db.models import UserRole +from simcore_service_webserver.licenses import _licensed_items_db +from simcore_service_webserver.licenses.errors import LicensedItemNotFoundError from simcore_service_webserver.projects.models import ProjectDict diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py index eb63d9bb75a..64f433d33dc 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py @@ -12,8 +12,8 @@ from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status -from simcore_service_webserver.catalog.licenses import _licensed_items_db from simcore_service_webserver.db.models import UserRole +from simcore_service_webserver.licenses import _licensed_items_db from simcore_service_webserver.projects.models import ProjectDict @@ -62,5 +62,5 @@ async def test_licensed_items_db_crud( url = client.app.router["purchase_licensed_item"].url_for( licensed_item_id=f"{_licensed_item_id}" ) - resp = await client.post(f"{url}", json={"wallet_id": 1, "num_of_seeds": 5}) + resp = await client.post(f"{url}", json={"wallet_id": 1, "num_of_seats": 5}) # NOTE: Not yet implemented diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_purchases_handlers.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_purchases_handlers.py new file mode 100644 index 00000000000..ce0fddeca19 --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_purchases_handlers.py @@ -0,0 +1,101 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments +# pylint: disable=too-many-statements +from decimal import Decimal +from http import HTTPStatus + +import pytest +from aiohttp.test_utils import TestClient +from models_library.api_schemas_resource_usage_tracker import ( + licensed_items_purchases as rut_licensed_items_purchases, +) +from models_library.api_schemas_webserver.licensed_items_purchases import ( + LicensedItemPurchaseGet, +) +from pytest_mock.plugin import MockerFixture +from pytest_simcore.helpers.assert_checks import assert_status +from pytest_simcore.helpers.webserver_login import UserInfoDict +from servicelib.aiohttp import status +from simcore_service_webserver.db.models import UserRole + +_LICENSED_ITEM_PURCHASE_GET = ( + rut_licensed_items_purchases.LicensedItemPurchaseGet.model_validate( + { + "licensed_item_purchase_id": "beb16d18-d57d-44aa-a638-9727fa4a72ef", + "product_name": "osparc", + "licensed_item_id": "303942ef-6d31-4ba8-afbe-dbb1fce2a953", + "wallet_id": 1, + "wallet_name": "My Wallet", + "pricing_unit_cost_id": 1, + "pricing_unit_cost": Decimal(10), + "start_at": "2023-01-11 13:11:47.293595", + "expire_at": "2023-01-11 13:11:47.293595", + "num_of_seats": 1, + "purchased_by_user": 1, + "purchased_at": "2023-01-11 13:11:47.293595", + "modified": "2023-01-11 13:11:47.293595", + } + ) +) + +_LICENSED_ITEM_PURCHASE_PAGE = rut_licensed_items_purchases.LicensedItemsPurchasesPage( + items=[_LICENSED_ITEM_PURCHASE_GET], + total=1, +) + + +@pytest.fixture +def mock_get_licensed_items_purchases_page(mocker: MockerFixture) -> tuple: + return mocker.patch( + "simcore_service_webserver.licenses._licensed_items_purchases_api.licensed_items_purchases.get_licensed_items_purchases_page", + spec=True, + return_value=_LICENSED_ITEM_PURCHASE_PAGE, + ) + + +@pytest.fixture +def mock_get_licensed_item_purchase(mocker: MockerFixture) -> tuple: + return mocker.patch( + "simcore_service_webserver.licenses._licensed_items_purchases_api.licensed_items_purchases.get_licensed_item_purchase", + spec=True, + return_value=_LICENSED_ITEM_PURCHASE_GET, + ) + + +@pytest.fixture +def mock_get_wallet_by_user(mocker: MockerFixture) -> tuple: + return mocker.patch( + "simcore_service_webserver.licenses._licensed_items_purchases_api.get_wallet_by_user", + spec=True, + ) + + +@pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) +async def test_licensed_items_db_crud( + client: TestClient, + logged_user: UserInfoDict, + expected: HTTPStatus, + mock_get_licensed_items_purchases_page: MockerFixture, + mock_get_licensed_item_purchase: MockerFixture, + mock_get_wallet_by_user: MockerFixture, +): + assert client.app + + # list + url = client.app.router["list_wallet_licensed_items_purchases"].url_for( + wallet_id="1" + ) + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert len(data) == 1 + assert LicensedItemPurchaseGet(**data[0]) + + # get + url = client.app.router["get_licensed_item_purchase"].url_for( + licensed_item_purchase_id=f"{_LICENSED_ITEM_PURCHASE_PAGE.items[0].licensed_item_purchase_id}" + ) + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert LicensedItemPurchaseGet(**data) From 3755dce4b61da4849cf1fb13ead9349c3f1723dd Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:20:55 +0100 Subject: [PATCH 12/51] open api specs --- .../api/v0/openapi.yaml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 890d5b77af7..54c2bfeaa16 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -2969,6 +2969,12 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden /v0/catalog/licensed-items/{licensed_item_id}: get: tags: @@ -2997,6 +3003,12 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden /v0/catalog/licensed-items/{licensed_item_id}:purchase: post: tags: @@ -3027,6 +3039,12 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden /v0/wallets/{wallet_id}/licensed-items-purchases: get: tags: @@ -3078,6 +3096,12 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden /v0/licensed-items-purchases/{licensed_item_purchase_id}: get: tags: @@ -3105,6 +3129,12 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Not Found + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden /v0/projects/{project_uuid}/checkpoint/{ref_id}/iterations: get: tags: From 418585271f35f0f42307574b3dc7ee7cb533fd50 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:39:47 +0100 Subject: [PATCH 13/51] fix type --- .../licensed_items_purchases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py index c755d4954b3..e75965a1b53 100644 --- a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/licensed_items_purchases.py @@ -38,14 +38,14 @@ class LicensedItemPurchaseGet(BaseModel): "wallet_id": 1, "wallet_name": "My Wallet", "pricing_unit_cost_id": 1, - "pricing_unit_cost": Decimal(10), + "pricing_unit_cost": 10, "start_at": "2023-01-11 13:11:47.293595", "expire_at": "2023-01-11 13:11:47.293595", "num_of_seats": 1, "purchased_by_user": 1, "purchased_at": "2023-01-11 13:11:47.293595", "modified": "2023-01-11 13:11:47.293595", - } # type: ignore[index,union-attr] + } ] } ) From 4c31cd001011c0c22cabdea9cb94c35c88a34494 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:42:31 +0100 Subject: [PATCH 14/51] fix type --- .../resource_usage_tracker/licensed_items_purchases.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py index 95c002df7cb..8cdeef79d60 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py @@ -9,6 +9,7 @@ LicensedItemPurchaseID, LicensedItemsPurchasesPage, ) +from models_library.basic_types import IDStr from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName from models_library.resource_tracker_licensed_items_purchases import ( @@ -37,7 +38,7 @@ async def get_licensed_items_purchases_page( wallet_id: WalletID, offset: int = 0, limit: int = 20, - order_by: OrderBy = OrderBy(field="purchased_at"), + order_by: OrderBy = OrderBy(field=IDStr("purchased_at")), ) -> LicensedItemsPurchasesPage: result = await rabbitmq_rpc_client.request( RESOURCE_USAGE_TRACKER_RPC_NAMESPACE, From 530fda09b465d24201ab8ca24d190fc66bfd1385 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:43:16 +0100 Subject: [PATCH 15/51] fix type --- .../api/rpc/_licensed_items_purchases.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py index c835848b219..1245ab3f6b4 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py @@ -3,6 +3,7 @@ LicensedItemPurchaseGet, LicensedItemsPurchasesPage, ) +from models_library.basic_types import IDStr from models_library.products import ProductName from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemPurchaseID, @@ -25,7 +26,7 @@ async def get_licensed_items_purchases_page( wallet_id: WalletID, offset: int = 0, limit: int = 20, - order_by: OrderBy = OrderBy(field="purchased_at"), + order_by: OrderBy = OrderBy(field=IDStr("purchased_at")), ) -> LicensedItemsPurchasesPage: return await licensed_items_purchases.list_licensed_items_purchases( db_engine=app.state.engine, From 00d40bda3036946af3b77c76761db26b2eadd9fb Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 10 Dec 2024 15:44:34 +0100 Subject: [PATCH 16/51] fix type --- .../services/modules/db/licensed_items_purchases_db.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py index 67950b7b73d..e9951042ddc 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py @@ -2,6 +2,9 @@ import sqlalchemy as sa from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) from models_library.rest_ordering import OrderBy, OrderDirection from models_library.wallets import WalletID from pydantic import NonNegativeInt @@ -17,7 +20,6 @@ from ....exceptions.errors import LicensedItemPurchaseNotFoundError from ....models.licensed_items_purchases import ( CreateLicensedItemsPurchasesDB, - LicensedItemPurchaseID, LicensedItemsPurchasesDB, ) From 969f982bc4e373f9112b34f524ce8d1d031ee3cb Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 05:58:35 +0100 Subject: [PATCH 17/51] review @pcrespov --- .../web-server/_licensed_items_purchases.py | 4 +- .../api/v0/openapi.yaml | 37 ++++++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/api/specs/web-server/_licensed_items_purchases.py b/api/specs/web-server/_licensed_items_purchases.py index 1b3f4b7cf71..8a993cef688 100644 --- a/api/specs/web-server/_licensed_items_purchases.py +++ b/api/specs/web-server/_licensed_items_purchases.py @@ -15,6 +15,7 @@ ) from models_library.generics import Envelope from models_library.rest_error import EnvelopedError +from models_library.rest_pagination import Page from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.licenses._exceptions_handlers import _TO_HTTP_ERROR_MAP from simcore_service_webserver.licenses._models import ( @@ -36,7 +37,8 @@ @router.get( "/wallets/{wallet_id}/licensed-items-purchases", - response_model=Envelope[list[LicensedItemPurchaseGet]], + response_model=Page[LicensedItemPurchaseGet], + tags=["wallets"], ) async def list_wallet_licensed_items_purchases( _path: Annotated[WalletsPathParams, Depends()], diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index ad1c9e49d11..6e40be15b8a 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3105,6 +3105,7 @@ paths: get: tags: - licenses + - wallets summary: List Wallet Licensed Items Purchases operationId: list_wallet_licensed_items_purchases parameters: @@ -3145,7 +3146,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Envelope_list_LicensedItemPurchaseGet__' + $ref: '#/components/schemas/Page_LicensedItemPurchaseGet_' '404': content: application/json: @@ -8869,22 +8870,6 @@ components: title: Error type: object title: Envelope[list[LicensedItemGet]] - Envelope_list_LicensedItemPurchaseGet__: - properties: - data: - anyOf: - - items: - $ref: '#/components/schemas/LicensedItemPurchaseGet' - type: array - - type: 'null' - title: Data - error: - anyOf: - - {} - - type: 'null' - title: Error - type: object - title: Envelope[list[LicensedItemPurchaseGet]] Envelope_list_OsparcCreditsAggregatedByServiceGet__: properties: data: @@ -11431,6 +11416,24 @@ components: - _links - data title: Page[CheckpointApiModel] + Page_LicensedItemPurchaseGet_: + properties: + _meta: + $ref: '#/components/schemas/PageMetaInfoLimitOffset' + _links: + $ref: '#/components/schemas/PageLinks' + data: + items: + $ref: '#/components/schemas/LicensedItemPurchaseGet' + type: array + title: Data + additionalProperties: false + type: object + required: + - _meta + - _links + - data + title: Page[LicensedItemPurchaseGet] Page_PaymentTransaction_: properties: _meta: From 138412e32736abbc1cbf67188daa30a9ffd5abc9 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 06:12:52 +0100 Subject: [PATCH 18/51] adding purchase item functionality --- .../src/models_library/resource_tracker.py | 1 + ...source_tracker_licensed_items_purchases.py | 5 +- ...f_add_cols_to_licensed_items_purchases_.py | 38 ++++++++++ ...7_add_cols_to_licensed_items_purchases_.py | 4 +- .../resource_tracker_credit_transactions.py | 10 ++- .../api/rpc/_licensed_items_purchases.py | 2 +- .../models/credit_transactions.py | 4 ++ .../services/credit_transactions.py | 1 + .../services/licensed_items_purchases.py | 72 ++++++++++++++----- .../modules/db/credit_transactions_db.py | 1 + .../process_message_running_service.py | 1 + .../test_api_licensed_items_purchases.py | 5 +- .../licenses/_licensed_items_api.py | 58 ++++++++++++++- .../licenses/_models.py | 3 + 14 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py diff --git a/packages/models-library/src/models_library/resource_tracker.py b/packages/models-library/src/models_library/resource_tracker.py index c94755817b3..20e35b7e614 100644 --- a/packages/models-library/src/models_library/resource_tracker.py +++ b/packages/models-library/src/models_library/resource_tracker.py @@ -48,6 +48,7 @@ class CreditTransactionStatus(StrAutoEnum): class CreditClassification(StrAutoEnum): ADD_WALLET_TOP_UP = auto() # user top up credits DEDUCT_SERVICE_RUN = auto() # computational/dynamic service run costs) + DEDUCT_LICENSE_PURCHASE = auto() class PricingPlanClassification(StrAutoEnum): diff --git a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py index d1ab2d88dc8..8cddc1d98aa 100644 --- a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py +++ b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py @@ -7,7 +7,7 @@ from .licensed_items import LicensedItemID from .products import ProductName -from .resource_tracker import PricingUnitCostId +from .resource_tracker import PricingPlanId, PricingUnitCostId, PricingUnitId from .users import UserID from .wallets import WalletID @@ -19,12 +19,15 @@ class LicensedItemsPurchasesCreate(BaseModel): licensed_item_id: LicensedItemID wallet_id: WalletID wallet_name: str + pricing_plan_id: PricingPlanId + pricing_unit_id: PricingUnitId pricing_unit_cost_id: PricingUnitCostId pricing_unit_cost: Decimal start_at: datetime expire_at: datetime num_of_seats: int purchased_by_user: UserID + user_email: str purchased_at: datetime model_config = ConfigDict(from_attributes=True) diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py new file mode 100644 index 00000000000..d829ece7e7a --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py @@ -0,0 +1,38 @@ +"""add cols to licensed_items_purchases table 3 + +Revision ID: 77ac824a77ff +Revises: d68b8128c23b +Create Date: 2024-12-10 16:42:14.041313+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "77ac824a77ff" +down_revision = "d68b8128c23b" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "resource_tracker_credit_transactions", + sa.Column( + "licensed_item_purchase_id", postgresql.UUID(as_uuid=True), nullable=True + ), + ) + # ### end Alembic commands ### + op.execute( + sa.DDL( + "ALTER TYPE credittransactionclassification ADD VALUE 'DEDUCT_LICENSE_PURCHASE'" + ) + ) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("resource_tracker_credit_transactions", "licensed_item_purchase_id") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py index ee47dcb5d4a..6f425116490 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py @@ -1,7 +1,7 @@ """add cols to licensed_items_purchases table Revision ID: 8fa15c4c3977 -Revises: 4d007819e61a +Revises: 5e27063c3ac9 Create Date: 2024-12-10 06:42:23.319239+00:00 """ @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "8fa15c4c3977" -down_revision = "4d007819e61a" +down_revision = "5e27063c3ac9" branch_labels = None depends_on = None diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py index d1501a42431..ca4cc470b5f 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py @@ -4,6 +4,7 @@ import enum import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID from ._common import ( NUMERIC_KWARGS, @@ -26,6 +27,7 @@ class CreditTransactionClassification(str, enum.Enum): DEDUCT_SERVICE_RUN = ( "DEDUCT_SERVICE_RUN" # computational/dynamic service run costs) ) + DEDUCT_LICENSE_PURCHASE = "DEDUCT_LICENSE_PURCHASE" resource_tracker_credit_transactions = sa.Table( @@ -117,7 +119,13 @@ class CreditTransactionClassification(str, enum.Enum): "payment_transaction_id", sa.String, nullable=True, - doc="Service run id connected with this transaction", + doc="Payment transaction id connected with this transaction", + ), + sa.Column( + "licensed_item_purchase_id", + UUID(as_uuid=True), + nullable=True, + doc="Licensed item purchase id connected with this transaction", ), column_created_datetime(timezone=True), sa.Column( diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py index 1245ab3f6b4..e8f71dfb97d 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py @@ -57,5 +57,5 @@ async def create_licensed_item_purchase( app: FastAPI, *, data: LicensedItemsPurchasesCreate ) -> LicensedItemPurchaseGet: return await licensed_items_purchases.create_licensed_item_purchase( - db_engine=app.state.engine, data=data + rabbitmq_client=app.state.rabbitmq_client, db_engine=app.state.engine, data=data ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py index 4cdf74b6429..b9fd942fee0 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py @@ -11,6 +11,9 @@ PricingUnitId, ServiceRunId, ) +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) from models_library.users import UserID from models_library.wallets import WalletID from pydantic import BaseModel, ConfigDict @@ -32,6 +35,7 @@ class CreditTransactionCreate(BaseModel): payment_transaction_id: str | None created_at: datetime last_heartbeat_at: datetime + licensed_item_purchase_id: LicensedItemPurchaseID | None class CreditTransactionCreditsUpdate(BaseModel): diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py index c58eb76be8a..fa314ee2550 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py @@ -43,6 +43,7 @@ async def create_credit_transaction( transaction_classification=CreditClassification.ADD_WALLET_TOP_UP, service_run_id=None, payment_transaction_id=credit_transaction_create_body.payment_transaction_id, + licensed_item_purchase_id=None, created_at=credit_transaction_create_body.created_at, last_heartbeat_at=credit_transaction_create_body.created_at, ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py index 3e106559b9e..f88316e095b 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py @@ -6,20 +6,28 @@ LicensedItemsPurchasesPage, ) from models_library.products import ProductName +from models_library.resource_tracker import ( + CreditClassification, + CreditTransactionStatus, +) from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemPurchaseID, LicensedItemsPurchasesCreate, ) from models_library.rest_ordering import OrderBy from models_library.wallets import WalletID +from simcore_postgres_database.utils_repos import transaction_context from sqlalchemy.ext.asyncio import AsyncEngine from ..api.rest.dependencies import get_resource_tracker_db_engine +from ..models.credit_transactions import CreditTransactionCreate from ..models.licensed_items_purchases import ( CreateLicensedItemsPurchasesDB, LicensedItemsPurchasesDB, ) -from .modules.db import licensed_items_purchases_db +from .modules.db import credit_transactions_db, licensed_items_purchases_db +from .modules.rabbitmq import RabbitMQClient, get_rabbitmq_client +from .utils import make_negative, sum_credit_transactions_and_publish_to_rabbitmq async def list_licensed_items_purchases( @@ -94,27 +102,59 @@ async def get_licensed_item_purchase( async def create_licensed_item_purchase( + rabbitmq_client: Annotated[RabbitMQClient, Depends(get_rabbitmq_client)], db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)], *, data: LicensedItemsPurchasesCreate, ) -> LicensedItemPurchaseGet: - _create_db_data = CreateLicensedItemsPurchasesDB( - product_name=data.product_name, - licensed_item_id=data.licensed_item_id, - wallet_id=data.wallet_id, - wallet_name=data.wallet_name, - pricing_unit_cost_id=data.pricing_unit_cost_id, - pricing_unit_cost=data.pricing_unit_cost, - start_at=data.start_at, - expire_at=data.expire_at, - num_of_seats=data.num_of_seats, - purchased_by_user=data.purchased_by_user, - purchased_at=data.purchased_at, - ) + async with transaction_context(db_engine) as conn: + item_purchase_create = CreateLicensedItemsPurchasesDB( + product_name=data.product_name, + licensed_item_id=data.licensed_item_id, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_unit_cost_id=data.pricing_unit_cost_id, + pricing_unit_cost=data.pricing_unit_cost, + start_at=data.start_at, + expire_at=data.expire_at, + num_of_seats=data.num_of_seats, + purchased_by_user=data.purchased_by_user, + purchased_at=data.purchased_at, + ) - licensed_item_purchase_db: LicensedItemsPurchasesDB = ( - await licensed_items_purchases_db.create(db_engine, data=_create_db_data) + licensed_item_purchase_db: LicensedItemsPurchasesDB = ( + await licensed_items_purchases_db.create( + db_engine, connection=conn, data=item_purchase_create + ) + ) + + # Deduct credits from credit_transactions table + transaction_create = CreditTransactionCreate( + product_name=data.product_name, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_plan_id=data.pricing_plan_id, + pricing_unit_id=data.pricing_unit_id, + pricing_unit_cost_id=data.pricing_unit_cost_id, + user_id=data.purchased_by_user, + user_email=data.user_email, + osparc_credits=make_negative(data.pricing_unit_cost), + transaction_status=CreditTransactionStatus.BILLED, + transaction_classification=CreditClassification.DEDUCT_LICENSE_PURCHASE, + service_run_id=None, + payment_transaction_id=None, + licensed_item_purchase_id=licensed_item_purchase_db.licensed_item_purchase_id, + created_at=data.start_at, + last_heartbeat_at=data.start_at, + ) + await credit_transactions_db.create_credit_transaction( + db_engine, connection=conn, data=transaction_create + ) + + # Publish wallet total credits to RabbitMQ + await sum_credit_transactions_and_publish_to_rabbitmq( + db_engine, rabbitmq_client, data.product_name, data.wallet_id ) return LicensedItemPurchaseGet( diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py index 76a8e9f1dfe..254a36a9732 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py @@ -48,6 +48,7 @@ async def create_credit_transaction( transaction_classification=data.transaction_classification, service_run_id=data.service_run_id, payment_transaction_id=data.payment_transaction_id, + licensed_item_purchase_id=data.licensed_item_purchase_id, created=data.created_at, last_heartbeat_at=data.last_heartbeat_at, modified=sa.func.now(), diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py index 8300ede8283..e9234f65435 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py @@ -143,6 +143,7 @@ async def _process_start_event( transaction_classification=CreditClassification.DEDUCT_SERVICE_RUN, service_run_id=service_run_id, payment_transaction_id=None, + licensed_item_purchase_id=None, created_at=msg.created_at, last_heartbeat_at=msg.created_at, ) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py index aad656d1728..e5920728d3c 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -45,19 +45,22 @@ async def test_rpc_licensed_items_purchases_workflow( licensed_item_id="beb16d18-d57d-44aa-a638-9727fa4a72ef", wallet_id=1, wallet_name="My Wallet", + pricing_plan_id=1, + pricing_unit_id=1, pricing_unit_cost_id=1, pricing_unit_cost=Decimal(10), start_at=datetime.now(tz=UTC), expire_at=datetime.now(tz=UTC), num_of_seats=1, purchased_by_user=1, + user_email="test@test.com", purchased_at=datetime.now(tz=UTC), ) created_item = await licensed_items_purchases.create_licensed_item_purchase( rpc_client, data=_create_data ) - assert isinstance(result, LicensedItemPurchaseGet) # nosec + assert isinstance(created_item, LicensedItemPurchaseGet) # nosec result = await licensed_items_purchases.get_licensed_item_purchase( rpc_client, diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py index bb024b0423b..a9ee7f1ef0b 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py @@ -1,6 +1,7 @@ # pylint: disable=unused-argument import logging +from datetime import UTC, datetime, timedelta from aiohttp import web from models_library.api_schemas_webserver.licensed_items import ( @@ -9,11 +10,21 @@ ) from models_library.licensed_items import LicensedItemID from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemsPurchasesCreate, +) from models_library.rest_ordering import OrderBy from models_library.users import UserID from pydantic import NonNegativeInt +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import ( + licensed_items_purchases, +) -from . import _licensed_items_db +from ..rabbitmq import get_rabbitmq_rpc_client +from ..resource_usage.api import get_pricing_plan_unit +from ..users.api import get_user +from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet +from . import _licensed_items_api, _licensed_items_db from ._models import LicensedItemsBodyParams _logger = logging.getLogger(__name__) @@ -74,4 +85,47 @@ async def purchase_licensed_item( licensed_item_id: LicensedItemID, body_params: LicensedItemsBodyParams, ) -> None: - raise NotImplementedError + # Check user wallet permissions + wallet = await get_wallet_with_available_credits_by_user_and_wallet( + app, user_id=user_id, wallet_id=body_params.wallet_id, product_name=product_name + ) + + licensed_item = await _licensed_items_api.get_licensed_item( + app, licensed_item_id=licensed_item_id, product_name=product_name + ) + + if licensed_item.pricing_plan_id != body_params.pricing_plan_id: + raise ValueError("You are lying!") + + pricing_unit = await get_pricing_plan_unit( + app, + product_name=product_name, + pricing_plan_id=body_params.pricing_plan_id, + pricing_unit_id=body_params.pricing_unit_id, + ) + + # Check whether wallet has enough credits + if wallet.available_credits - pricing_unit.current_cost_per_unit < 0: + raise ValueError("Not enough credits!") + + user = await get_user(app, user_id=user_id) + + _data = LicensedItemsPurchasesCreate( + product_name=product_name, + licensed_item_id=licensed_item_id, + wallet_id=wallet.wallet_id, + wallet_name=wallet.name, + pricing_plan_id=body_params.pricing_plan_id, + pricing_unit_id=body_params.pricing_unit_id, + pricing_unit_cost_id=pricing_unit.current_cost_per_unit_id, + pricing_unit_cost=pricing_unit.current_cost_per_unit, + start_at=datetime.now(tz=UTC), + expire_at=datetime.now(tz=UTC) + + timedelta(days=30), # <-- Temporary agreement with OM for proof of concept + num_of_seats=body_params.num_of_seats, + purchased_by_user=user_id, + user_email=user["email"], + purchased_at=datetime.now(tz=UTC), + ) + rpc_client = get_rabbitmq_rpc_client(app) + await licensed_items_purchases.create_licensed_item_purchase(rpc_client, data=_data) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_models.py b/services/web/server/src/simcore_service_webserver/licenses/_models.py index 2d8514e28e9..d5c2ac0947e 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_models.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_models.py @@ -2,6 +2,7 @@ from models_library.basic_types import IDStr from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemPurchaseID, ) @@ -52,6 +53,8 @@ class LicensedItemsListQueryParams( class LicensedItemsBodyParams(BaseModel): wallet_id: WalletID + pricing_plan_id: PricingPlanId + pricing_unit_id: PricingUnitId num_of_seats: int model_config = ConfigDict(extra="forbid") From 7e9de14334e18e5b8be1395b511f02e74ef4c847 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 06:17:55 +0100 Subject: [PATCH 19/51] open api specs --- .../simcore_service_webserver/api/v0/openapi.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 6e40be15b8a..62e9ebe076e 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -10372,6 +10372,16 @@ components: exclusiveMinimum: true title: Wallet Id minimum: 0 + pricing_plan_id: + type: integer + exclusiveMinimum: true + title: Pricing Plan Id + minimum: 0 + pricing_unit_id: + type: integer + exclusiveMinimum: true + title: Pricing Unit Id + minimum: 0 num_of_seats: type: integer title: Num Of Seats @@ -10379,6 +10389,8 @@ components: type: object required: - wallet_id + - pricing_plan_id + - pricing_unit_id - num_of_seats title: LicensedItemsBodyParams LicensedResourceType: From 4f1c5d6a2b925046ef5003a2b40e054b278e0115 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 06:31:35 +0100 Subject: [PATCH 20/51] improve error handling --- .../licenses/_exceptions_handlers.py | 11 ++++++++++- .../licenses/_licensed_items_api.py | 11 +++++++++-- .../src/simcore_service_webserver/licenses/errors.py | 4 ++++ .../simcore_service_webserver/wallets/_handlers.py | 9 ++++++++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py index 720e7611671..d12b95fafa0 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_exceptions_handlers.py @@ -9,7 +9,8 @@ exception_handling_decorator, to_exceptions_handlers_map, ) -from .errors import LicensedItemNotFoundError +from ..wallets.errors import WalletNotEnoughCreditsError +from .errors import LicensedItemNotFoundError, LicensedItemPricingPlanMatchError _logger = logging.getLogger(__name__) @@ -23,6 +24,14 @@ status.HTTP_403_FORBIDDEN, "Wallet {wallet_id} forbidden.", ), + WalletNotEnoughCreditsError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Not enough credits in the wallet.", + ), + LicensedItemPricingPlanMatchError: HttpErrorInfo( + status.HTTP_400_BAD_REQUEST, + "The provided pricing plan does not match the one associated with the licensed item.", + ), } diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py index a9ee7f1ef0b..1e196de47d8 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py @@ -24,8 +24,10 @@ from ..resource_usage.api import get_pricing_plan_unit from ..users.api import get_user from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet +from ..wallets.errors import WalletNotEnoughCreditsError from . import _licensed_items_api, _licensed_items_db from ._models import LicensedItemsBodyParams +from .errors import LicensedItemPricingPlanMatchError _logger = logging.getLogger(__name__) @@ -95,7 +97,10 @@ async def purchase_licensed_item( ) if licensed_item.pricing_plan_id != body_params.pricing_plan_id: - raise ValueError("You are lying!") + raise LicensedItemPricingPlanMatchError( + pricing_plan_id=body_params.pricing_plan_id, + licensed_item_id=licensed_item_id, + ) pricing_unit = await get_pricing_plan_unit( app, @@ -106,7 +111,9 @@ async def purchase_licensed_item( # Check whether wallet has enough credits if wallet.available_credits - pricing_unit.current_cost_per_unit < 0: - raise ValueError("Not enough credits!") + raise WalletNotEnoughCreditsError( + reason=f"Wallet '{wallet.name}' has {wallet.available_credits} credits." + ) user = await get_user(app, user_id=user_id) diff --git a/services/web/server/src/simcore_service_webserver/licenses/errors.py b/services/web/server/src/simcore_service_webserver/licenses/errors.py index 0313499429e..18c57966123 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/errors.py +++ b/services/web/server/src/simcore_service_webserver/licenses/errors.py @@ -7,3 +7,7 @@ class LicensesValueError(WebServerBaseError, ValueError): class LicensedItemNotFoundError(LicensesValueError): msg_template = "License good {licensed_item_id} not found" + + +class LicensedItemPricingPlanMatchError(LicensesValueError): + msg_template = "The provided pricing plan {pricing_plan_id} does not match the one associated with the licensed item {licensed_item_id}." diff --git a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py index 093edf71c21..9afcdb7c437 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py @@ -47,7 +47,11 @@ MSG_BILLING_DETAILS_NOT_DEFINED_ERROR, MSG_PRICE_NOT_DEFINED_ERROR, ) -from .errors import WalletAccessForbiddenError, WalletNotFoundError +from .errors import ( + WalletAccessForbiddenError, + WalletNotEnoughCreditsError, + WalletNotFoundError, +) _logger = logging.getLogger(__name__) @@ -87,6 +91,9 @@ async def wrapper(request: web.Request) -> web.StreamResponse: except ProductPriceNotDefinedError as exc: raise web.HTTPConflict(reason=MSG_PRICE_NOT_DEFINED_ERROR) from exc + except WalletNotEnoughCreditsError as exc: + raise web.HTTPPaymentRequired(reason=f"{exc}") from exc + except BillingDetailsNotFoundError as exc: error_code = create_error_code(exc) From 26f9c57c2c3582712079f97a1fecdf41094312f4 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 06:46:57 +0100 Subject: [PATCH 21/51] open api specs --- .../api/v0/openapi.yaml | 60 +++++++++++++++++++ .../licenses/_licensed_items_api.py | 4 +- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 62e9ebe076e..fe6f65828b9 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3031,6 +3031,18 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Forbidden + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request /v0/catalog/licensed-items/{licensed_item_id}: get: tags: @@ -3065,6 +3077,18 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Forbidden + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request /v0/catalog/licensed-items/{licensed_item_id}:purchase: post: tags: @@ -3101,6 +3125,18 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Forbidden + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request /v0/wallets/{wallet_id}/licensed-items-purchases: get: tags: @@ -3159,6 +3195,18 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Forbidden + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request /v0/licensed-items-purchases/{licensed_item_purchase_id}: get: tags: @@ -3192,6 +3240,18 @@ paths: schema: $ref: '#/components/schemas/EnvelopedError' description: Forbidden + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request /v0/projects/{project_uuid}/checkpoint/{ref_id}/iterations: get: tags: diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py index 1e196de47d8..6feacf24b1d 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py @@ -25,7 +25,7 @@ from ..users.api import get_user from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet from ..wallets.errors import WalletNotEnoughCreditsError -from . import _licensed_items_api, _licensed_items_db +from . import _licensed_items_db from ._models import LicensedItemsBodyParams from .errors import LicensedItemPricingPlanMatchError @@ -92,7 +92,7 @@ async def purchase_licensed_item( app, user_id=user_id, wallet_id=body_params.wallet_id, product_name=product_name ) - licensed_item = await _licensed_items_api.get_licensed_item( + licensed_item = await get_licensed_item( app, licensed_item_id=licensed_item_id, product_name=product_name ) From 583b2b4e4991457a61a315aa180aa7c056cbb82e Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 07:05:04 +0100 Subject: [PATCH 22/51] fix import --- .../api_schemas_webserver/wallets.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py index c9460ab74c1..ef5a4d5395d 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py @@ -20,12 +20,40 @@ class WalletGet(OutputSchema): created: datetime modified: datetime - model_config = ConfigDict(from_attributes=True, frozen=False) + model_config = ConfigDict( + from_attributes=True, + frozen=False, + json_schema_extra={ + "examples": [ + { + "wallet_id": 1, + "name": "pm_0987654321", + "description": "https://example.com/payment-method/form", + "owner": "https://example.com/payment-method/form", + "thumbnail": "https://example.com/payment-method/form", + "status": "https://example.com/payment-method/form", + "created": "https://example.com/payment-method/form", + "modified": "https://example.com/payment-method/form", + } + ] + }, + ) class WalletGetWithAvailableCredits(WalletGet): available_credits: Decimal + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + **WalletGet.model_config["json_schema_extra"]["examples"][0], + "available_credits": 10.5, + } + ] + } + ) + class WalletGetPermissions(WalletGet): read: bool From 4a1008f94af0856f68b8edc88267d76580828619 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 07:22:28 +0100 Subject: [PATCH 23/51] fix import --- .../api_schemas_webserver/wallets.py | 12 +-- .../licensed_items_purchases.py | 2 +- .../licenses/test_licensed_items_handlers.py | 79 ++++++++++++++++++- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py index ef5a4d5395d..84f1b38d7f3 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py @@ -27,13 +27,13 @@ class WalletGet(OutputSchema): "examples": [ { "wallet_id": 1, - "name": "pm_0987654321", - "description": "https://example.com/payment-method/form", - "owner": "https://example.com/payment-method/form", + "name": "My wallet", + "description": "My description", + "owner": 1, "thumbnail": "https://example.com/payment-method/form", - "status": "https://example.com/payment-method/form", - "created": "https://example.com/payment-method/form", - "modified": "https://example.com/payment-method/form", + "status": "ACTIVE", + "created": "2024-03-25T00:00:00", + "modified": "2024-03-25T00:00:00", } ] }, diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py index 8cdeef79d60..a9463271d75 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/licensed_items_purchases.py @@ -6,13 +6,13 @@ ) from models_library.api_schemas_resource_usage_tracker.licensed_items_purchases import ( LicensedItemPurchaseGet, - LicensedItemPurchaseID, LicensedItemsPurchasesPage, ) from models_library.basic_types import IDStr from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, LicensedItemsPurchasesCreate, ) from models_library.rest_ordering import OrderBy diff --git a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py index 64f433d33dc..b1fee67dafa 100644 --- a/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licensed_items_handlers.py @@ -7,8 +7,13 @@ import pytest from aiohttp.test_utils import TestClient +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, +) from models_library.api_schemas_webserver.licensed_items import LicensedItemGet +from models_library.api_schemas_webserver.wallets import WalletGetWithAvailableCredits from models_library.licensed_items import LicensedResourceType +from pytest_mock.plugin import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status @@ -18,7 +23,7 @@ @pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) -async def test_licensed_items_db_crud( +async def test_licensed_items_listing( client: TestClient, logged_user: UserInfoDict, user_project: ProjectDict, @@ -58,9 +63,77 @@ async def test_licensed_items_db_crud( data, _ = await assert_status(resp, status.HTTP_200_OK) assert LicensedItemGet(**data) + +@pytest.fixture +def mock_licensed_items_purchase_functions(mocker: MockerFixture) -> tuple: + mock_wallet_credits = mocker.patch( + "simcore_service_webserver.licenses._licensed_items_api.get_wallet_with_available_credits_by_user_and_wallet", + spec=True, + return_value=WalletGetWithAvailableCredits.model_validate( + WalletGetWithAvailableCredits.model_config["json_schema_extra"]["examples"][ + 0 + ] + ), + ) + mock_get_pricing_unit = mocker.patch( + "simcore_service_webserver.licenses._licensed_items_api.get_pricing_plan_unit", + spec=True, + return_value=PricingUnitGet.model_validate( + PricingUnitGet.model_config["json_schema_extra"]["examples"][0] + ), + ) + mock_create_licensed_item_purchase = mocker.patch( + "simcore_service_webserver.licenses._licensed_items_api.licensed_items_purchases.create_licensed_item_purchase", + spec=True, + ) + + return ( + mock_wallet_credits, + mock_get_pricing_unit, + mock_create_licensed_item_purchase, + ) + + +@pytest.mark.parametrize("user_role,expected", [(UserRole.USER, status.HTTP_200_OK)]) +async def test_licensed_items_purchase( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + osparc_product_name: str, + expected: HTTPStatus, + pricing_plan_id: int, + mock_licensed_items_purchase_functions: tuple, +): + assert client.app + + licensed_item_db = await _licensed_items_db.create( + client.app, + product_name=osparc_product_name, + name="Model A", + licensed_resource_type=LicensedResourceType.VIP_MODEL, + pricing_plan_id=pricing_plan_id, + ) + _licensed_item_id = licensed_item_db.licensed_item_id + + # get + url = client.app.router["get_licensed_item"].url_for( + licensed_item_id=f"{_licensed_item_id}" + ) + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, status.HTTP_200_OK) + assert LicensedItemGet(**data) + # purchase url = client.app.router["purchase_licensed_item"].url_for( licensed_item_id=f"{_licensed_item_id}" ) - resp = await client.post(f"{url}", json={"wallet_id": 1, "num_of_seats": 5}) - # NOTE: Not yet implemented + resp = await client.post( + f"{url}", + json={ + "wallet_id": 1, + "num_of_seats": 5, + "pricing_plan_id": pricing_plan_id, + "pricing_unit_id": 1, + }, + ) + await assert_status(resp, status.HTTP_204_NO_CONTENT) From 776806bb014806e4b247b33ce8ed8e5af6d2a7ab Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 07:54:16 +0100 Subject: [PATCH 24/51] fix typecheck --- .../src/models_library/api_schemas_webserver/wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py index 84f1b38d7f3..9cb0c3c7374 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py @@ -47,7 +47,7 @@ class WalletGetWithAvailableCredits(WalletGet): json_schema_extra={ "examples": [ { - **WalletGet.model_config["json_schema_extra"]["examples"][0], + **WalletGet.model_config["json_schema_extra"]["examples"][0], # type: ignore "available_credits": 10.5, } ] From fadfa1fd957be2a86abaeb0fa4834f243a6e006a Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 08:16:25 +0100 Subject: [PATCH 25/51] fix typecheck --- .../models_library/api_schemas_webserver/wallets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py index 9cb0c3c7374..a4f33ab3cad 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/wallets.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/wallets.py @@ -60,6 +60,19 @@ class WalletGetPermissions(WalletGet): write: bool delete: bool + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + **WalletGet.model_config["json_schema_extra"]["examples"][0], # type: ignore + "read": True, + "write": True, + "delete": True, + } + ] + } + ) + class CreateWalletBodyParams(OutputSchema): name: str From 07663139218612a036ccbe3cb622cca107722bf5 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 14:03:22 +0100 Subject: [PATCH 26/51] contract between webserver and api server --- .../rpc_interfaces/webserver/auth/api_keys.py | 6 +- .../webserver/licenses/__init__.py | 0 .../webserver/licenses/licensed_items.py | 104 ++++++++++++++ .../licenses/_rpc.py | 80 +++++++++++ .../licenses/plugin.py | 7 +- .../with_dbs/04/licenses/test_licenses_rpc.py | 127 ++++++++++++++++++ 6 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/__init__.py create mode 100644 packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/licensed_items.py create mode 100644 services/web/server/src/simcore_service_webserver/licenses/_rpc.py create mode 100644 services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/auth/api_keys.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/auth/api_keys.py index e70889e3de1..2609de81c5e 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/auth/api_keys.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/auth/api_keys.py @@ -26,7 +26,7 @@ async def create_api_key( product_name=product_name, api_key=api_key, ) - assert isinstance(result, ApiKeyGet) + assert isinstance(result, ApiKeyGet) # nosec return result @@ -45,7 +45,7 @@ async def get_api_key( product_name=product_name, api_key_id=api_key_id, ) - assert isinstance(result, ApiKeyGet) + assert isinstance(result, ApiKeyGet) # nosec return result @@ -63,4 +63,4 @@ async def delete_api_key( product_name=product_name, api_key_id=api_key_id, ) - assert result is None + assert result is None # nosec diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/__init__.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/licensed_items.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/licensed_items.py new file mode 100644 index 00000000000..e212854bae5 --- /dev/null +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/licenses/licensed_items.py @@ -0,0 +1,104 @@ +import logging + +from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGet, + LicensedItemGetPage, +) +from models_library.licensed_items import LicensedItemID +from models_library.products import ProductName +from models_library.rabbitmq_basic_types import RPCMethodName +from models_library.resource_tracker import ServiceRunId +from models_library.users import UserID +from models_library.wallets import WalletID +from pydantic import TypeAdapter +from servicelib.logging_utils import log_decorator +from servicelib.rabbitmq import RabbitMQRPCClient + +_logger = logging.getLogger(__name__) + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_licensed_items( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + product_name: str, + offset: int, + limit: int, +) -> LicensedItemGetPage: + result: LicensedItemGetPage = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_licensed_items"), + product_name=product_name, + offset=offset, + limit=limit, + ) + assert isinstance(result, LicensedItemGetPage) + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_licensed_items_for_wallet( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + user_id: UserID, + product_name: ProductName, + wallet_id: WalletID, +) -> LicensedItemGet: + result: LicensedItemGet = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_licensed_items_for_wallet"), + user_id=user_id, + product_name=product_name, + wallet_id=wallet_id, + ) + assert isinstance(result, LicensedItemGet) # nosec + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def checkout_licensed_item_for_wallet( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + user_id: UserID, + product_name: ProductName, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunId, +) -> None: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("checkout_licensed_item_for_wallet"), + user_id=user_id, + product_name=product_name, + wallet_id=wallet_id, + licensed_item_id=licensed_item_id, + num_of_seats=num_of_seats, + service_run_id=service_run_id, + ) + assert result is None # nosec + + +@log_decorator(_logger, level=logging.DEBUG) +async def release_licensed_item_for_wallet( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + user_id: UserID, + product_name: ProductName, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunId, +) -> None: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("release_licensed_item_for_wallet"), + user_id=user_id, + product_name=product_name, + wallet_id=wallet_id, + licensed_item_id=licensed_item_id, + num_of_seats=num_of_seats, + service_run_id=service_run_id, + ) + assert result is None # nosec diff --git a/services/web/server/src/simcore_service_webserver/licenses/_rpc.py b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py new file mode 100644 index 00000000000..fede0759b0d --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/licenses/_rpc.py @@ -0,0 +1,80 @@ +from aiohttp import web +from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE +from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from models_library.basic_types import IDStr +from models_library.licensed_items import LicensedItemID +from models_library.products import ProductName +from models_library.resource_tracker import ServiceRunId +from models_library.rest_ordering import OrderBy +from models_library.users import UserID +from models_library.wallets import WalletID +from servicelib.rabbitmq import RPCRouter + +from ..rabbitmq import get_rabbitmq_rpc_server +from . import _licensed_items_api + +router = RPCRouter() + + +@router.expose() +async def get_licensed_items( + app: web.Application, + *, + product_name: ProductName, + offset: int, + limit: int, +) -> LicensedItemGetPage: + licensed_item_get_page: LicensedItemGetPage = ( + await _licensed_items_api.list_licensed_items( + app=app, + product_name=product_name, + offset=offset, + limit=limit, + order_by=OrderBy(field=IDStr("name")), + ) + ) + return licensed_item_get_page + + +@router.expose(reraise_if_error_type=(NotImplementedError,)) +async def get_licensed_items_for_wallet( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + wallet_id: WalletID, +) -> None: + raise NotImplementedError + + +@router.expose(reraise_if_error_type=(NotImplementedError,)) +async def checkout_licensed_item_for_wallet( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunId, +) -> None: + raise NotImplementedError + + +@router.expose(reraise_if_error_type=(NotImplementedError,)) +async def release_licensed_item_for_wallet( + app: web.Application, + *, + user_id: str, + product_name: str, + wallet_id: WalletID, + licensed_item_id: LicensedItemID, + num_of_seats: int, + service_run_id: ServiceRunId, +) -> None: + raise NotImplementedError + + +async def register_rpc_routes_on_startup(app: web.Application): + rpc_server = get_rabbitmq_rpc_server(app) + await rpc_server.register_router(router, WEBSERVER_RPC_NAMESPACE, app) diff --git a/services/web/server/src/simcore_service_webserver/licenses/plugin.py b/services/web/server/src/simcore_service_webserver/licenses/plugin.py index 6c2ea7ce0d9..137c7b2d1dc 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/plugin.py +++ b/services/web/server/src/simcore_service_webserver/licenses/plugin.py @@ -7,7 +7,8 @@ from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup -from . import _licensed_items_handlers, _licensed_items_purchases_handlers +from ..rabbitmq import setup_rabbitmq +from . import _licensed_items_handlers, _licensed_items_purchases_handlers, _rpc _logger = logging.getLogger(__name__) @@ -25,3 +26,7 @@ def setup_licenses(app: web.Application): # routes app.router.add_routes(_licensed_items_handlers.routes) app.router.add_routes(_licensed_items_purchases_handlers.routes) + + setup_rabbitmq(app) + if app[APP_SETTINGS_KEY].WEBSERVER_RABBITMQ: + app.on_startup.append(_rpc.register_rpc_routes_on_startup) 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 new file mode 100644 index 00000000000..e3ab4f4cb3d --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/04/licenses/test_licenses_rpc.py @@ -0,0 +1,127 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable + + +from collections.abc import Awaitable, Callable + +import pytest +from aiohttp.test_utils import TestClient +from models_library.licensed_items import LicensedResourceType +from models_library.products import ProductName +from pytest_mock import MockerFixture +from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict +from pytest_simcore.helpers.typing_env import EnvVarsDict +from pytest_simcore.helpers.webserver_login import UserInfoDict +from servicelib.rabbitmq import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + checkout_licensed_item_for_wallet, + get_licensed_items, + get_licensed_items_for_wallet, + release_licensed_item_for_wallet, +) +from settings_library.rabbit import RabbitSettings +from simcore_postgres_database.models.users import UserRole +from simcore_service_webserver.application_settings import ApplicationSettings +from simcore_service_webserver.licenses import _licensed_items_db + +pytest_simcore_core_services_selection = [ + "rabbit", +] + + +@pytest.fixture +def app_environment( + rabbit_service: RabbitSettings, + app_environment: EnvVarsDict, + monkeypatch: pytest.MonkeyPatch, +): + new_envs = setenvs_from_dict( + monkeypatch, + { + **app_environment, + "RABBIT_HOST": rabbit_service.RABBIT_HOST, + "RABBIT_PORT": f"{rabbit_service.RABBIT_PORT}", + "RABBIT_USER": rabbit_service.RABBIT_USER, + "RABBIT_SECURE": f"{rabbit_service.RABBIT_SECURE}", + "RABBIT_PASSWORD": rabbit_service.RABBIT_PASSWORD.get_secret_value(), + }, + ) + + settings = ApplicationSettings.create_from_envs() + assert settings.WEBSERVER_RABBITMQ + + return new_envs + + +@pytest.fixture +def user_role() -> UserRole: + return UserRole.USER + + +@pytest.fixture +async def rpc_client( + rabbitmq_rpc_client: Callable[[str], Awaitable[RabbitMQRPCClient]], + mocker: MockerFixture, +) -> RabbitMQRPCClient: + return await rabbitmq_rpc_client("client") + + +async def test_api_keys_workflow( + client: TestClient, + rpc_client: RabbitMQRPCClient, + osparc_product_name: ProductName, + logged_user: UserInfoDict, + pricing_plan_id: int, +): + assert client.app + + result = await get_licensed_items( + rpc_client, product_name=osparc_product_name, offset=0, limit=20 + ) + assert len(result.items) == 0 + assert result.total == 0 + + await _licensed_items_db.create( + client.app, + product_name=osparc_product_name, + name="Model A", + licensed_resource_type=LicensedResourceType.VIP_MODEL, + pricing_plan_id=pricing_plan_id, + ) + + result = await get_licensed_items( + rpc_client, product_name=osparc_product_name, offset=0, limit=20 + ) + assert len(result.items) == 1 + assert result.total == 1 + + with pytest.raises(NotImplementedError): + await get_licensed_items_for_wallet( + rpc_client, + user_id=logged_user["id"], + product_name=osparc_product_name, + wallet_id=1, + ) + + with pytest.raises(NotImplementedError): + await checkout_licensed_item_for_wallet( + rpc_client, + user_id=logged_user["id"], + product_name=osparc_product_name, + wallet_id=1, + licensed_item_id="c5139a2e-4e1f-4ebe-9bfd-d17f195111ee", + num_of_seats=1, + service_run_id="run_1", + ) + + with pytest.raises(NotImplementedError): + await release_licensed_item_for_wallet( + rpc_client, + user_id=logged_user["id"], + product_name=osparc_product_name, + wallet_id=1, + licensed_item_id="c5139a2e-4e1f-4ebe-9bfd-d17f195111ee", + num_of_seats=1, + service_run_id="run_1", + ) From a851dd1fb72c4f1c0c75e97fa1514d1716db56f5 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Wed, 11 Dec 2024 15:15:09 +0100 Subject: [PATCH 27/51] add rabbitmq_rpc client to api-server --- .../api/dependencies/rabbitmq.py | 8 ++++++++ .../src/simcore_service_api_server/services/rabbitmq.py | 6 ++++++ services/api-server/tests/unit/test_services_rabbitmq.py | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py index 16de774ce52..0b86401d05e 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py @@ -6,6 +6,7 @@ from servicelib.aiohttp.application_setup import ApplicationSetupError from servicelib.fastapi.dependencies import get_app from servicelib.rabbitmq import RabbitMQClient +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from tenacity import before_sleep_log, retry, stop_after_delay, wait_fixed from ...services.log_streaming import LogDistributor @@ -15,6 +16,13 @@ _logger = logging.getLogger(__name__) +def get_rabbitmq_rpc_client( + app: Annotated[FastAPI, Depends(get_app)] +) -> RabbitMQRPCClient: + assert app.state.rabbitmq_rpc_client # nosec + return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_client) + + def get_rabbitmq_client(app: Annotated[FastAPI, Depends(get_app)]) -> RabbitMQClient: assert app.state.rabbitmq_client # nosec return cast(RabbitMQClient, app.state.rabbitmq_client) diff --git a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services/rabbitmq.py index 7b72e54b5ea..b5bb07bfe8a 100644 --- a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services/rabbitmq.py @@ -2,6 +2,7 @@ from fastapi import FastAPI from servicelib.rabbitmq import RabbitMQClient, wait_till_rabbitmq_responsive +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from settings_library.rabbit import RabbitSettings from simcore_service_api_server.core.health_checker import ApiServerHealthChecker @@ -18,6 +19,9 @@ def setup_rabbitmq(app: FastAPI) -> None: async def _on_startup() -> None: await wait_till_rabbitmq_responsive(settings.dsn) + app.state.rabbitmq_rpc_client = await RabbitMQRPCClient.create( + client_name="api_server", settings=settings + ) app.state.rabbitmq_client = RabbitMQClient( client_name="api_server", settings=settings ) @@ -40,6 +44,8 @@ async def _on_shutdown() -> None: await app.state.log_distributor.teardown() if app.state.rabbitmq_client: await app.state.rabbitmq_client.close() + if app.state.rabbitmq_rpc_client: + await app.state.rabbitmq_rpc_client.close() app.add_event_handler("startup", _on_startup) app.add_event_handler("shutdown", _on_shutdown) diff --git a/services/api-server/tests/unit/test_services_rabbitmq.py b/services/api-server/tests/unit/test_services_rabbitmq.py index 03c0f758fca..43901336d2b 100644 --- a/services/api-server/tests/unit/test_services_rabbitmq.py +++ b/services/api-server/tests/unit/test_services_rabbitmq.py @@ -11,7 +11,7 @@ from collections.abc import AsyncIterable, Callable, Iterable from contextlib import asynccontextmanager from datetime import datetime, timedelta -from typing import Final, Literal +from typing import Final, Literal, cast from unittest.mock import AsyncMock from uuid import UUID @@ -167,7 +167,7 @@ async def _go( if log_message is None: log_message = LoggerRabbitMessage( user_id=user_id, - project_id=project_id_ or faker.uuid4(), + project_id=project_id_ or cast(UUID, faker.uuid4(cast_to=None)), node_id=node_id_, messages=messages_ or [faker.text() for _ in range(10)], log_level=level_ or logging.INFO, From 1cfbfc7e0e5aac411a4d7f1e40d0b895dc738f8a Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Wed, 11 Dec 2024 16:11:17 +0100 Subject: [PATCH 28/51] start adding first endpoint --- .../simcore_service_api_server/api/root.py | 4 ++++ .../api/routes/licensed_items.py | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py diff --git a/services/api-server/src/simcore_service_api_server/api/root.py b/services/api-server/src/simcore_service_api_server/api/root.py index aa715ade554..d73247b7baf 100644 --- a/services/api-server/src/simcore_service_api_server/api/root.py +++ b/services/api-server/src/simcore_service_api_server/api/root.py @@ -7,6 +7,7 @@ from .routes import ( files, health, + licensed_items, meta, solvers, solvers_jobs, @@ -40,6 +41,9 @@ def create_router(settings: ApplicationSettings): router.include_router(studies_jobs.router, tags=["studies"], prefix="/studies") router.include_router(wallets.router, tags=["wallets"], prefix="/wallets") router.include_router(_credits.router, tags=["credits"], prefix="/credits") + router.include_router( + licensed_items.router, tags=["licensed-items"], prefix="/licensed-items" + ) # NOTE: multiple-files upload is currently disabled # Web form to upload files at http://localhost:8000/v0/upload-form-view diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py new file mode 100644 index 00000000000..97d82248fcd --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends +from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + get_licensed_items, +) +from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client + +router = APIRouter() + + +@router.get( + "/", response_model=LicensedItemGetPage, description="Get all licensed items" +) +async def get_licensed_items_asd( + wallet_id: int, + webserver_rpc_api: Annotated[RabbitMQRPCClient, Depends(get_rabbitmq_rpc_client)], +) -> LicensedItemGetPage: + return await get_licensed_items( + rabbitmq_rpc_client=webserver_rpc_api, + ) From c715cdf688e44476f3205f35fb03ad6d02bacf4b Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Wed, 11 Dec 2024 16:12:12 +0100 Subject: [PATCH 29/51] minor change --- .../simcore_service_api_server/api/routes/licensed_items.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 97d82248fcd..8574be2a1b0 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -4,7 +4,7 @@ from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( - get_licensed_items, + get_licensed_items as _get_licensed_items, ) from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client @@ -14,10 +14,10 @@ @router.get( "/", response_model=LicensedItemGetPage, description="Get all licensed items" ) -async def get_licensed_items_asd( +async def get_licensed_items( wallet_id: int, webserver_rpc_api: Annotated[RabbitMQRPCClient, Depends(get_rabbitmq_rpc_client)], ) -> LicensedItemGetPage: - return await get_licensed_items( + return await _get_licensed_items( rabbitmq_rpc_client=webserver_rpc_api, ) From a41e126d207d4a4f1ce240411afd4f2774eb8114 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 06:23:06 +0100 Subject: [PATCH 30/51] services -> services_http --- .../api/dependencies/rabbitmq.py | 2 +- .../api/dependencies/webserver.py | 2 +- .../simcore_service_api_server/api/routes/files.py | 2 +- .../api/routes/health.py | 8 ++++---- .../api/routes/solvers.py | 2 +- .../api/routes/solvers_jobs.py | 8 ++++---- .../api/routes/solvers_jobs_getters.py | 14 +++++++------- .../api/routes/studies.py | 2 +- .../api/routes/studies_jobs.py | 12 ++++++------ .../simcore_service_api_server/api/routes/users.py | 2 +- .../simcore_service_api_server/core/application.py | 4 ++-- .../core/health_checker.py | 2 +- .../{services => services_http}/__init__.py | 0 .../{services => services_http}/catalog.py | 0 .../{services => services_http}/director_v2.py | 0 .../{services => services_http}/jobs.py | 0 .../{services => services_http}/log_streaming.py | 0 .../{services => services_http}/rabbitmq.py | 2 +- .../solver_job_models_converters.py | 0 .../solver_job_outputs.py | 0 .../{services => services_http}/storage.py | 0 .../study_job_models_converters.py | 0 .../{services => services_http}/webserver.py | 0 .../api-server/tests/unit/api_solvers/conftest.py | 2 +- .../api_solvers/test_api_routers_solvers_jobs.py | 2 +- services/api-server/tests/unit/conftest.py | 2 +- .../api-server/tests/unit/test_api_solver_jobs.py | 2 +- .../tests/unit/test_models_schemas_files.py | 2 +- .../tests/unit/test_services_directorv2.py | 2 +- .../tests/unit/test_services_rabbitmq.py | 4 ++-- .../test_services_solver_job_models_converters.py | 4 ++-- .../tests/unit/test_services_solver_job_outputs.py | 2 +- 32 files changed, 42 insertions(+), 42 deletions(-) rename services/api-server/src/simcore_service_api_server/{services => services_http}/__init__.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/catalog.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/director_v2.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/jobs.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/log_streaming.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/rabbitmq.py (97%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/solver_job_models_converters.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/solver_job_outputs.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/storage.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/study_job_models_converters.py (100%) rename services/api-server/src/simcore_service_api_server/{services => services_http}/webserver.py (100%) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py index 0b86401d05e..e90c60861eb 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py @@ -9,7 +9,7 @@ from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from tenacity import before_sleep_log, retry, stop_after_delay, wait_fixed -from ...services.log_streaming import LogDistributor +from ...services_http.log_streaming import LogDistributor _MAX_WAIT_FOR_LOG_DISTRIBUTOR_SECONDS: Final[int] = 10 diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py index 94cfed68b55..d70a64575e2 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py @@ -9,7 +9,7 @@ from ..._constants import MSG_BACKEND_SERVICE_UNAVAILABLE from ...core.settings import ApplicationSettings, WebServerSettings -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from .application import get_app, get_settings from .authentication import Identity, get_active_user_email, get_current_identity diff --git a/services/api-server/src/simcore_service_api_server/api/routes/files.py b/services/api-server/src/simcore_service_api_server/api/routes/files.py index 219306f693b..2187e68ad06 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/files.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/files.py @@ -37,7 +37,7 @@ FileUploadData, UploadLinks, ) -from ...services.storage import StorageApi, StorageFileMetaData, to_file_api_model +from ...services_http.storage import StorageApi, StorageFileMetaData, to_file_api_model from ..dependencies.authentication import get_current_user_id from ..dependencies.services import get_api_client from ._common import API_SERVER_DEV_FEATURES_ENABLED diff --git a/services/api-server/src/simcore_service_api_server/api/routes/health.py b/services/api-server/src/simcore_service_api_server/api/routes/health.py index 1537d1a5d65..0b7bfbcbc9f 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/health.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/health.py @@ -10,10 +10,10 @@ from ..._meta import API_VERSION, PROJECT_NAME from ...core.health_checker import ApiServerHealthChecker, get_health_checker -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.storage import StorageApi -from ...services.webserver import WebserverApi +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.storage import StorageApi +from ...services_http.webserver import WebserverApi from ..dependencies.application import get_reverse_url_mapper from ..dependencies.services import get_api_client diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py index c85b8b39baf..2ef1cea93e6 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py @@ -13,7 +13,7 @@ from ...models.schemas.errors import ErrorGet from ...models.schemas.model_adapter import ServicePricingPlanGetLegacy from ...models.schemas.solvers import Solver, SolverKeyId, SolverPort -from ...services.catalog import CatalogApi +from ...services_http.catalog import CatalogApi from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py index af1c80c70ac..a7a893ca244 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py @@ -26,10 +26,10 @@ JobStatus, ) from ...models.schemas.solvers import Solver, SolverKeyId -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import replace_custom_metadata, start_project, stop_project -from ...services.solver_job_models_converters import ( +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import replace_custom_metadata, start_project, stop_project +from ...services_http.solver_job_models_converters import ( create_job_from_project, create_jobstatus_from_task, create_new_project_for_job, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py index 4903d1cb815..6b863ebb53a 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py @@ -38,16 +38,16 @@ WalletGetWithAvailableCreditsLegacy, ) from ...models.schemas.solvers import SolverKeyId -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import ( +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import ( get_custom_metadata, raise_if_job_not_associated_with_solver, ) -from ...services.log_streaming import LogDistributor, LogStreamer -from ...services.solver_job_models_converters import create_job_from_project -from ...services.solver_job_outputs import ResultsTypes, get_solver_output_results -from ...services.storage import StorageApi, to_file_api_model +from ...services_http.log_streaming import LogDistributor, LogStreamer +from ...services_http.solver_job_models_converters import create_job_from_project +from ...services_http.solver_job_outputs import ResultsTypes, get_solver_output_results +from ...services_http.storage import StorageApi, to_file_api_model from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.database import Engine, get_db_engine diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies.py b/services/api-server/src/simcore_service_api_server/api/routes/studies.py index 392acd8c72a..77d273d8233 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies.py @@ -13,7 +13,7 @@ from ...models.pagination import OnePage, Page, PaginationParams from ...models.schemas.errors import ErrorGet from ...models.schemas.studies import Study, StudyID, StudyPort -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from ..dependencies.webserver import get_webserver_session _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index 8d23def5c0b..11755e93640 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -32,21 +32,21 @@ JobStatus, ) from ...models.schemas.studies import JobLogsMap, Study, StudyID -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import ( +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import ( get_custom_metadata, replace_custom_metadata, start_project, stop_project, ) -from ...services.solver_job_models_converters import create_jobstatus_from_task -from ...services.storage import StorageApi -from ...services.study_job_models_converters import ( +from ...services_http.solver_job_models_converters import create_jobstatus_from_task +from ...services_http.storage import StorageApi +from ...services_http.study_job_models_converters import ( create_job_from_study, create_job_outputs_from_project_outputs, get_project_and_file_inputs_from_job_inputs, ) -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from ..dependencies.application import get_reverse_url_mapper from ._common import API_SERVER_DEV_FEATURES_ENABLED from ._constants import FMSG_CHANGELOG_CHANGED_IN_VERSION, FMSG_CHANGELOG_NEW_IN_VERSION diff --git a/services/api-server/src/simcore_service_api_server/api/routes/users.py b/services/api-server/src/simcore_service_api_server/api/routes/users.py index b63ff52f18c..14954a45285 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/users.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/users.py @@ -6,7 +6,7 @@ from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.schemas.errors import ErrorGet from ...models.schemas.profiles import Profile, ProfileUpdate -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from ..dependencies.webserver import get_webserver_session _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/core/application.py b/services/api-server/src/simcore_service_api_server/core/application.py index 714d7993b6b..4de017698a6 100644 --- a/services/api-server/src/simcore_service_api_server/core/application.py +++ b/services/api-server/src/simcore_service_api_server/core/application.py @@ -12,8 +12,8 @@ from .._meta import API_VERSION, API_VTAG, APP_NAME from ..api.root import create_router from ..api.routes.health import router as health_router -from ..services import catalog, director_v2, storage, webserver -from ..services.rabbitmq import setup_rabbitmq +from ..services_http import catalog, director_v2, storage, webserver +from ..services_http.rabbitmq import setup_rabbitmq from ._prometheus_instrumentation import setup_prometheus_instrumentation from .events import create_start_app_handler, create_stop_app_handler from .openapi import override_openapi_method, use_route_names_as_operation_ids diff --git a/services/api-server/src/simcore_service_api_server/core/health_checker.py b/services/api-server/src/simcore_service_api_server/core/health_checker.py index 068b2d79f37..a05e046b2e8 100644 --- a/services/api-server/src/simcore_service_api_server/core/health_checker.py +++ b/services/api-server/src/simcore_service_api_server/core/health_checker.py @@ -16,7 +16,7 @@ from .._meta import PROJECT_NAME from ..models.schemas.jobs import JobID, JobLog -from ..services.log_streaming import LogDistributor +from ..services_http.log_streaming import LogDistributor METRICS_NAMESPACE: Final[str] = PROJECT_NAME.replace("-", "_") diff --git a/services/api-server/src/simcore_service_api_server/services/__init__.py b/services/api-server/src/simcore_service_api_server/services_http/__init__.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/__init__.py rename to services/api-server/src/simcore_service_api_server/services_http/__init__.py diff --git a/services/api-server/src/simcore_service_api_server/services/catalog.py b/services/api-server/src/simcore_service_api_server/services_http/catalog.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/catalog.py rename to services/api-server/src/simcore_service_api_server/services_http/catalog.py diff --git a/services/api-server/src/simcore_service_api_server/services/director_v2.py b/services/api-server/src/simcore_service_api_server/services_http/director_v2.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/director_v2.py rename to services/api-server/src/simcore_service_api_server/services_http/director_v2.py diff --git a/services/api-server/src/simcore_service_api_server/services/jobs.py b/services/api-server/src/simcore_service_api_server/services_http/jobs.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/jobs.py rename to services/api-server/src/simcore_service_api_server/services_http/jobs.py diff --git a/services/api-server/src/simcore_service_api_server/services/log_streaming.py b/services/api-server/src/simcore_service_api_server/services_http/log_streaming.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/log_streaming.py rename to services/api-server/src/simcore_service_api_server/services_http/log_streaming.py diff --git a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py similarity index 97% rename from services/api-server/src/simcore_service_api_server/services/rabbitmq.py rename to services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py index b5bb07bfe8a..a0ad4801048 100644 --- a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py @@ -6,7 +6,7 @@ from settings_library.rabbit import RabbitSettings from simcore_service_api_server.core.health_checker import ApiServerHealthChecker -from ..services.log_streaming import LogDistributor +from ..services_http.log_streaming import LogDistributor _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/services/solver_job_models_converters.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/solver_job_models_converters.py rename to services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py diff --git a/services/api-server/src/simcore_service_api_server/services/solver_job_outputs.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/solver_job_outputs.py rename to services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py diff --git a/services/api-server/src/simcore_service_api_server/services/storage.py b/services/api-server/src/simcore_service_api_server/services_http/storage.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/storage.py rename to services/api-server/src/simcore_service_api_server/services_http/storage.py diff --git a/services/api-server/src/simcore_service_api_server/services/study_job_models_converters.py b/services/api-server/src/simcore_service_api_server/services_http/study_job_models_converters.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/study_job_models_converters.py rename to services/api-server/src/simcore_service_api_server/services_http/study_job_models_converters.py diff --git a/services/api-server/src/simcore_service_api_server/services/webserver.py b/services/api-server/src/simcore_service_api_server/services_http/webserver.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/webserver.py rename to services/api-server/src/simcore_service_api_server/services_http/webserver.py diff --git a/services/api-server/tests/unit/api_solvers/conftest.py b/services/api-server/tests/unit/api_solvers/conftest.py index ec8bf7d5630..4d008380aaf 100644 --- a/services/api-server/tests/unit/api_solvers/conftest.py +++ b/services/api-server/tests/unit/api_solvers/conftest.py @@ -16,7 +16,7 @@ from pytest_simcore.helpers import faker_catalog from respx import MockRouter from simcore_service_api_server.core.settings import ApplicationSettings -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet @pytest.fixture diff --git a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py index 865983537b0..e3f97def12d 100644 --- a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py +++ b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py @@ -22,7 +22,7 @@ from simcore_service_api_server._meta import API_VTAG from simcore_service_api_server.core.settings import ApplicationSettings from simcore_service_api_server.models.schemas.jobs import Job, JobInputs, JobStatus -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet from starlette import status diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py index a8ade97aee9..2b994b2f612 100644 --- a/services/api-server/tests/unit/conftest.py +++ b/services/api-server/tests/unit/conftest.py @@ -46,7 +46,7 @@ from simcore_service_api_server.core.application import init_app from simcore_service_api_server.core.settings import ApplicationSettings from simcore_service_api_server.db.repositories.api_keys import UserAndProductTuple -from simcore_service_api_server.services.solver_job_outputs import ResultsTypes +from simcore_service_api_server.services_http.solver_job_outputs import ResultsTypes @pytest.fixture diff --git a/services/api-server/tests/unit/test_api_solver_jobs.py b/services/api-server/tests/unit/test_api_solver_jobs.py index 4ed52877cfa..0f0e91f126f 100644 --- a/services/api-server/tests/unit/test_api_solver_jobs.py +++ b/services/api-server/tests/unit/test_api_solver_jobs.py @@ -28,7 +28,7 @@ WalletGetWithAvailableCreditsLegacy, ) from simcore_service_api_server.models.schemas.solvers import Solver -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet def _start_job_side_effect( diff --git a/services/api-server/tests/unit/test_models_schemas_files.py b/services/api-server/tests/unit/test_models_schemas_files.py index 2ae9c4e5144..578216dd253 100644 --- a/services/api-server/tests/unit/test_models_schemas_files.py +++ b/services/api-server/tests/unit/test_models_schemas_files.py @@ -16,7 +16,7 @@ from models_library.projects_nodes_io import StorageFileID from pydantic import TypeAdapter, ValidationError from simcore_service_api_server.models.schemas.files import File -from simcore_service_api_server.services.storage import to_file_api_model +from simcore_service_api_server.services_http.storage import to_file_api_model FILE_CONTENT = "This is a test" diff --git a/services/api-server/tests/unit/test_services_directorv2.py b/services/api-server/tests/unit/test_services_directorv2.py index b88965fd755..bd5e6d9a7ae 100644 --- a/services/api-server/tests/unit/test_services_directorv2.py +++ b/services/api-server/tests/unit/test_services_directorv2.py @@ -12,7 +12,7 @@ from respx import MockRouter from settings_library.director_v2 import DirectorV2Settings from simcore_service_api_server.exceptions.backend_errors import JobNotFoundError -from simcore_service_api_server.services.director_v2 import DirectorV2Api +from simcore_service_api_server.services_http.director_v2 import DirectorV2Api @pytest.fixture diff --git a/services/api-server/tests/unit/test_services_rabbitmq.py b/services/api-server/tests/unit/test_services_rabbitmq.py index 43901336d2b..be0220042ab 100644 --- a/services/api-server/tests/unit/test_services_rabbitmq.py +++ b/services/api-server/tests/unit/test_services_rabbitmq.py @@ -38,11 +38,11 @@ from simcore_service_api_server.api.dependencies.rabbitmq import get_log_distributor from simcore_service_api_server.core.health_checker import get_health_checker from simcore_service_api_server.models.schemas.jobs import JobID, JobLog -from simcore_service_api_server.services.director_v2 import ( +from simcore_service_api_server.services_http.director_v2 import ( ComputationTaskGet, DirectorV2Api, ) -from simcore_service_api_server.services.log_streaming import ( +from simcore_service_api_server.services_http.log_streaming import ( LogDistributor, LogStreamer, LogStreamerRegistrationConflictError, diff --git a/services/api-server/tests/unit/test_services_solver_job_models_converters.py b/services/api-server/tests/unit/test_services_solver_job_models_converters.py index 1016096dce5..40d60359477 100644 --- a/services/api-server/tests/unit/test_services_solver_job_models_converters.py +++ b/services/api-server/tests/unit/test_services_solver_job_models_converters.py @@ -10,7 +10,7 @@ from simcore_service_api_server.models.schemas.files import File from simcore_service_api_server.models.schemas.jobs import ArgumentTypes, Job, JobInputs from simcore_service_api_server.models.schemas.solvers import Solver -from simcore_service_api_server.services.solver_job_models_converters import ( +from simcore_service_api_server.services_http.solver_job_models_converters import ( create_job_from_project, create_job_inputs_from_node_inputs, create_jobstatus_from_task, @@ -220,7 +220,7 @@ def fake_url_for(*args, **kwargs): @pytest.mark.skip(reason="TODO: next PR") def test_create_jobstatus_from_task(): from simcore_service_api_server.models.schemas.jobs import JobStatus - from simcore_service_api_server.services.director_v2 import ComputationTaskGet + from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet task = ComputationTaskGet.model_validate({}) # TODO: job_status: JobStatus = create_jobstatus_from_task(task) diff --git a/services/api-server/tests/unit/test_services_solver_job_outputs.py b/services/api-server/tests/unit/test_services_solver_job_outputs.py index b02022e5daa..01612e197e5 100644 --- a/services/api-server/tests/unit/test_services_solver_job_outputs.py +++ b/services/api-server/tests/unit/test_services_solver_job_outputs.py @@ -6,7 +6,7 @@ from typing import Union, get_args, get_origin from simcore_service_api_server.models.schemas.jobs import ArgumentTypes, File -from simcore_service_api_server.services.solver_job_outputs import ( +from simcore_service_api_server.services_http.solver_job_outputs import ( BaseFileLink, ResultsTypes, ) From deed91a9bf021803aaae7cd5fae5bc715c57d919 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 06:59:02 +0100 Subject: [PATCH 31/51] start developing an webapi rpc client --- .../api/routes/licensed_items.py | 5 +++ .../models/schemas/model_adapter.py | 21 ++++++++++++ .../services_rpc/wb_api_server.py | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 8574be2a1b0..5f40440fa8a 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -6,7 +6,9 @@ from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( get_licensed_items as _get_licensed_items, ) +from simcore_service_api_server.api.dependencies.authentication import get_product_name from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client +from simcore_service_api_server.models.pagination import PaginationParams router = APIRouter() @@ -16,8 +18,11 @@ ) async def get_licensed_items( wallet_id: int, + page_params: Annotated[PaginationParams, Depends()], webserver_rpc_api: Annotated[RabbitMQRPCClient, Depends(get_rabbitmq_rpc_client)], + product_name: Annotated[str, Depends(get_product_name)], ) -> LicensedItemGetPage: return await _get_licensed_items( rabbitmq_rpc_client=webserver_rpc_api, + product_name=product_name, ) diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 3b88cd82ef7..17a76df3ff8 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -7,6 +7,9 @@ from models_library.api_schemas_api_server.pricing_plans import ( ServicePricingPlanGet as _ServicePricingPlanGet, ) +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGet as _LicensedItemGet, +) from models_library.api_schemas_webserver.product import ( GetCreditPrice as _GetCreditPrice, ) @@ -18,6 +21,7 @@ ) from models_library.basic_types import IDStr, NonNegativeDecimal from models_library.groups import GroupID +from models_library.licensed_items import LicensedItemID, LicensedResourceType from models_library.resource_tracker import ( PricingPlanClassification, PricingPlanId, @@ -121,3 +125,20 @@ class ServicePricingPlanGetLegacy(BaseModel): assert set(ServicePricingPlanGetLegacy.model_fields.keys()) == set( _ServicePricingPlanGet.model_fields.keys() ) + + +class LicensedItemGet(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + licensed_item_id: LicensedItemID + name: str + licensed_resource_type: LicensedResourceType + pricing_plan_id: PricingPlanId + created_at: datetime + modified_at: datetime + + +assert set(LicensedItemGet.model_fields.keys()) == set( + _LicensedItemGet.model_fields.keys() +) diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py new file mode 100644 index 00000000000..8988185b5ac --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from fastapi_pagination import Page, create_page +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + get_licensed_items as _get_licensed_items, +) +from simcore_service_api_server.models.pagination import PaginationParams + +from ..models.schemas.model_adapter import LicensedItemGet + + +@dataclass +class WbApiRpcClient: + _rabbitmq_rpc_client: RabbitMQRPCClient + + async def get_licensed_items( + self, product_name: str, page_params: PaginationParams + ) -> Page[LicensedItemGet]: + licensed_items_page = await _get_licensed_items( + rabbitmq_rpc_client=self._rabbitmq_rpc_client, + product_name=product_name, + offset=page_params.offset, + limit=page_params.limit, + ) + return create_page( + [ + LicensedItemGet.model_validate(elm.model_dump()) + for elm in licensed_items_page.items + ], + total=licensed_items_page.total, + params=page_params, + ) From 6d220d3924302bdac4a003101a7cfa4c3ef94a47 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 07:19:08 +0100 Subject: [PATCH 32/51] webserver.py -> webserver_http.py --- .../api/dependencies/{webserver.py => webserver_http.py} | 0 .../src/simcore_service_api_server/api/routes/credits.py | 2 +- .../src/simcore_service_api_server/api/routes/solvers.py | 2 +- .../simcore_service_api_server/api/routes/solvers_jobs.py | 2 +- .../src/simcore_service_api_server/api/routes/studies.py | 2 +- .../simcore_service_api_server/api/routes/studies_jobs.py | 2 +- .../src/simcore_service_api_server/api/routes/users.py | 2 +- .../src/simcore_service_api_server/api/routes/wallets.py | 2 +- .../src/simcore_service_api_server/core/application.py | 3 +++ .../src/simcore_service_api_server/services_http/jobs.py | 2 +- .../services_rpc/wb_api_server.py | 7 +++++++ 11 files changed, 18 insertions(+), 8 deletions(-) rename services/api-server/src/simcore_service_api_server/api/dependencies/{webserver.py => webserver_http.py} (100%) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_http.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py rename to services/api-server/src/simcore_service_api_server/api/dependencies/webserver_http.py diff --git a/services/api-server/src/simcore_service_api_server/api/routes/credits.py b/services/api-server/src/simcore_service_api_server/api/routes/credits.py index 5b5258cfb01..36069b54988 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/credits.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/credits.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from ...models.schemas.model_adapter import GetCreditPriceLegacy -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION router = APIRouter() diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py index 2ef1cea93e6..f9f80b37c4c 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py @@ -17,7 +17,7 @@ from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._common import API_SERVER_DEV_FEATURES_ENABLED from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py index a7a893ca244..609bf0b56a6 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py @@ -37,7 +37,7 @@ from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import ( FMSG_CHANGELOG_ADDED_IN_VERSION, FMSG_CHANGELOG_CHANGED_IN_VERSION, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies.py b/services/api-server/src/simcore_service_api_server/api/routes/studies.py index 77d273d8233..f6483ea6531 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies.py @@ -14,7 +14,7 @@ from ...models.schemas.errors import ErrorGet from ...models.schemas.studies import Study, StudyID, StudyPort from ...services_http.webserver import AuthSession -from ..dependencies.webserver import get_webserver_session +from ..dependencies.webserver_http import get_webserver_session _logger = logging.getLogger(__name__) router = APIRouter() diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index 11755e93640..f1e5513414b 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -18,7 +18,6 @@ from ...api.dependencies.authentication import get_current_user_id from ...api.dependencies.services import get_api_client -from ...api.dependencies.webserver import get_webserver_session from ...exceptions.backend_errors import ProjectAlreadyStartedError from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet @@ -48,6 +47,7 @@ ) from ...services_http.webserver import AuthSession from ..dependencies.application import get_reverse_url_mapper +from ..dependencies.webserver_http import get_webserver_session from ._common import API_SERVER_DEV_FEATURES_ENABLED from ._constants import FMSG_CHANGELOG_CHANGED_IN_VERSION, FMSG_CHANGELOG_NEW_IN_VERSION from .solvers_jobs import JOBS_STATUS_CODES diff --git a/services/api-server/src/simcore_service_api_server/api/routes/users.py b/services/api-server/src/simcore_service_api_server/api/routes/users.py index 14954a45285..1aee57c4648 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/users.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/users.py @@ -7,7 +7,7 @@ from ...models.schemas.errors import ErrorGet from ...models.schemas.profiles import Profile, ProfileUpdate from ...services_http.webserver import AuthSession -from ..dependencies.webserver import get_webserver_session +from ..dependencies.webserver_http import get_webserver_session _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py index 40e2233fce0..ca34726eae3 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py @@ -6,7 +6,7 @@ from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.schemas.errors import ErrorGet from ...models.schemas.model_adapter import WalletGetWithAvailableCreditsLegacy -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/core/application.py b/services/api-server/src/simcore_service_api_server/core/application.py index 4de017698a6..847570db3d6 100644 --- a/services/api-server/src/simcore_service_api_server/core/application.py +++ b/services/api-server/src/simcore_service_api_server/core/application.py @@ -7,6 +7,7 @@ from servicelib.fastapi.profiler_middleware import ProfilerMiddleware from servicelib.fastapi.tracing import setup_tracing from servicelib.logging_utils import config_all_loggers +from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client from .. import exceptions from .._meta import API_VERSION, API_VTAG, APP_NAME @@ -14,6 +15,7 @@ from ..api.routes.health import router as health_router from ..services_http import catalog, director_v2, storage, webserver from ..services_http.rabbitmq import setup_rabbitmq +from ..services_rpc import wb_api_server from ._prometheus_instrumentation import setup_prometheus_instrumentation from .events import create_start_app_handler, create_stop_app_handler from .openapi import override_openapi_method, use_route_names_as_operation_ids @@ -92,6 +94,7 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI: settings.API_SERVER_WEBSERVER, tracing_settings=settings.API_SERVER_TRACING, ) + wb_api_server.setup(app, get_rabbitmq_rpc_client(app)) if settings.API_SERVER_CATALOG: catalog.setup( diff --git a/services/api-server/src/simcore_service_api_server/services_http/jobs.py b/services/api-server/src/simcore_service_api_server/services_http/jobs.py index 277f9625f17..ed2ef50d588 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/jobs.py +++ b/services/api-server/src/simcore_service_api_server/services_http/jobs.py @@ -9,7 +9,7 @@ from ..api.dependencies.authentication import get_current_user_id from ..api.dependencies.services import get_api_client -from ..api.dependencies.webserver import get_webserver_session +from ..api.dependencies.webserver_http import get_webserver_session from ..models.schemas.jobs import ( JobID, JobMetadata, diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 8988185b5ac..33bd4c65ab6 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -1,5 +1,6 @@ from dataclasses import dataclass +from fastapi import FastAPI from fastapi_pagination import Page, create_page from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( @@ -31,3 +32,9 @@ async def get_licensed_items( total=licensed_items_page.total, params=page_params, ) + + +def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): + app.state.wb_api_rpc_client = WbApiRpcClient( + _rabbitmq_rpc_client=rabbitmq_rmp_client + ) From a027ecfdba699e0e71f4d6334394c00d62eac48d Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 07:26:11 +0100 Subject: [PATCH 33/51] connect pieces to expose licensed items --- .../api/dependencies/webserver_rpc.py | 7 +++++++ .../api/routes/licensed_items.py | 20 ++++++++----------- .../api/routes/solvers_jobs_getters.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py new file mode 100644 index 00000000000..8fdecd651e9 --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI +from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient + + +def get_wb_api_rpc_client(app: FastAPI) -> WbApiRpcClient: + assert app.state.wb_api_rpc_client # nosec + return app.state.wb_api_rpc_client diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 5f40440fa8a..2f593f1b5ff 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -2,13 +2,11 @@ from fastapi import APIRouter, Depends from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage -from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient -from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( - get_licensed_items as _get_licensed_items, -) -from simcore_service_api_server.api.dependencies.authentication import get_product_name -from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client -from simcore_service_api_server.models.pagination import PaginationParams + +from ...api.dependencies.authentication import get_product_name +from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client +from ...models.pagination import PaginationParams +from ...services_rpc.wb_api_server import WbApiRpcClient router = APIRouter() @@ -17,12 +15,10 @@ "/", response_model=LicensedItemGetPage, description="Get all licensed items" ) async def get_licensed_items( - wallet_id: int, page_params: Annotated[PaginationParams, Depends()], - webserver_rpc_api: Annotated[RabbitMQRPCClient, Depends(get_rabbitmq_rpc_client)], + web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], product_name: Annotated[str, Depends(get_product_name)], ) -> LicensedItemGetPage: - return await _get_licensed_items( - rabbitmq_rpc_client=webserver_rpc_api, - product_name=product_name, + return await web_api_rpc.get_licensed_items( + product_name=product_name, page_params=page_params ) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py index 6b863ebb53a..3c2bd479b0a 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py @@ -53,7 +53,7 @@ from ..dependencies.database import Engine, get_db_engine from ..dependencies.rabbitmq import get_log_check_timeout, get_log_distributor from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION from .solvers_jobs import ( JOBS_STATUS_CODES, From cefc23e075609726a8159f73fe491e6bc5f24d80 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 09:32:02 +0100 Subject: [PATCH 34/51] start adding test --- .../api/dependencies/webserver_rpc.py | 11 ++++-- .../api/routes/licensed_items.py | 23 +++++++++---- .../tests/unit/test_licensed_items.py | 34 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 services/api-server/tests/unit/test_licensed_items.py diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py index 8fdecd651e9..caa6fb8ea2e 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py @@ -1,7 +1,12 @@ -from fastapi import FastAPI +from typing import Annotated, cast + +from fastapi import Depends, FastAPI +from servicelib.fastapi.dependencies import get_app from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient -def get_wb_api_rpc_client(app: FastAPI) -> WbApiRpcClient: +async def get_wb_api_rpc_client( + app: Annotated[FastAPI, Depends(get_app)] +) -> WbApiRpcClient: assert app.state.wb_api_rpc_client # nosec - return app.state.wb_api_rpc_client + return cast(WbApiRpcClient, app.state.wb_api_rpc_client) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 2f593f1b5ff..30c52c3ba9c 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -1,24 +1,35 @@ -from typing import Annotated +from typing import Annotated, Any -from fastapi import APIRouter, Depends -from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from fastapi import APIRouter, Depends, status +from simcore_service_api_server.exceptions.service_errors_utils import ( + DEFAULT_BACKEND_SERVICE_STATUS_CODES, +) from ...api.dependencies.authentication import get_product_name from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client -from ...models.pagination import PaginationParams +from ...models.pagination import Page, PaginationParams +from ...models.schemas.model_adapter import LicensedItemGet from ...services_rpc.wb_api_server import WbApiRpcClient router = APIRouter() +_LICENSE_ITEMS_STATUS_CODES: dict[int | str, dict[str, Any]] = { + **DEFAULT_BACKEND_SERVICE_STATUS_CODES, +} + @router.get( - "/", response_model=LicensedItemGetPage, description="Get all licensed items" + "/page", + response_model=Page[LicensedItemGet], + status_code=status.HTTP_200_OK, + responses=_LICENSE_ITEMS_STATUS_CODES, + description="Get all licensed items", ) async def get_licensed_items( page_params: Annotated[PaginationParams, Depends()], web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], product_name: Annotated[str, Depends(get_product_name)], -) -> LicensedItemGetPage: +) -> Page[LicensedItemGet]: return await web_api_rpc.get_licensed_items( product_name=product_name, page_params=page_params ) diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py new file mode 100644 index 00000000000..ff6c651d018 --- /dev/null +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -0,0 +1,34 @@ +import pytest +from fastapi import status +from httpx import AsyncClient, BasicAuth +from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from pytest_mock import MockerFixture +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from simcore_service_api_server._meta import API_VTAG + + +@pytest.fixture +async def mock_wb_api_server_rcp(mocker: MockerFixture) -> MockerFixture: + async def _get_backend_licensed_items( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + product_name: str, + offset: int, + limit: int, + ) -> LicensedItemGetPage: + return None + + mocker.patch( + "servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items.get_licensed_items", + _get_backend_licensed_items, + ) + + return mocker + + +async def test_get_licensed_items( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + + resp = await client.get(f"{API_VTAG}/credits/price", auth=auth) + assert resp.status_code == status.HTTP_200_OK From 542b80b8ed5489f9aff553c5b0e3de500c1c6dfc Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 10:11:42 +0100 Subject: [PATCH 35/51] mock backend with pydantic model example --- .../api_schemas_webserver/licensed_items.py | 16 +++++- .../tests/unit/test_licensed_items.py | 50 +++++++++++++++---- 2 files changed, 56 insertions(+), 10 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 5c170588856..3455e8a81ac 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 @@ -3,7 +3,7 @@ from models_library.licensed_items import LicensedItemID, LicensedResourceType from models_library.resource_tracker import PricingPlanId -from pydantic import PositiveInt +from pydantic import ConfigDict, PositiveInt from ._base import OutputSchema @@ -15,6 +15,20 @@ class LicensedItemGet(OutputSchema): pricing_plan_id: PricingPlanId created_at: datetime modified_at: datetime + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + "licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1", + "name": "best-model", + "licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}", + "pricing_plan_id": "15", + "created_at": "2024-12-12 09:59:26.422140", + "modified_at": "2024-12-12 09:59:26.422140", + } + ] + } + ) class LicensedItemGetPage(NamedTuple): diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index ff6c651d018..1c48e541912 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -1,34 +1,66 @@ import pytest from fastapi import status from httpx import AsyncClient, BasicAuth -from models_library.api_schemas_webserver.licensed_items import LicensedItemGetPage +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGet as _LicensedItemGet, +) +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGetPage as _LicensedItemGetPage, +) +from pydantic import TypeAdapter from pytest_mock import MockerFixture from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from simcore_service_api_server._meta import API_VTAG +from simcore_service_api_server.models.pagination import Page +from simcore_service_api_server.models.schemas.model_adapter import LicensedItemGet @pytest.fixture -async def mock_wb_api_server_rcp(mocker: MockerFixture) -> MockerFixture: +async def mock_rabbitmq_rpc_client(mocker: MockerFixture) -> MockerFixture: + class DummyRabbitMqRpcClient: + pass + + def _get_dummy_rpc_client(): + return DummyRabbitMqRpcClient + + mocker.patch( + "simcore_service_api_server.core.application.get_rabbitmq_rpc_client", + sideeffect=_get_dummy_rpc_client, + ) + return mocker + + +@pytest.fixture +async def mock_wb_api_server_rcp( + mock_rabbitmq_rpc_client: MockerFixture, +) -> MockerFixture: async def _get_backend_licensed_items( rabbitmq_rpc_client: RabbitMQRPCClient, *, product_name: str, offset: int, limit: int, - ) -> LicensedItemGetPage: - return None + ) -> _LicensedItemGetPage: + extra = _LicensedItemGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + return _LicensedItemGetPage( + items=[_LicensedItemGet.model_validate(ex) for ex in examples], + total=len(examples), + ) - mocker.patch( - "servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items.get_licensed_items", + mock_rabbitmq_rpc_client.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", _get_backend_licensed_items, ) - return mocker + return mock_rabbitmq_rpc_client async def test_get_licensed_items( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): - - resp = await client.get(f"{API_VTAG}/credits/price", auth=auth) + resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) assert resp.status_code == status.HTTP_200_OK + TypeAdapter(Page[LicensedItemGet]).validate_json(resp.text) From 51ef078dde652d4752bb364f938682d2b1a9380b Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 10:58:20 +0100 Subject: [PATCH 36/51] require input input names in exception mapper --- .../exceptions/service_errors_utils.py | 1 + .../services_http/catalog.py | 14 +++-- .../services_http/director_v2.py | 16 ++--- .../services_http/storage.py | 14 ++--- .../services_http/webserver.py | 62 ++++++++++++------- 5 files changed, 66 insertions(+), 41 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index 39167c92040..1e6a41bf992 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -128,6 +128,7 @@ def service_exception_handler( def service_exception_mapper( + *, service_name: str, http_status_map: HttpStatusMap, ) -> Callable[ diff --git a/services/api-server/src/simcore_service_api_server/services_http/catalog.py b/services/api-server/src/simcore_service_api_server/services_http/catalog.py index 34f092a6191..6f6ace38f1c 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/catalog.py +++ b/services/api-server/src/simcore_service_api_server/services_http/catalog.py @@ -68,7 +68,7 @@ def to_solver(self) -> Solver: # - Error handling: What do we reraise, suppress, transform??? # -_exception_mapper = partial(service_exception_mapper, "Catalog") +_exception_mapper = partial(service_exception_mapper, service_name="Catalog") TruncatedCatalogServiceOutAdapter: Final[ TypeAdapter[TruncatedCatalogServiceOut] @@ -91,7 +91,9 @@ class CatalogApi(BaseServiceClientApi): SEE osparc-simcore/services/catalog/openapi.json """ - @_exception_mapper({status.HTTP_404_NOT_FOUND: ListSolversOrStudiesError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ListSolversOrStudiesError} + ) async def list_solvers( self, *, @@ -134,7 +136,9 @@ async def list_solvers( ) return solvers - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError} + ) async def get_service( self, *, user_id: int, name: SolverKeyId, version: VersionStr, product_name: str ) -> Solver: @@ -163,7 +167,9 @@ async def get_service( solver: Solver = service.to_solver() return solver - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError} + ) async def get_service_ports( self, *, user_id: int, name: SolverKeyId, version: VersionStr, product_name: str ): diff --git a/services/api-server/src/simcore_service_api_server/services_http/director_v2.py b/services/api-server/src/simcore_service_api_server/services_http/director_v2.py index 45f42af73eb..5a40fae7ca4 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/director_v2.py +++ b/services/api-server/src/simcore_service_api_server/services_http/director_v2.py @@ -60,11 +60,11 @@ class TaskLogFileGet(BaseModel): # API CLASS --------------------------------------------- -_exception_mapper = partial(service_exception_mapper, "Director V2") +_exception_mapper = partial(service_exception_mapper, service_name="Director V2") class DirectorV2Api(BaseServiceClientApi): - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_computation( self, *, @@ -85,7 +85,7 @@ async def create_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def start_computation( self, *, @@ -115,7 +115,7 @@ async def start_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def get_computation( self, *, project_id: UUID, user_id: PositiveInt ) -> ComputationTaskGet: @@ -129,7 +129,7 @@ async def get_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def stop_computation( self, *, project_id: UUID, user_id: PositiveInt ) -> ComputationTaskGet: @@ -143,7 +143,7 @@ async def stop_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def delete_computation(self, *, project_id: UUID, user_id: PositiveInt): response = await self.client.request( "DELETE", @@ -155,7 +155,9 @@ async def delete_computation(self, *, project_id: UUID, user_id: PositiveInt): ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: LogFileNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: LogFileNotFoundError} + ) async def get_computation_logs( self, *, user_id: PositiveInt, project_id: UUID ) -> JobLogsMap: diff --git a/services/api-server/src/simcore_service_api_server/services_http/storage.py b/services/api-server/src/simcore_service_api_server/services_http/storage.py index 0095dd343f5..aa8b724ce98 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/storage.py +++ b/services/api-server/src/simcore_service_api_server/services_http/storage.py @@ -24,7 +24,7 @@ _logger = logging.getLogger(__name__) -_exception_mapper = partial(service_exception_mapper, "Storage") +_exception_mapper = partial(service_exception_mapper, service_name="Storage") _FILE_ID_PATTERN = re.compile(r"^api\/(?P[\w-]+)\/(?P.+)$") AccessRight = Literal["read", "write"] @@ -54,7 +54,7 @@ class StorageApi(BaseServiceClientApi): # SIMCORE_S3_ID = 0 - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def list_files( self, *, @@ -81,7 +81,7 @@ async def list_files( ) return files - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def search_owned_files( self, *, @@ -119,7 +119,7 @@ async def search_owned_files( assert len(files) <= limit if limit else True # nosec return files - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_download_link( self, *, user_id: int, file_id: UUID, file_name: str ) -> AnyUrl: @@ -138,7 +138,7 @@ async def get_download_link( link: AnyUrl = presigned_link.link return link - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def delete_file(self, *, user_id: int, quoted_storage_file_id: str) -> None: response = await self.client.delete( f"/locations/{self.SIMCORE_S3_ID}/files/{quoted_storage_file_id}", @@ -146,7 +146,7 @@ async def delete_file(self, *, user_id: int, quoted_storage_file_id: str) -> Non ) response.raise_for_status() - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_upload_links( self, *, user_id: int, file_id: UUID, file_name: str ) -> FileUploadSchema: @@ -183,7 +183,7 @@ async def create_abort_upload_link( url = url.include_query_params(**query) return url - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_soft_link( self, *, user_id: int, target_s3_path: str, as_file_id: UUID ) -> File: diff --git a/services/api-server/src/simcore_service_api_server/services_http/webserver.py b/services/api-server/src/simcore_service_api_server/services_http/webserver.py index c7d5680eb37..2a88d8783b3 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/webserver.py +++ b/services/api-server/src/simcore_service_api_server/services_http/webserver.py @@ -88,7 +88,7 @@ _logger = logging.getLogger(__name__) -_exception_mapper = partial(service_exception_mapper, "Webserver") +_exception_mapper = partial(service_exception_mapper, service_name="Webserver") _JOB_STATUS_MAP = { status.HTTP_402_PAYMENT_REQUIRED: PaymentRequiredError, @@ -243,7 +243,7 @@ async def _wait_for_long_running_task_results(self, lrt_response: httpx.Response # PROFILE -------------------------------------------------- - @_exception_mapper(_PROFILE_STATUS_MAP) + @_exception_mapper(http_status_map=_PROFILE_STATUS_MAP) async def get_me(self) -> Profile: response = await self.client.get("/me", cookies=self.session_cookies) response.raise_for_status() @@ -263,7 +263,7 @@ async def get_me(self) -> Profile: gravatar_id=got.gravatar_id, ) - @_exception_mapper(_PROFILE_STATUS_MAP) + @_exception_mapper(http_status_map=_PROFILE_STATUS_MAP) async def update_me(self, *, profile_update: ProfileUpdate) -> Profile: update = WebProfileUpdate.model_construct( @@ -283,7 +283,7 @@ async def update_me(self, *, profile_update: ProfileUpdate) -> Profile: # PROJECTS ------------------------------------------------- - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_project( self, project: ProjectCreateNew, @@ -308,7 +308,7 @@ async def create_project( result = await self._wait_for_long_running_task_results(response) return ProjectGet.model_validate(result) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def clone_project( self, *, @@ -333,7 +333,7 @@ async def clone_project( result = await self._wait_for_long_running_task_results(response) return ProjectGet.model_validate(result) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def get_project(self, *, project_id: UUID) -> ProjectGet: response = await self.client.get( f"/projects/{project_id}", @@ -363,7 +363,7 @@ async def get_projects_page(self, *, limit: int, offset: int): show_hidden=False, ) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def delete_project(self, *, project_id: ProjectID) -> None: response = await self.client.delete( f"/projects/{project_id}", @@ -371,7 +371,9 @@ async def delete_project(self, *, project_id: ProjectID) -> None: ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectPortsNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectPortsNotFoundError} + ) async def get_project_metadata_ports( self, *, project_id: ProjectID ) -> list[StudyPort]: @@ -389,7 +391,9 @@ async def get_project_metadata_ports( assert isinstance(data, list) # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError} + ) async def get_project_metadata( self, *, project_id: ProjectID ) -> ProjectMetadataGet: @@ -402,7 +406,7 @@ async def get_project_metadata( assert data is not None # nosec return data - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def patch_project(self, *, project_id: UUID, patch_params: ProjectPatch): response = await self.client.patch( f"/projects/{project_id}", @@ -411,7 +415,9 @@ async def patch_project(self, *, project_id: UUID, patch_params: ProjectPatch): ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError} + ) async def update_project_metadata( self, *, project_id: ProjectID, metadata: dict[str, MetaValueType] ) -> ProjectMetadataGet: @@ -425,7 +431,9 @@ async def update_project_metadata( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError} + ) async def get_project_node_pricing_unit( self, *, project_id: UUID, node_id: UUID ) -> PricingUnitGetLegacy: @@ -439,7 +447,9 @@ async def get_project_node_pricing_unit( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError} + ) async def connect_pricing_unit_to_project_node( self, *, @@ -455,7 +465,7 @@ async def connect_pricing_unit_to_project_node( response.raise_for_status() @_exception_mapper( - _JOB_STATUS_MAP + http_status_map=_JOB_STATUS_MAP | { status.HTTP_409_CONFLICT: ProjectAlreadyStartedError, status.HTTP_406_NOT_ACCEPTABLE: ClusterNotFoundError, @@ -477,7 +487,7 @@ async def start_project( ) response.raise_for_status() - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def update_project_inputs( self, *, @@ -498,7 +508,7 @@ async def update_project_inputs( assert data is not None # nosec return data - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_project_inputs( self, *, project_id: ProjectID ) -> dict[NodeID, ProjectInputGet]: @@ -517,7 +527,9 @@ async def get_project_inputs( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOutputNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOutputNotFoundError} + ) async def get_project_outputs( self, *, project_id: ProjectID ) -> dict[NodeID, dict[str, Any]]: @@ -536,7 +548,7 @@ async def get_project_outputs( assert data is not None # nosec return data - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def update_node_outputs( self, *, project_id: UUID, node_id: UUID, new_node_outputs: NodeOutputs ) -> None: @@ -549,7 +561,7 @@ async def update_node_outputs( # WALLETS ------------------------------------------------- - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_default_wallet(self) -> WalletGetWithAvailableCreditsLegacy: response = await self.client.get( "/wallets/default", @@ -564,7 +576,7 @@ async def get_default_wallet(self) -> WalletGetWithAvailableCreditsLegacy: assert data is not None # nosec return data - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_wallet( self, *, wallet_id: int ) -> WalletGetWithAvailableCreditsLegacy: @@ -581,7 +593,7 @@ async def get_wallet( assert data is not None # nosec return data - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_project_wallet(self, *, project_id: ProjectID) -> WalletGet: response = await self.client.get( f"/projects/{project_id}/wallet", @@ -594,7 +606,9 @@ async def get_project_wallet(self, *, project_id: ProjectID) -> WalletGet: # PRODUCTS ------------------------------------------------- - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProductPriceNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProductPriceNotFoundError} + ) async def get_product_price(self) -> GetCreditPriceLegacy: response = await self.client.get( "/credits-price", @@ -607,7 +621,9 @@ async def get_product_price(self) -> GetCreditPriceLegacy: # SERVICES ------------------------------------------------- - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingPlanNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingPlanNotFoundError} + ) async def get_service_pricing_plan( self, *, solver_key: SolverKeyId, version: VersionStr ) -> ServicePricingPlanGet | None: From 044199e76fbd50d57ba882b3d49f15c290c83b53 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 12:08:02 +0100 Subject: [PATCH 37/51] handle rpc client exceptions --- .../exceptions/service_errors_utils.py | 50 ++++++++++++++++--- .../services_rpc/wb_api_server.py | 7 +++ .../tests/unit/test_licensed_items.py | 27 +++++++++- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index 1e6a41bf992..45fe88c19fe 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -1,3 +1,4 @@ +import asyncio import logging from collections.abc import Callable, Coroutine, Mapping from contextlib import contextmanager @@ -8,6 +9,7 @@ import httpx from fastapi import HTTPException, status from pydantic import ValidationError +from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError from ..models.schemas.errors import ErrorGet @@ -50,8 +52,12 @@ class ToApiTuple(NamedTuple): # service to public-api status maps -E = TypeVar("E", bound=BaseBackEndError) -HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, E] +BackEndErrorType = TypeVar("BackEndErrorType", bound=BaseBackEndError) +RpcExceptionType = TypeVar( + "RpcExceptionType", bound=Exception +) # need more specific rpc exception base class +HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, BackEndErrorType] +RabbitMqRpcExceptionMap: TypeAlias = Mapping[RpcExceptionType, BackEndErrorType] def _get_http_exception_kwargs( @@ -98,6 +104,7 @@ def _get_http_exception_kwargs( def service_exception_handler( service_name: str, http_status_map: HttpStatusMap, + rpc_exception_map: RabbitMqRpcExceptionMap, **context, ): status_code: int @@ -126,21 +133,49 @@ def service_exception_handler( status_code=status_code, detail=detail, headers=headers ) from exc + except BaseException as exc: # currently no baseclass for rpc errors + if ( + type(exc) == asyncio.TimeoutError + ): # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Request to backend timed out", + ) from exc + if type(exc) in { + asyncio.exceptions.CancelledError, + RuntimeError, + RemoteMethodNotRegisteredError, + }: # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, detail="Request to failed" + ) from exc + if backend_error_type := rpc_exception_map.get(type(exc)): + raise backend_error_type(**context) from exc + raise + def service_exception_mapper( *, service_name: str, - http_status_map: HttpStatusMap, + http_status_map: HttpStatusMap = {}, + rpc_exception_map: RabbitMqRpcExceptionMap = {}, ) -> Callable[ [Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]], Callable[Concatenate[Self, P], Coroutine[Any, Any, R]], ]: def _decorator(member_func: Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]): - _assert_correct_kwargs(func=member_func, status_map=http_status_map) + _assert_correct_kwargs( + func=member_func, + exception_types=set(http_status_map.values()).union( + set(rpc_exception_map.values()) + ), + ) @wraps(member_func) async def _wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> R: - with service_exception_handler(service_name, http_status_map, **kwargs): + with service_exception_handler( + service_name, http_status_map, rpc_exception_map, **kwargs + ): return await member_func(self, *args, **kwargs) return _wrapper @@ -148,13 +183,14 @@ async def _wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> R: return _decorator -def _assert_correct_kwargs(func: Callable, status_map: HttpStatusMap): +def _assert_correct_kwargs(func: Callable, exception_types: set[BackEndErrorType]): _required_kwargs = { name for name, param in signature(func).parameters.items() if param.kind == param.KEYWORD_ONLY } - for exc_type in status_map.values(): + for exc_type in exception_types: + assert isinstance(exc_type, type) # nosec _exception_inputs = exc_type.named_fields() assert _exception_inputs.issubset( _required_kwargs diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 33bd4c65ab6..636db5a4497 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from functools import partial from fastapi import FastAPI from fastapi_pagination import Page, create_page @@ -6,15 +7,21 @@ from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( get_licensed_items as _get_licensed_items, ) +from simcore_service_api_server.exceptions.service_errors_utils import ( + service_exception_mapper, +) from simcore_service_api_server.models.pagination import PaginationParams from ..models.schemas.model_adapter import LicensedItemGet +_exception_mapper = partial(service_exception_mapper, service_name="WebApiServer") + @dataclass class WbApiRpcClient: _rabbitmq_rpc_client: RabbitMQRPCClient + @_exception_mapper(rpc_exception_map={}) async def get_licensed_items( self, product_name: str, page_params: PaginationParams ) -> Page[LicensedItemGet]: diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index 1c48e541912..b318fcc5bff 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -1,3 +1,5 @@ +import asyncio + import pytest from fastapi import status from httpx import AsyncClient, BasicAuth @@ -10,6 +12,7 @@ from pydantic import TypeAdapter from pytest_mock import MockerFixture from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError from simcore_service_api_server._meta import API_VTAG from simcore_service_api_server.models.pagination import Page from simcore_service_api_server.models.schemas.model_adapter import LicensedItemGet @@ -32,7 +35,7 @@ def _get_dummy_rpc_client(): @pytest.fixture async def mock_wb_api_server_rcp( - mock_rabbitmq_rpc_client: MockerFixture, + mock_rabbitmq_rpc_client: MockerFixture, exception_to_raise: Exception | None ) -> MockerFixture: async def _get_backend_licensed_items( rabbitmq_rpc_client: RabbitMQRPCClient, @@ -41,6 +44,8 @@ async def _get_backend_licensed_items( offset: int, limit: int, ) -> _LicensedItemGetPage: + if exception_to_raise is not None: + raise exception_to_raise extra = _LicensedItemGet.model_config.get("json_schema_extra") assert isinstance(extra, dict) examples = extra.get("examples") @@ -58,9 +63,29 @@ async def _get_backend_licensed_items( return mock_rabbitmq_rpc_client +@pytest.mark.parametrize("exception_to_raise", [None]) async def test_get_licensed_items( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) assert resp.status_code == status.HTTP_200_OK TypeAdapter(Page[LicensedItemGet]).validate_json(resp.text) + + +@pytest.mark.parametrize("exception_to_raise", [asyncio.TimeoutError()]) +async def test_get_licensed_items_timeout( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) + assert resp.status_code == status.HTTP_504_GATEWAY_TIMEOUT + + +@pytest.mark.parametrize( + "exception_to_raise", + [asyncio.CancelledError(), RuntimeError(), RemoteMethodNotRegisteredError()], +) +async def test_get_licensed_items_502( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) + assert resp.status_code == status.HTTP_502_BAD_GATEWAY From 3577938ce4341730641e00fe9dcf139c7aef446c Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 12:13:39 +0100 Subject: [PATCH 38/51] update openapi spes --- services/api-server/openapi.json | 220 +++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index d70ef50e6bc..3faeaa58e7f 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -5348,6 +5348,118 @@ } ] } + }, + "/v0/licensed-items/page": { + "get": { + "tags": [ + "licensed-items" + ], + "summary": "Get Licensed Items", + "description": "Get all licensed items", + "operationId": "get_licensed_items", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_LicensedItemGet_" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -6086,6 +6198,55 @@ "submitted_at": "2021-04-01 07:15:54.631007" } }, + "LicensedItemGet": { + "properties": { + "licensed_item_id": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "licensed_resource_type": { + "$ref": "#/components/schemas/LicensedResourceType" + }, + "pricing_plan_id": { + "type": "integer", + "exclusiveMinimum": true, + "title": "Pricing Plan Id", + "minimum": 0 + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + } + }, + "type": "object", + "required": [ + "licensed_item_id", + "name", + "licensed_resource_type", + "pricing_plan_id", + "created_at", + "modified_at" + ], + "title": "LicensedItemGet" + }, + "LicensedResourceType": { + "type": "string", + "enum": [ + "VIP_MODEL" + ], + "title": "LicensedResourceType" + }, "Links": { "properties": { "first": { @@ -6409,6 +6570,65 @@ ], "title": "Page[Job]" }, + "Page_LicensedItemGet_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/LicensedItemGet" + }, + "type": "array", + "title": "Items" + }, + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" + }, + "limit": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Limit" + }, + "offset": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Offset" + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "type": "object", + "required": [ + "items", + "total", + "limit", + "offset", + "links" + ], + "title": "Page[LicensedItemGet]" + }, "Page_Study_": { "properties": { "items": { From 0bd8e4fb535e904a26cd4e6b6991d960bbd705af Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 12:25:00 +0100 Subject: [PATCH 39/51] fix import --- .../api/dependencies/webserver_rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py index caa6fb8ea2e..df47e0a5be9 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py @@ -2,7 +2,8 @@ from fastapi import Depends, FastAPI from servicelib.fastapi.dependencies import get_app -from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient + +from ...services_rpc.wb_api_server import WbApiRpcClient async def get_wb_api_rpc_client( From 582fb9b7a407981e1e345afa760c78e5d2263b6e Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 12:31:52 +0100 Subject: [PATCH 40/51] ensure relative imports --- .../api/routes/licensed_items.py | 4 +--- .../api/routes/studies.py | 4 +--- .../core/application.py | 2 +- .../exceptions/handlers/__init__.py | 2 +- .../handlers/_handlers_backend_errors.py | 2 +- .../exceptions/service_errors_utils.py | 2 +- .../src/simcore_service_api_server/main.py | 3 ++- .../models/schemas/meta.py | 3 ++- .../models/schemas/solvers.py | 2 +- .../models/schemas/studies.py | 2 +- .../services_http/catalog.py | 4 ++-- .../services_http/rabbitmq.py | 2 +- .../services_http/solver_job_outputs.py | 5 ++--- .../services_http/webserver.py | 21 ++++++++++--------- .../services_rpc/wb_api_server.py | 6 ++---- 15 files changed, 30 insertions(+), 34 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 30c52c3ba9c..ddf4ae3363e 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -1,12 +1,10 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, status -from simcore_service_api_server.exceptions.service_errors_utils import ( - DEFAULT_BACKEND_SERVICE_STATUS_CODES, -) from ...api.dependencies.authentication import get_product_name from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client +from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.pagination import Page, PaginationParams from ...models.schemas.model_adapter import LicensedItemGet from ...services_rpc.wb_api_server import WbApiRpcClient diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies.py b/services/api-server/src/simcore_service_api_server/api/routes/studies.py index f6483ea6531..d5f3e2c821e 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies.py @@ -6,10 +6,8 @@ from models_library.api_schemas_webserver.projects import ProjectGet from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from simcore_service_api_server.api.routes._constants import ( - FMSG_CHANGELOG_NEW_IN_VERSION, -) +from ...api.routes._constants import FMSG_CHANGELOG_NEW_IN_VERSION from ...models.pagination import OnePage, Page, PaginationParams from ...models.schemas.errors import ErrorGet from ...models.schemas.studies import Study, StudyID, StudyPort diff --git a/services/api-server/src/simcore_service_api_server/core/application.py b/services/api-server/src/simcore_service_api_server/core/application.py index 847570db3d6..571b98c3c6a 100644 --- a/services/api-server/src/simcore_service_api_server/core/application.py +++ b/services/api-server/src/simcore_service_api_server/core/application.py @@ -7,10 +7,10 @@ from servicelib.fastapi.profiler_middleware import ProfilerMiddleware from servicelib.fastapi.tracing import setup_tracing from servicelib.logging_utils import config_all_loggers -from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client from .. import exceptions from .._meta import API_VERSION, API_VTAG, APP_NAME +from ..api.dependencies.rabbitmq import get_rabbitmq_rpc_client from ..api.root import create_router from ..api.routes.health import router as health_router from ..services_http import catalog, director_v2, storage, webserver diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py index d2779338980..91c4e0d9ccf 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py @@ -1,11 +1,11 @@ from fastapi import FastAPI from fastapi.exceptions import RequestValidationError from httpx import HTTPError as HttpxException -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError from starlette import status from starlette.exceptions import HTTPException from ..._constants import MSG_INTERNAL_ERROR_USER_FRIENDLY_TEMPLATE +from ...exceptions.backend_errors import BaseBackEndError from ..custom_errors import CustomBaseError from ..log_streaming_errors import LogStreamingBaseError from ._custom_errors import custom_error_handler diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py index e46d0f8f977..ca335deceac 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py @@ -1,7 +1,7 @@ -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError from starlette.requests import Request from starlette.responses import JSONResponse +from ...exceptions.backend_errors import BaseBackEndError from ._utils import create_error_json_response diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index 45fe88c19fe..6407d89785f 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -10,8 +10,8 @@ from fastapi import HTTPException, status from pydantic import ValidationError from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError +from ..exceptions.backend_errors import BaseBackEndError from ..models.schemas.errors import ErrorGet _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/main.py b/services/api-server/src/simcore_service_api_server/main.py index 8b636ac4315..493874ee6eb 100644 --- a/services/api-server/src/simcore_service_api_server/main.py +++ b/services/api-server/src/simcore_service_api_server/main.py @@ -1,7 +1,8 @@ """Main application to be deployed in for example uvicorn. """ from fastapi import FastAPI -from simcore_service_api_server.core.application import init_app + +from .core.application import init_app # SINGLETON FastAPI app the_app: FastAPI = init_app() diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/meta.py b/services/api-server/src/simcore_service_api_server/models/schemas/meta.py index 9e195214ec0..4251d0f2a77 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/meta.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/meta.py @@ -2,7 +2,8 @@ from models_library.api_schemas__common.meta import BaseMeta from pydantic import ConfigDict, HttpUrl -from simcore_service_api_server.models._utils_pydantic import UriSchema + +from ...models._utils_pydantic import UriSchema class Meta(BaseMeta): diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py b/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py index 9db1e9696ad..f3be20211cd 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py @@ -7,8 +7,8 @@ from models_library.services_regex import COMPUTATIONAL_SERVICE_KEY_RE from packaging.version import Version from pydantic import BaseModel, ConfigDict, Field, HttpUrl, StringConstraints -from simcore_service_api_server.models._utils_pydantic import UriSchema +from ...models._utils_pydantic import UriSchema from ..api_resources import compose_resource_name from ..basic_types import VersionStr diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/studies.py b/services/api-server/src/simcore_service_api_server/models/schemas/studies.py index 1905477236f..e0686713508 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/studies.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/studies.py @@ -2,8 +2,8 @@ from models_library import projects, projects_nodes_io from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field -from simcore_service_api_server.models._utils_pydantic import UriSchema +from ...models._utils_pydantic import UriSchema from .. import api_resources from . import solvers diff --git a/services/api-server/src/simcore_service_api_server/services_http/catalog.py b/services/api-server/src/simcore_service_api_server/services_http/catalog.py index 6f6ace38f1c..c380025c672 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/catalog.py +++ b/services/api-server/src/simcore_service_api_server/services_http/catalog.py @@ -13,11 +13,11 @@ from pydantic import ConfigDict, TypeAdapter, ValidationError from settings_library.catalog import CatalogSettings from settings_library.tracing import TracingSettings -from simcore_service_api_server.exceptions.backend_errors import ( + +from ..exceptions.backend_errors import ( ListSolversOrStudiesError, SolverOrStudyNotFoundError, ) - from ..exceptions.service_errors_utils import service_exception_mapper from ..models.basic_types import VersionStr from ..models.schemas.solvers import LATEST_VERSION, Solver, SolverKeyId, SolverPort diff --git a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py index a0ad4801048..30c116bc128 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py @@ -4,8 +4,8 @@ from servicelib.rabbitmq import RabbitMQClient, wait_till_rabbitmq_responsive from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from settings_library.rabbit import RabbitSettings -from simcore_service_api_server.core.health_checker import ApiServerHealthChecker +from ..core.health_checker import ApiServerHealthChecker from ..services_http.log_streaming import LogDistributor _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py index dac7610b5a3..5457a259f8c 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py +++ b/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py @@ -7,9 +7,8 @@ from pydantic import StrictBool, StrictFloat, StrictInt, TypeAdapter from simcore_sdk import node_ports_v2 from simcore_sdk.node_ports_v2 import DBManager, Nodeports -from simcore_service_api_server.exceptions.backend_errors import ( - SolverOutputNotFoundError, -) + +from ..exceptions.backend_errors import SolverOutputNotFoundError log = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/services_http/webserver.py b/services/api-server/src/simcore_service_api_server/services_http/webserver.py index 2a88d8783b3..79a921a7b42 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/webserver.py +++ b/services/api-server/src/simcore_service_api_server/services_http/webserver.py @@ -46,9 +46,18 @@ X_SIMCORE_PARENT_PROJECT_UUID, ) from settings_library.tracing import TracingSettings -from simcore_service_api_server.exceptions.backend_errors import ( +from tenacity import TryAgain +from tenacity.asyncio import AsyncRetrying +from tenacity.before_sleep import before_sleep_log +from tenacity.stop import stop_after_delay +from tenacity.wait import wait_fixed + +from ..core.settings import WebServerSettings +from ..exceptions.backend_errors import ( + ClusterNotFoundError, ConfigurationError, ForbiddenWalletError, + JobNotFoundError, ListJobsError, PaymentRequiredError, PricingPlanNotFoundError, @@ -61,15 +70,6 @@ SolverOutputNotFoundError, WalletNotFoundError, ) -from simcore_service_api_server.models.schemas.model_adapter import GetCreditPriceLegacy -from tenacity import TryAgain -from tenacity.asyncio import AsyncRetrying -from tenacity.before_sleep import before_sleep_log -from tenacity.stop import stop_after_delay -from tenacity.wait import wait_fixed - -from ..core.settings import WebServerSettings -from ..exceptions.backend_errors import ClusterNotFoundError, JobNotFoundError from ..exceptions.service_errors_utils import ( service_exception_handler, service_exception_mapper, @@ -78,6 +78,7 @@ from ..models.pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from ..models.schemas.jobs import MetaValueType from ..models.schemas.model_adapter import ( + GetCreditPriceLegacy, PricingUnitGetLegacy, WalletGetWithAvailableCreditsLegacy, ) diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 636db5a4497..9c57778cd71 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -7,11 +7,9 @@ from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( get_licensed_items as _get_licensed_items, ) -from simcore_service_api_server.exceptions.service_errors_utils import ( - service_exception_mapper, -) -from simcore_service_api_server.models.pagination import PaginationParams +from ..exceptions.service_errors_utils import service_exception_mapper +from ..models.pagination import PaginationParams from ..models.schemas.model_adapter import LicensedItemGet _exception_mapper = partial(service_exception_mapper, service_name="WebApiServer") From 9e173b9d756cf800ac4070864ee462178df0a0ff Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 13:17:16 +0100 Subject: [PATCH 41/51] fix exception detail --- .../exceptions/service_errors_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index 6407d89785f..ba18cc1c38d 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -147,7 +147,8 @@ def service_exception_handler( RemoteMethodNotRegisteredError, }: # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 raise HTTPException( - status_code=status.HTTP_502_BAD_GATEWAY, detail="Request to failed" + status_code=status.HTTP_502_BAD_GATEWAY, + detail="Request to backend failed", ) from exc if backend_error_type := rpc_exception_map.get(type(exc)): raise backend_error_type(**context) from exc From 351261198152526bc9300291b4202924a18e89ff Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 14:52:52 +0100 Subject: [PATCH 42/51] @pcrespov @matusdrobuliak66 remove /page in endpoint --- .../simcore_service_api_server/api/routes/licensed_items.py | 2 +- services/api-server/tests/unit/test_licensed_items.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index ddf4ae3363e..4bfe4e8e44d 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -17,7 +17,7 @@ @router.get( - "/page", + "", response_model=Page[LicensedItemGet], status_code=status.HTTP_200_OK, responses=_LICENSE_ITEMS_STATUS_CODES, diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index b318fcc5bff..d0ea3cabda8 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -67,7 +67,7 @@ async def _get_backend_licensed_items( async def test_get_licensed_items( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): - resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_200_OK TypeAdapter(Page[LicensedItemGet]).validate_json(resp.text) @@ -76,7 +76,7 @@ async def test_get_licensed_items( async def test_get_licensed_items_timeout( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): - resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_504_GATEWAY_TIMEOUT @@ -87,5 +87,5 @@ async def test_get_licensed_items_timeout( async def test_get_licensed_items_502( mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth ): - resp = await client.get(f"{API_VTAG}/licensed-items/page", auth=auth) + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) assert resp.status_code == status.HTTP_502_BAD_GATEWAY From 115a60ffc6c2b44e40cb580344dfe99f74fa1641 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 14:54:58 +0100 Subject: [PATCH 43/51] @pcrespov rename rpc client to _client --- .../services_rpc/wb_api_server.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 9c57778cd71..4d05b983d82 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -17,14 +17,14 @@ @dataclass class WbApiRpcClient: - _rabbitmq_rpc_client: RabbitMQRPCClient + _client: RabbitMQRPCClient @_exception_mapper(rpc_exception_map={}) async def get_licensed_items( self, product_name: str, page_params: PaginationParams ) -> Page[LicensedItemGet]: licensed_items_page = await _get_licensed_items( - rabbitmq_rpc_client=self._rabbitmq_rpc_client, + rabbitmq_rpc_client=self._client, product_name=product_name, offset=page_params.offset, limit=page_params.limit, @@ -40,6 +40,4 @@ async def get_licensed_items( def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): - app.state.wb_api_rpc_client = WbApiRpcClient( - _rabbitmq_rpc_client=rabbitmq_rmp_client - ) + app.state.wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) From f87ccb4dffa163b9f1f195f9da8ab26c51fe185a Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 14:58:19 +0100 Subject: [PATCH 44/51] @pcrespov move model_config below fields --- .../models/schemas/model_adapter.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 17a76df3ff8..378614bbc2b 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -40,9 +40,6 @@ class GetCreditPriceLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) product_name: str = Field(alias="productName") usd_per_credit: ( Annotated[ @@ -62,6 +59,9 @@ class GetCreditPriceLegacy(BaseModel): "Can be None if this product's price is UNDEFINED", alias="minPaymentAmountUsd", ) + model_config = ConfigDict( + populate_by_name=True, + ) assert set(GetCreditPriceLegacy.model_fields.keys()) == set( @@ -70,9 +70,6 @@ class GetCreditPriceLegacy(BaseModel): class PricingUnitGetLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) pricing_unit_id: PricingUnitId = Field(alias="pricingUnitId") unit_name: str = Field(alias="unitName") unit_extra_info: UnitExtraInfo = Field(alias="unitExtraInfo") @@ -80,6 +77,9 @@ class PricingUnitGetLegacy(BaseModel): Decimal, PlainSerializer(float, return_type=NonNegativeFloat, when_used="json") ] = Field(alias="currentCostPerUnit") default: bool + model_config = ConfigDict( + populate_by_name=True, + ) assert set(PricingUnitGetLegacy.model_fields.keys()) == set( @@ -88,9 +88,6 @@ class PricingUnitGetLegacy(BaseModel): class WalletGetWithAvailableCreditsLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) wallet_id: WalletID = Field(alias="walletId") name: IDStr description: str | None = None @@ -102,6 +99,9 @@ class WalletGetWithAvailableCreditsLegacy(BaseModel): available_credits: Annotated[ Decimal, PlainSerializer(float, return_type=NonNegativeFloat, when_used="json") ] = Field(alias="availableCredits") + model_config = ConfigDict( + populate_by_name=True, + ) assert set(WalletGetWithAvailableCreditsLegacy.model_fields.keys()) == set( @@ -110,9 +110,6 @@ class WalletGetWithAvailableCreditsLegacy(BaseModel): class ServicePricingPlanGetLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) pricing_plan_id: PricingPlanId = Field(alias="pricingPlanId") display_name: str = Field(alias="displayName") description: str @@ -120,6 +117,9 @@ class ServicePricingPlanGetLegacy(BaseModel): created_at: datetime = Field(alias="createdAt") pricing_plan_key: str = Field(alias="pricingPlanKey") pricing_units: list[PricingUnitGetLegacy] = Field(alias="pricingUnits") + model_config = ConfigDict( + populate_by_name=True, + ) assert set(ServicePricingPlanGetLegacy.model_fields.keys()) == set( @@ -128,15 +128,15 @@ class ServicePricingPlanGetLegacy(BaseModel): class LicensedItemGet(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) licensed_item_id: LicensedItemID name: str licensed_resource_type: LicensedResourceType pricing_plan_id: PricingPlanId created_at: datetime modified_at: datetime + model_config = ConfigDict( + populate_by_name=True, + ) assert set(LicensedItemGet.model_fields.keys()) == set( From 87f5b570601ecbe7286937a96284edd594b5bbba Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 15:03:59 +0100 Subject: [PATCH 45/51] @pcrespov name -> display_name --- .../models/schemas/model_adapter.py | 2 +- .../services_rpc/wb_api_server.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 378614bbc2b..64938f9fceb 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -129,7 +129,7 @@ class ServicePricingPlanGetLegacy(BaseModel): class LicensedItemGet(BaseModel): licensed_item_id: LicensedItemID - name: str + display_name: str licensed_resource_type: LicensedResourceType pricing_plan_id: PricingPlanId created_at: datetime diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 4d05b983d82..39cd216529c 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -31,7 +31,14 @@ async def get_licensed_items( ) return create_page( [ - LicensedItemGet.model_validate(elm.model_dump()) + LicensedItemGet( + licensed_item_id=elm.licensed_item_id, + display_name=elm.name, + licensed_resource_type=elm.licensed_resource_type, + pricing_plan_id=elm.pricing_plan_id, + created_at=elm.created_at, + modified_at=elm.modified_at, + ) for elm in licensed_items_page.items ], total=licensed_items_page.total, From 30eb79933dd11bbcc949f955117867c476301bb2 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 15:11:25 +0100 Subject: [PATCH 46/51] hide endpoint from openapi.json @pcrespov @matusdrobuliak66 --- services/api-server/openapi.json | 220 ------------------ .../api/routes/licensed_items.py | 1 + .../models/schemas/model_adapter.py | 2 +- .../services_rpc/wb_api_server.py | 2 +- 4 files changed, 3 insertions(+), 222 deletions(-) diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index 3faeaa58e7f..d70ef50e6bc 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -5348,118 +5348,6 @@ } ] } - }, - "/v0/licensed-items/page": { - "get": { - "tags": [ - "licensed-items" - ], - "summary": "Get Licensed Items", - "description": "Get all licensed items", - "operationId": "get_licensed_items", - "security": [ - { - "HTTPBasic": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 100, - "minimum": 1, - "default": 50, - "title": "Limit" - } - }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0, - "title": "Offset" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Page_LicensedItemGet_" - } - } - } - }, - "429": { - "description": "Too many requests", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } - }, - "502": { - "description": "Unexpected error when communicating with backend service", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } - }, - "503": { - "description": "Service unavailable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } - }, - "504": { - "description": "Request to a backend service timed out.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } } }, "components": { @@ -6198,55 +6086,6 @@ "submitted_at": "2021-04-01 07:15:54.631007" } }, - "LicensedItemGet": { - "properties": { - "licensed_item_id": { - "type": "string", - "format": "uuid", - "title": "Licensed Item Id" - }, - "name": { - "type": "string", - "title": "Name" - }, - "licensed_resource_type": { - "$ref": "#/components/schemas/LicensedResourceType" - }, - "pricing_plan_id": { - "type": "integer", - "exclusiveMinimum": true, - "title": "Pricing Plan Id", - "minimum": 0 - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "modified_at": { - "type": "string", - "format": "date-time", - "title": "Modified At" - } - }, - "type": "object", - "required": [ - "licensed_item_id", - "name", - "licensed_resource_type", - "pricing_plan_id", - "created_at", - "modified_at" - ], - "title": "LicensedItemGet" - }, - "LicensedResourceType": { - "type": "string", - "enum": [ - "VIP_MODEL" - ], - "title": "LicensedResourceType" - }, "Links": { "properties": { "first": { @@ -6570,65 +6409,6 @@ ], "title": "Page[Job]" }, - "Page_LicensedItemGet_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/LicensedItemGet" - }, - "type": "array", - "title": "Items" - }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ], - "title": "Total" - }, - "limit": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, - { - "type": "null" - } - ], - "title": "Limit" - }, - "offset": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ], - "title": "Offset" - }, - "links": { - "$ref": "#/components/schemas/Links" - } - }, - "type": "object", - "required": [ - "items", - "total", - "limit", - "offset", - "links" - ], - "title": "Page[LicensedItemGet]" - }, "Page_Study_": { "properties": { "items": { diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 4bfe4e8e44d..3384f7ac243 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -22,6 +22,7 @@ status_code=status.HTTP_200_OK, responses=_LICENSE_ITEMS_STATUS_CODES, description="Get all licensed items", + include_in_schema=False, ) async def get_licensed_items( page_params: Annotated[PaginationParams, Depends()], diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 64938f9fceb..e5a04e198c5 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -129,7 +129,7 @@ class ServicePricingPlanGetLegacy(BaseModel): class LicensedItemGet(BaseModel): licensed_item_id: LicensedItemID - display_name: str + name: Annotated[str, Field(alias="display_name")] licensed_resource_type: LicensedResourceType pricing_plan_id: PricingPlanId created_at: datetime diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index 39cd216529c..b26535f6762 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -33,7 +33,7 @@ async def get_licensed_items( [ LicensedItemGet( licensed_item_id=elm.licensed_item_id, - display_name=elm.name, + name=elm.name, licensed_resource_type=elm.licensed_resource_type, pricing_plan_id=elm.pricing_plan_id, created_at=elm.created_at, From f04040140873d87828c04c9d882ca50326454839 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 15:46:17 +0100 Subject: [PATCH 47/51] make mypy happy --- .../simcore_service_api_server/api/routes/licensed_items.py | 2 +- .../src/simcore_service_api_server/services_http/webserver.py | 1 + .../simcore_service_api_server/services_rpc/wb_api_server.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py index 3384f7ac243..d02151080ad 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -28,7 +28,7 @@ async def get_licensed_items( page_params: Annotated[PaginationParams, Depends()], web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], product_name: Annotated[str, Depends(get_product_name)], -) -> Page[LicensedItemGet]: +): return await web_api_rpc.get_licensed_items( product_name=product_name, page_params=page_params ) diff --git a/services/api-server/src/simcore_service_api_server/services_http/webserver.py b/services/api-server/src/simcore_service_api_server/services_http/webserver.py index 79a921a7b42..a66bd7ff3d3 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/webserver.py +++ b/services/api-server/src/simcore_service_api_server/services_http/webserver.py @@ -197,6 +197,7 @@ async def _page_projects( with service_exception_handler( service_name="Webserver", http_status_map={status.HTTP_404_NOT_FOUND: ListJobsError}, + rpc_exception_map={}, ): resp = await self.client.get( "/projects", diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index b26535f6762..207712e3e47 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from functools import partial +from typing import cast from fastapi import FastAPI from fastapi_pagination import Page, create_page @@ -29,7 +30,7 @@ async def get_licensed_items( offset=page_params.offset, limit=page_params.limit, ) - return create_page( + page = create_page( [ LicensedItemGet( licensed_item_id=elm.licensed_item_id, @@ -44,6 +45,7 @@ async def get_licensed_items( total=licensed_items_page.total, params=page_params, ) + return cast(Page[LicensedItemGet], page) def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): From 85fdb000083de7c929af75728ed3ac2d53a4ca2a Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Thu, 12 Dec 2024 15:56:55 +0100 Subject: [PATCH 48/51] add __init__.py --- .../src/simcore_service_api_server/services_rpc/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/api-server/src/simcore_service_api_server/services_rpc/__init__.py diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/__init__.py b/services/api-server/src/simcore_service_api_server/services_rpc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 7afde22051bda0a3036d08236ae29592f955be51 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Dec 2024 08:40:55 +0100 Subject: [PATCH 49/51] fix pylint --- .../exceptions/service_errors_utils.py | 3 +++ .../api-server/tests/unit/test_exceptions.py | 16 ++++++++-------- .../api-server/tests/unit/test_licensed_items.py | 4 ++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index ba18cc1c38d..e83c40e2dbe 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -1,3 +1,4 @@ +# pylint: disable=dangerous-default-value import asyncio import logging from collections.abc import Callable, Coroutine, Mapping @@ -164,6 +165,8 @@ def service_exception_mapper( [Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]], Callable[Concatenate[Self, P], Coroutine[Any, Any, R]], ]: + assert len(http_status_map.keys()) > 0 or len(rpc_exception_map.keys()) > 0 + def _decorator(member_func: Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]): _assert_correct_kwargs( func=member_func, diff --git a/services/api-server/tests/unit/test_exceptions.py b/services/api-server/tests/unit/test_exceptions.py index 6949e72d088..be81922bf4f 100644 --- a/services/api-server/tests/unit/test_exceptions.py +++ b/services/api-server/tests/unit/test_exceptions.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable # pylint: disable=unused-argument # pylint: disable=redefined-outer-name - +# pylint: disable=too-many-arguments from http import HTTPStatus from uuid import UUID @@ -25,8 +25,8 @@ async def test_backend_service_exception_mapper(): @service_exception_mapper( - "DummyService", - {status.HTTP_400_BAD_REQUEST: ProfileNotFoundError}, + service_name="DummyService", + http_status_map={status.HTTP_400_BAD_REQUEST: ProfileNotFoundError}, ) async def my_endpoint(status_code: int): raise HTTPStatusError( @@ -103,26 +103,26 @@ async def coro1(project_id): pass with pytest.raises(AssertionError): - _assert_correct_kwargs(func=coro1, status_map=status_map) + _assert_correct_kwargs(func=coro1, exception_types=set(status_map.values())) async def coro2(project_id=UUID("9c201eb7-ba04-4d9b-abe6-f16b406ca86d")): pass with pytest.raises(AssertionError) as exc: - _assert_correct_kwargs(func=coro2, status_map=status_map) + _assert_correct_kwargs(func=coro2, exception_types=set(status_map.values())) async def coro3(*, project_id): pass - _assert_correct_kwargs(func=coro3, status_map=status_map) + _assert_correct_kwargs(func=coro3, exception_types=set(status_map.values())) async def coro4(*, project_id=UUID("ce56af2e-e9e5-46a4-8067-662077de5528")): pass - _assert_correct_kwargs(func=coro4, status_map=status_map) + _assert_correct_kwargs(func=coro4, exception_types=set(status_map.values())) async def coro5(*, project_uuid): pass with pytest.raises(AssertionError): - _assert_correct_kwargs(func=coro5, status_map=status_map) + _assert_correct_kwargs(func=coro5, exception_types=set(status_map.values())) diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index d0ea3cabda8..185f015ca30 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -1,3 +1,7 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments import asyncio import pytest From f80853ab8ee0577fce4340809e3766c1b57f4418 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Dec 2024 08:44:37 +0100 Subject: [PATCH 50/51] small fix --- .../exceptions/service_errors_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index e83c40e2dbe..90bdc27bad4 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -165,8 +165,6 @@ def service_exception_mapper( [Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]], Callable[Concatenate[Self, P], Coroutine[Any, Any, R]], ]: - assert len(http_status_map.keys()) > 0 or len(rpc_exception_map.keys()) > 0 - def _decorator(member_func: Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]): _assert_correct_kwargs( func=member_func, From 8a52ec338cdbdae5b7b2db124da5821fc54a9825 Mon Sep 17 00:00:00 2001 From: Mads Bisgaard Date: Fri, 13 Dec 2024 09:50:46 +0100 Subject: [PATCH 51/51] fix unit api-server tests --- .../core/application.py | 3 -- .../services_http/rabbitmq.py | 3 ++ .../test_api_routers_solvers_jobs_logs.py | 2 +- .../tests/unit/test_licensed_items.py | 33 ++++++++----------- .../tests/unit/test_services_rabbitmq.py | 2 +- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/services/api-server/src/simcore_service_api_server/core/application.py b/services/api-server/src/simcore_service_api_server/core/application.py index 571b98c3c6a..4de017698a6 100644 --- a/services/api-server/src/simcore_service_api_server/core/application.py +++ b/services/api-server/src/simcore_service_api_server/core/application.py @@ -10,12 +10,10 @@ from .. import exceptions from .._meta import API_VERSION, API_VTAG, APP_NAME -from ..api.dependencies.rabbitmq import get_rabbitmq_rpc_client from ..api.root import create_router from ..api.routes.health import router as health_router from ..services_http import catalog, director_v2, storage, webserver from ..services_http.rabbitmq import setup_rabbitmq -from ..services_rpc import wb_api_server from ._prometheus_instrumentation import setup_prometheus_instrumentation from .events import create_start_app_handler, create_stop_app_handler from .openapi import override_openapi_method, use_route_names_as_operation_ids @@ -94,7 +92,6 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI: settings.API_SERVER_WEBSERVER, tracing_settings=settings.API_SERVER_TRACING, ) - wb_api_server.setup(app, get_rabbitmq_rpc_client(app)) if settings.API_SERVER_CATALOG: catalog.setup( diff --git a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py index 30c116bc128..07201402876 100644 --- a/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py @@ -4,6 +4,8 @@ from servicelib.rabbitmq import RabbitMQClient, wait_till_rabbitmq_responsive from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from settings_library.rabbit import RabbitSettings +from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client +from simcore_service_api_server.services_rpc import wb_api_server from ..core.health_checker import ApiServerHealthChecker from ..services_http.log_streaming import LogDistributor @@ -36,6 +38,7 @@ async def _on_startup() -> None: await app.state.health_checker.setup( app.state.settings.API_SERVER_HEALTH_CHECK_TASK_PERIOD_SECONDS ) + wb_api_server.setup(app, get_rabbitmq_rpc_client(app)) async def _on_shutdown() -> None: if app.state.health_checker: diff --git a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py index eb821e46d01..80a7176ca85 100644 --- a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py +++ b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py @@ -79,7 +79,7 @@ def fake_project_for_streaming( fake_project = ProjectGet.model_validate(data) fake_project.workbench = {faker.uuid4(): faker.uuid4()} mocker.patch( - "simcore_service_api_server.api.dependencies.webserver.AuthSession.get_project", + "simcore_service_api_server.api.dependencies.webserver_http.AuthSession.get_project", return_value=fake_project, ) diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py index 185f015ca30..e0db32dc3b7 100644 --- a/services/api-server/tests/unit/test_licensed_items.py +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -5,7 +5,7 @@ import asyncio import pytest -from fastapi import status +from fastapi import FastAPI, status from httpx import AsyncClient, BasicAuth from models_library.api_schemas_webserver.licensed_items import ( LicensedItemGet as _LicensedItemGet, @@ -18,28 +18,17 @@ from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError from simcore_service_api_server._meta import API_VTAG +from simcore_service_api_server.api.dependencies.webserver_rpc import ( + get_wb_api_rpc_client, +) from simcore_service_api_server.models.pagination import Page from simcore_service_api_server.models.schemas.model_adapter import LicensedItemGet - - -@pytest.fixture -async def mock_rabbitmq_rpc_client(mocker: MockerFixture) -> MockerFixture: - class DummyRabbitMqRpcClient: - pass - - def _get_dummy_rpc_client(): - return DummyRabbitMqRpcClient - - mocker.patch( - "simcore_service_api_server.core.application.get_rabbitmq_rpc_client", - sideeffect=_get_dummy_rpc_client, - ) - return mocker +from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient @pytest.fixture async def mock_wb_api_server_rcp( - mock_rabbitmq_rpc_client: MockerFixture, exception_to_raise: Exception | None + app: FastAPI, mocker: MockerFixture, exception_to_raise: Exception | None ) -> MockerFixture: async def _get_backend_licensed_items( rabbitmq_rpc_client: RabbitMQRPCClient, @@ -59,12 +48,18 @@ async def _get_backend_licensed_items( total=len(examples), ) - mock_rabbitmq_rpc_client.patch( + class DummyRpcClient: + pass + + app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient( + _client=DummyRpcClient() + ) + mocker.patch( "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", _get_backend_licensed_items, ) - return mock_rabbitmq_rpc_client + return mocker @pytest.mark.parametrize("exception_to_raise", [None]) diff --git a/services/api-server/tests/unit/test_services_rabbitmq.py b/services/api-server/tests/unit/test_services_rabbitmq.py index be0220042ab..aa644e81500 100644 --- a/services/api-server/tests/unit/test_services_rabbitmq.py +++ b/services/api-server/tests/unit/test_services_rabbitmq.py @@ -441,7 +441,7 @@ async def deregister(self, job_id: None): async def test_log_generator(mocker: MockFixture, faker: Faker): mocker.patch( - "simcore_service_api_server.services.log_streaming.LogStreamer._project_done", + "simcore_service_api_server.services_http.log_streaming.LogStreamer._project_done", return_value=True, ) log_streamer = LogStreamer(user_id=3, director2_api=None, job_id=None, log_distributor=_MockLogDistributor(), log_check_timeout=1) # type: ignore