diff --git a/api/specs/web-server/_catalog.py b/api/specs/web-server/_catalog.py index 76df1b93ae5..a065665e672 100644 --- a/api/specs/web-server/_catalog.py +++ b/api/specs/web-server/_catalog.py @@ -10,6 +10,7 @@ ServiceResourcesGet, ServiceUpdate, ) +from models_library.api_schemas_webserver.resource_usage import ServicePricingPlanGet from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.catalog._handlers import ( @@ -130,3 +131,15 @@ def get_service_resources( _params: Annotated[ServicePathParams, Depends()], ): ... + + +@router.get( + "/catalog/services/{service_key:path}/{service_version}/pricing-plan", + response_model=Envelope[ServicePricingPlanGet], + summary="Retrieve default pricing plan for provided service", + tags=["pricing-plans"], +) +async def get_service_pricing_plan( + _params: Annotated[ServicePathParams, Depends()], +): + ... diff --git a/api/specs/web-server/_resource_usage.py b/api/specs/web-server/_resource_usage.py index 387cc95990f..74b66fbbbb0 100644 --- a/api/specs/web-server/_resource_usage.py +++ b/api/specs/web-server/_resource_usage.py @@ -11,17 +11,24 @@ from _common import assert_handler_signature_against_model from fastapi import APIRouter, Query -from models_library.api_schemas_webserver.resource_usage import ServiceRunGet +from models_library.api_schemas_webserver.resource_usage import ( + PricingUnitGet, + ServiceRunGet, +) from models_library.generics import Envelope +from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.rest_pagination import DEFAULT_NUMBER_OF_ITEMS_PER_PAGE from models_library.wallets import WalletID from pydantic import NonNegativeInt from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.resource_usage._pricing_plans_handlers import ( + _GetPricingPlanUnitPathParams, +) from simcore_service_webserver.resource_usage._service_runs_handlers import ( - _ListServicesPathParams, + _ListServicesResourceUsagesPathParams, ) -router = APIRouter(prefix=f"/{API_VTAG}", tags=["usage"]) +router = APIRouter(prefix=f"/{API_VTAG}") # @@ -30,9 +37,10 @@ @router.get( - "/resource-usage/services", + "/services/-/resource-usages", response_model=Envelope[list[ServiceRunGet]], summary="Retrieve finished and currently running user services (user and product are taken from context, optionally wallet_id parameter might be provided).", + tags=["usage"], ) async def list_resource_usage_services( wallet_id: WalletID = Query(None), @@ -43,5 +51,22 @@ async def list_resource_usage_services( assert_handler_signature_against_model( - list_resource_usage_services, _ListServicesPathParams + list_resource_usage_services, _ListServicesResourceUsagesPathParams +) + + +@router.get( + "/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}", + response_model=Envelope[PricingUnitGet], + summary="Retrieve detail information about pricing unit", + tags=["pricing-plans"], +) +async def get_pricing_plan_unit( + pricing_plan_id: PricingPlanId, pricing_unit_id: PricingUnitId +): + ... + + +assert_handler_signature_against_model( + get_pricing_plan_unit, _GetPricingPlanUnitPathParams ) diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/pricing_plans.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/pricing_plans.py new file mode 100644 index 00000000000..1e29b32b989 --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/pricing_plans.py @@ -0,0 +1,61 @@ +from datetime import datetime +from decimal import Decimal +from typing import Any, ClassVar + +from models_library.resource_tracker import ( + PricingPlanClassification, + PricingPlanId, + PricingUnitCostId, + PricingUnitId, +) +from pydantic import BaseModel + + +class PricingUnitGet(BaseModel): + pricing_unit_id: PricingUnitId + unit_name: str + current_cost_per_unit: Decimal + current_cost_per_unit_id: PricingUnitCostId + default: bool + specific_info: dict + + class Config: + schema_extra: ClassVar[dict[str, Any]] = { + "examples": [ + { + "pricing_unit_id": 1, + "unit_name": "SMALL", + "current_cost_per_unit": 5.7, + "current_cost_per_unit_id": 1, + "default": True, + "specific_info": {}, + } + ] + } + + +class ServicePricingPlanGet(BaseModel): + pricing_plan_id: PricingPlanId + display_name: str + description: str + classification: PricingPlanClassification + created_at: datetime + pricing_plan_key: str + pricing_units: list[PricingUnitGet] + + class Config: + schema_extra: ClassVar[dict[str, Any]] = { + "examples": [ + { + "pricing_plan_id": 1, + "display_name": "Pricing Plan for Sleeper", + "description": "Special Pricing Plan for Sleeper", + "classification": "TIER", + "created_at": "2023-01-11 13:11:47.293595", + "pricing_plan_key": "pricing-plan-sleeper", + "pricing_units": [ + PricingUnitGet.Config.schema_extra["examples"][0] + ], + } + ] + } diff --git a/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/service_runs.py b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/service_runs.py new file mode 100644 index 00000000000..b747674cd40 --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_resource_usage_tracker/service_runs.py @@ -0,0 +1,35 @@ +from datetime import datetime +from decimal import Decimal + +from models_library.projects import ProjectID +from models_library.projects_nodes_io import NodeID +from models_library.resource_tracker import ( + CreditTransactionStatus, + ServiceRunId, + ServiceRunStatus, +) +from models_library.services import ServiceKey, ServiceVersion +from models_library.users import UserID +from models_library.wallets import WalletID +from pydantic import BaseModel + + +class ServiceRunGet(BaseModel): + service_run_id: ServiceRunId + wallet_id: WalletID | None + wallet_name: str | None + user_id: UserID + project_id: ProjectID + project_name: str + node_id: NodeID + node_name: str + service_key: ServiceKey + service_version: ServiceVersion + service_type: str + service_resources: dict + started_at: datetime + stopped_at: datetime | None + service_run_status: ServiceRunStatus + # Cost in credits + credit_cost: Decimal | None + transaction_status: CreditTransactionStatus | None diff --git a/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py b/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py index 0cea2daedda..37daae1674f 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/resource_usage.py @@ -2,7 +2,6 @@ from decimal import Decimal from models_library.resource_tracker import ( - PricingDetailId, PricingPlanClassification, PricingPlanId, ServiceRunId, @@ -14,7 +13,7 @@ from ..projects import ProjectID from ..projects_nodes_io import NodeID -from ..resource_tracker import ServiceRunStatus +from ..resource_tracker import PricingUnitId, ServiceRunStatus from ..services import ServiceKey, ServiceVersion from ._base import OutputSchema @@ -41,18 +40,18 @@ class ServiceRunGet( service_run_status: ServiceRunStatus -class PricingDetailMinimalGet(OutputSchema): - pricing_detail_id: PricingDetailId +class PricingUnitGet(OutputSchema): + pricing_unit_id: PricingUnitId unit_name: str - cost_per_unit: Decimal - valid_from: datetime - simcore_default: bool + current_cost_per_unit: Decimal + default: bool -class PricingPlanGet(OutputSchema): +class ServicePricingPlanGet(OutputSchema): pricing_plan_id: PricingPlanId - name: str + display_name: str description: str classification: PricingPlanClassification created_at: datetime - details: list[PricingDetailMinimalGet] + pricing_plan_key: str + pricing_units: list[PricingUnitGet] diff --git a/packages/models-library/src/models_library/rabbitmq_messages.py b/packages/models-library/src/models_library/rabbitmq_messages.py index aaeb28b9982..8c220ca17a3 100644 --- a/packages/models-library/src/models_library/rabbitmq_messages.py +++ b/packages/models-library/src/models_library/rabbitmq_messages.py @@ -201,7 +201,8 @@ class RabbitResourceTrackingStartedMessage(RabbitResourceTrackingBaseMessage): wallet_name: str | None pricing_plan_id: int | None - pricing_detail_id: int | None + pricing_unit_id: int | None + pricing_unit_cost_id: int | None product_name: str simcore_user_agent: str diff --git a/packages/models-library/src/models_library/resource_tracker.py b/packages/models-library/src/models_library/resource_tracker.py index dc50d6ea5a0..05b47959bee 100644 --- a/packages/models-library/src/models_library/resource_tracker.py +++ b/packages/models-library/src/models_library/resource_tracker.py @@ -7,7 +7,8 @@ ServiceRunId: TypeAlias = str PricingPlanId: TypeAlias = PositiveInt -PricingDetailId: TypeAlias = PositiveInt +PricingUnitId: TypeAlias = PositiveInt +PricingUnitCostId: TypeAlias = PositiveInt CreditTransactionId: TypeAlias = PositiveInt diff --git a/packages/models-library/src/models_library/services_creation.py b/packages/models-library/src/models_library/services_creation.py index bd46273963c..e2102efe075 100644 --- a/packages/models-library/src/models_library/services_creation.py +++ b/packages/models-library/src/models_library/services_creation.py @@ -11,7 +11,8 @@ class CreateServiceMetricsAdditionalParams(BaseModel): wallet_id: WalletID | None wallet_name: str | None pricing_plan_id: int | None - pricing_detail_id: int | None + pricing_unit_id: int | None + pricing_unit_cost_id: int | None product_name: str simcore_user_agent: str user_email: str @@ -28,7 +29,8 @@ class Config: "wallet_id": 1, "wallet_name": "a private wallet for me", "pricing_plan_id": 1, - "pricing_detail_id": 1, + "pricing_unit_id": 1, + "pricing_unit_detail_id": 1, "product_name": "osparc", "simcore_user_agent": "undefined", "user_email": "test@test.com", diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/b102946c8134_changes_in_pricing_plans.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/b102946c8134_changes_in_pricing_plans.py new file mode 100644 index 00000000000..607bc364bf8 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/b102946c8134_changes_in_pricing_plans.py @@ -0,0 +1,294 @@ +"""changes in pricing plans + +Revision ID: b102946c8134 +Revises: 6e9f34338072 +Create Date: 2023-10-01 12:50:08.671566+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "b102946c8134" +down_revision = "6e9f34338072" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # CREATE RESOURCE_TRACKER_PRICING_UNIT_COSTS + op.create_table( + "resource_tracker_pricing_unit_costs", + sa.Column("pricing_unit_cost_id", sa.BigInteger(), nullable=False), + sa.Column("pricing_plan_id", sa.BigInteger(), nullable=False), + sa.Column("pricing_plan_key", sa.String(), nullable=False), + sa.Column("pricing_unit_id", sa.BigInteger(), nullable=False), + sa.Column("pricing_unit_name", sa.String(), nullable=False), + sa.Column("cost_per_unit", sa.Numeric(scale=2), nullable=False), + sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False), + sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "specific_info", postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column( + "created", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("comment", sa.String(), nullable=True), + sa.Column( + "modified", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("pricing_unit_cost_id"), + ) + op.create_index( + op.f("ix_resource_tracker_pricing_unit_costs_pricing_plan_id"), + "resource_tracker_pricing_unit_costs", + ["pricing_plan_id"], + unique=False, + ) + op.create_index( + op.f("ix_resource_tracker_pricing_unit_costs_pricing_unit_id"), + "resource_tracker_pricing_unit_costs", + ["pricing_unit_id"], + unique=False, + ) + op.create_index( + op.f("ix_resource_tracker_pricing_unit_costs_valid_to"), + "resource_tracker_pricing_unit_costs", + ["valid_to"], + unique=False, + ) + + # CREATE RESOURCE_TRACKER_PRICING_UNITS + op.create_table( + "resource_tracker_pricing_units", + sa.Column("pricing_unit_id", sa.BigInteger(), nullable=False), + sa.Column("pricing_plan_id", sa.BigInteger(), nullable=False), + sa.Column("unit_name", sa.String(), nullable=False), + sa.Column("default", sa.Boolean(), nullable=False), + sa.Column( + "specific_info", postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column( + "created", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "modified", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["pricing_plan_id"], + ["resource_tracker_pricing_plans.pricing_plan_id"], + name="fk_resource_tracker_pricing_units_pricing_plan_id", + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("pricing_unit_id"), + sa.UniqueConstraint( + "pricing_plan_id", "unit_name", name="pricing_plan_and_unit_constrain_key" + ), + ) + op.create_index( + op.f("ix_resource_tracker_pricing_units_pricing_plan_id"), + "resource_tracker_pricing_units", + ["pricing_plan_id"], + unique=False, + ) + + # DROP RESOURCE_TRACKER_PRICING_DETAILS + op.drop_index( + "ix_resource_tracker_pricing_details_pricing_plan_id", + table_name="resource_tracker_pricing_details", + ) + op.drop_index( + "ix_resource_tracker_pricing_details_valid_to", + table_name="resource_tracker_pricing_details", + ) + op.drop_table("resource_tracker_pricing_details") + + # MODIFY RESOURCE_TRACKER_CREDIT_TRANSACTIONS + op.add_column( + "resource_tracker_credit_transactions", + sa.Column("pricing_unit_id", sa.BigInteger(), nullable=True), + ) + op.add_column( + "resource_tracker_credit_transactions", + sa.Column("pricing_unit_cost_id", sa.BigInteger(), nullable=True), + ) + op.drop_column("resource_tracker_credit_transactions", "pricing_detail_id") + + # MODIFY RESOURCE_TRACKER_PRICING_PLAN_TO_SERVICE + op.add_column( + "resource_tracker_pricing_plan_to_service", + sa.Column("service_default_plan", sa.Boolean(), nullable=False), + ) + op.drop_column("resource_tracker_pricing_plan_to_service", "product") + + # MODIFY RESOURCE_TRACKER_PRICING_PLANS + op.add_column( + "resource_tracker_pricing_plans", + sa.Column("display_name", sa.String(), nullable=False), + ) + op.add_column( + "resource_tracker_pricing_plans", + sa.Column("pricing_plan_key", sa.String(), nullable=False), + ) + op.create_unique_constraint( + "pricing_plans_pricing_plan_key", + "resource_tracker_pricing_plans", + ["product_name", "pricing_plan_key"], + ) + op.drop_column("resource_tracker_pricing_plans", "name") + + # MODIFY RESOURCE_TRACKER_SERVICE_RUNS + op.add_column( + "resource_tracker_service_runs", + sa.Column("pricing_unit_id", sa.BigInteger(), nullable=True), + ) + op.add_column( + "resource_tracker_service_runs", + sa.Column("pricing_unit_cost_id", sa.BigInteger(), nullable=True), + ) + op.add_column( + "resource_tracker_service_runs", + sa.Column("pricing_unit_cost", sa.Numeric(scale=2), nullable=True), + ) + op.drop_column("resource_tracker_service_runs", "pricing_detail_id") + op.drop_column("resource_tracker_service_runs", "pricing_detail_cost_per_unit") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "resource_tracker_service_runs", + sa.Column( + "pricing_detail_cost_per_unit", + sa.NUMERIC(), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "resource_tracker_service_runs", + sa.Column("pricing_detail_id", sa.BIGINT(), autoincrement=False, nullable=True), + ) + op.drop_column("resource_tracker_service_runs", "pricing_unit_cost") + op.drop_column("resource_tracker_service_runs", "pricing_unit_cost_id") + op.drop_column("resource_tracker_service_runs", "pricing_unit_id") + op.add_column( + "resource_tracker_pricing_plans", + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + ) + op.drop_constraint( + "pricing_plans_pricing_plan_key", + "resource_tracker_pricing_plans", + type_="unique", + ) + op.drop_column("resource_tracker_pricing_plans", "pricing_plan_key") + op.drop_column("resource_tracker_pricing_plans", "display_name") + op.add_column( + "resource_tracker_pricing_plan_to_service", + sa.Column("product", sa.VARCHAR(), autoincrement=False, nullable=False), + ) + op.drop_column("resource_tracker_pricing_plan_to_service", "service_default_plan") + op.add_column( + "resource_tracker_credit_transactions", + sa.Column("pricing_detail_id", sa.BIGINT(), autoincrement=False, nullable=True), + ) + op.drop_column("resource_tracker_credit_transactions", "pricing_unit_cost_id") + op.drop_column("resource_tracker_credit_transactions", "pricing_unit_id") + op.create_table( + "resource_tracker_pricing_details", + sa.Column("pricing_detail_id", sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column("pricing_plan_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column("unit_name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column( + "valid_from", + postgresql.TIMESTAMP(timezone=True), + autoincrement=False, + nullable=False, + ), + sa.Column( + "valid_to", + postgresql.TIMESTAMP(timezone=True), + autoincrement=False, + nullable=True, + ), + sa.Column( + "specific_info", + postgresql.JSONB(astext_type=sa.Text()), + autoincrement=False, + nullable=False, + ), + sa.Column( + "created", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.Column( + "modified", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + autoincrement=False, + nullable=False, + ), + sa.Column("simcore_default", sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column("cost_per_unit", sa.NUMERIC(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint( + ["pricing_plan_id"], + ["resource_tracker_pricing_plans.pricing_plan_id"], + name="fk_resource_tracker_pricing_details_pricing_plan_id", + onupdate="CASCADE", + ondelete="RESTRICT", + ), + sa.PrimaryKeyConstraint( + "pricing_detail_id", name="resource_tracker_pricing_details_pkey" + ), + ) + op.create_index( + "ix_resource_tracker_pricing_details_valid_to", + "resource_tracker_pricing_details", + ["valid_to"], + unique=False, + ) + op.create_index( + "ix_resource_tracker_pricing_details_pricing_plan_id", + "resource_tracker_pricing_details", + ["pricing_plan_id"], + unique=False, + ) + op.drop_index( + op.f("ix_resource_tracker_pricing_units_pricing_plan_id"), + table_name="resource_tracker_pricing_units", + ) + op.drop_table("resource_tracker_pricing_units") + op.drop_index( + op.f("ix_resource_tracker_pricing_unit_costs_valid_to"), + table_name="resource_tracker_pricing_unit_costs", + ) + op.drop_index( + op.f("ix_resource_tracker_pricing_unit_costs_pricing_unit_id"), + table_name="resource_tracker_pricing_unit_costs", + ) + op.drop_index( + op.f("ix_resource_tracker_pricing_unit_costs_pricing_plan_id"), + table_name="resource_tracker_pricing_unit_costs", + ) + op.drop_table("resource_tracker_pricing_unit_costs") + # ### end Alembic commands ### 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 3433bb8679b..414e8d3a1f8 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 @@ -60,7 +60,13 @@ class CreditTransactionClassification(str, enum.Enum): doc="Pricing plan", ), sa.Column( - "pricing_detail_id", + "pricing_unit_id", + sa.BigInteger, + nullable=True, + doc="Pricing detail", + ), + sa.Column( + "pricing_unit_cost_id", sa.BigInteger, nullable=True, doc="Pricing detail", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py index 7b03f6ec88f..b0040d93ae6 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py @@ -35,13 +35,14 @@ nullable=False, doc="MAJOR.MINOR.PATCH semantic versioning (see https://semver.org)", ), + column_created_datetime(timezone=True), + column_modified_datetime(timezone=True), sa.Column( - "product", - sa.String, + "service_default_plan", + sa.Boolean(), nullable=False, - doc="Product", + default=False, + doc="Option to mark default pricing plan for the service (ex. when there are more pricing plans for the same service)", ), - column_created_datetime(timezone=True), - column_modified_datetime(timezone=True), # --------------------------- ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py index c94182fd224..8ec50b0f206 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py @@ -38,7 +38,7 @@ class PricingPlanClassification(str, enum.Enum): index=True, ), sa.Column( - "name", + "display_name", sa.String, nullable=False, doc="Name of the pricing plan, ex. DYNAMIC_SERVICES_TIERS, CPU_HOURS, STORAGE", @@ -64,5 +64,15 @@ class PricingPlanClassification(str, enum.Enum): ), column_created_datetime(timezone=True), column_modified_datetime(timezone=True), + sa.Column( + "pricing_plan_key", + sa.String, + nullable=False, + default=False, + doc="Unique human readable pricing plan key that might be used for integration", + ), # --------------------------- + sa.UniqueConstraint( + "product_name", "pricing_plan_key", name="pricing_plans_pricing_plan_key" + ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_details.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py similarity index 71% rename from packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_details.py rename to packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py index 52afa2b28fc..165cd9b369b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_details.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py @@ -10,11 +10,11 @@ from ._common import NUMERIC_KWARGS, column_created_datetime, column_modified_datetime from .base import metadata -resource_tracker_pricing_details = sa.Table( - "resource_tracker_pricing_details", +resource_tracker_pricing_unit_costs = sa.Table( + "resource_tracker_pricing_unit_costs", metadata, sa.Column( - "pricing_detail_id", + "pricing_unit_cost_id", sa.BigInteger, nullable=False, primary_key=True, @@ -23,21 +23,28 @@ sa.Column( "pricing_plan_id", sa.BigInteger, - sa.ForeignKey( - "resource_tracker_pricing_plans.pricing_plan_id", - name="fk_resource_tracker_pricing_details_pricing_plan_id", - onupdate="CASCADE", - ondelete="RESTRICT", - ), nullable=False, - doc="Foreign key to pricing plan", + doc="Parent pricing plan", index=True, ), sa.Column( - "unit_name", + "pricing_plan_key", sa.String, nullable=False, - doc="The custom name of the pricing plan, ex. DYNAMIC_SERVICES_TIERS, COMPUTATIONAL_SERVICES_TIERS, CPU_HOURS, STORAGE", + doc="Parent pricing key (storing for historical reasons)", + ), + sa.Column( + "pricing_unit_id", + sa.BigInteger, + nullable=False, + doc="Parent pricing unit", + index=True, + ), + sa.Column( + "pricing_unit_name", + sa.String, + nullable=False, + doc="Parent pricing unit name (storing for historical reasons)", ), sa.Column( "cost_per_unit", @@ -58,13 +65,6 @@ doc="To when the pricing unit was active, if null it is still active", index=True, ), - sa.Column( - "simcore_default", - sa.Boolean(), - nullable=False, - default=False, - doc="Option to mark default pricing plan by creator", - ), sa.Column( "specific_info", JSONB, @@ -73,6 +73,11 @@ doc="Specific internal info of the pricing unit, ex. for tiers we can store in which EC2 instance type we run the service.", ), column_created_datetime(timezone=True), + sa.Column( + "comment", + sa.String, + nullable=True, + doc="Option to store comment", + ), column_modified_datetime(timezone=True), - # --------------------------- ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py new file mode 100644 index 00000000000..11aa5e7a548 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py @@ -0,0 +1,64 @@ +""" Pricing details table + - each pricing plan table can have multiple units. These units are stored in the + pricing details table with their unit cost. Each unit cost (row in this table) has + id which uniquely defines the prices at this point of the time. We always store whole + history and do not update the rows of this table. +""" +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +from ._common import column_created_datetime, column_modified_datetime +from .base import metadata + +resource_tracker_pricing_units = sa.Table( + "resource_tracker_pricing_units", + metadata, + sa.Column( + "pricing_unit_id", + sa.BigInteger, + nullable=False, + primary_key=True, + doc="Identifier index", + ), + sa.Column( + "pricing_plan_id", + sa.BigInteger, + sa.ForeignKey( + "resource_tracker_pricing_plans.pricing_plan_id", + name="fk_resource_tracker_pricing_units_pricing_plan_id", + onupdate="CASCADE", + ondelete="CASCADE", + ), + nullable=False, + doc="Foreign key to pricing plan", + index=True, + ), + sa.Column( + "unit_name", + sa.String, + nullable=False, + doc="The custom name of the pricing plan, ex. SMALL, MEDIUM, LARGE", + ), + sa.Column( + "default", + sa.Boolean(), + nullable=False, + default=False, + doc="Option to mark default pricing plan by creator", + ), + sa.Column( + "specific_info", + JSONB, + nullable=False, + default="'{}'::jsonb", + doc="Specific internal info of the pricing unit, ex. for tiers we can store in which EC2 instance type we run the service.", + ), + column_created_datetime(timezone=True), + column_modified_datetime(timezone=True), + # --------------------------- + sa.UniqueConstraint( + "pricing_plan_id", + "unit_name", + name="pricing_plan_and_unit_constrain_key", + ), +) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py index a77227574ab..1007a24f9cb 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_service_runs.py @@ -56,16 +56,22 @@ class ResourceTrackerServiceRunStatus(str, enum.Enum): doc="Pricing plan id for billing purposes", ), sa.Column( - "pricing_detail_id", + "pricing_unit_id", sa.BigInteger, nullable=True, - doc="Pricing detail id for billing purposes", + doc="Pricing unit id for billing purposes", ), sa.Column( - "pricing_detail_cost_per_unit", + "pricing_unit_cost_id", + sa.BigInteger, + nullable=True, + doc="Pricing unit cost id for billing purposes", + ), + sa.Column( + "pricing_unit_cost", sa.Numeric(**NUMERIC_KWARGS), # type: ignore nullable=True, - doc="Pricing detail cost per unit used for billing purposes", + doc="Pricing unit cost used for billing purposes", ), # User agent field sa.Column( diff --git a/services/director-v2/openapi.json b/services/director-v2/openapi.json index edc4f2f21fd..f4564345aad 100644 --- a/services/director-v2/openapi.json +++ b/services/director-v2/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "simcore-service-director-v2", "description": " Orchestrates the pipeline of services defined by the user", @@ -60,37 +60,30 @@ } } }, - "/v0/services": { - "get": { + "/v2/computations": { + "post": { "tags": [ - "services" - ], - "summary": "List Services", - "description": "Lists services available in the deployed registry", - "operationId": "list_services_v0_services_get", - "parameters": [ - { - "description": "The service type:\n - computational - a computational service\n - interactive - an interactive service\n", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ServiceType" - } - ], - "description": "The service type:\n - computational - a computational service\n - interactive - an interactive service\n" - }, - "name": "service_type", - "in": "query" - } + "computations" ], + "summary": "Create and optionally start a new computation", + "operationId": "create_computation_v2_computations_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComputationCreate" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServicesArrayEnveloped" + "$ref": "#/components/schemas/ComputationGet" } } } @@ -108,38 +101,34 @@ } } }, - "/v0/services/{service_key}/{service_version}/extras": { + "/v2/computations/{project_id}": { "get": { "tags": [ - "services" + "computations" ], - "summary": "Get Extra Service Versioned", - "description": "Returns the service extras", - "operationId": "get_extra_service_versioned_v0_services__service_key___service_version__extras_get", + "summary": "Returns a computation pipeline state", + "operationId": "get_computation_v2_computations__project_id__get", "parameters": [ { - "description": "Distinctive name for the node based on the docker registry path", "required": true, "schema": { - "title": "Service Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", "type": "string", - "description": "Distinctive name for the node based on the docker registry path" + "format": "uuid", + "title": "Project Id" }, - "name": "service_key", + "name": "project_id", "in": "path" }, { - "description": "The tag/version of the service", "required": true, "schema": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string", - "description": "The tag/version of the service" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "service_version", - "in": "path" + "name": "user_id", + "in": "query" } ], "responses": { @@ -148,7 +137,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceExtrasEnveloped" + "$ref": "#/components/schemas/ComputationGet" } } } @@ -164,49 +153,88 @@ } } } - } - }, - "/v0/services/{service_key}/{service_version}": { - "get": { + }, + "delete": { "tags": [ - "services" + "computations" ], - "summary": "Get Service Versioned", - "description": "Returns details of the selected service if available in the platform", - "operationId": "get_service_versioned_v0_services__service_key___service_version__get", + "summary": "Deletes a computation pipeline", + "operationId": "delete_computation_v2_computations__project_id__delete", "parameters": [ { - "description": "Distinctive name for the node based on the docker registry path", "required": true, "schema": { - "title": "Service Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", "type": "string", - "description": "Distinctive name for the node based on the docker registry path" + "format": "uuid", + "title": "Project Id" }, - "name": "service_key", + "name": "project_id", "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComputationDelete" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v2/computations/{project_id}:stop": { + "post": { + "tags": [ + "computations" + ], + "summary": "Stops a computation pipeline", + "operationId": "stop_computation_v2_computations__project_id__stop_post", + "parameters": [ { - "description": "The tag/version of the service", "required": true, "schema": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", "type": "string", - "description": "The tag/version of the service" + "format": "uuid", + "title": "Project Id" }, - "name": "service_version", + "name": "project_id", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComputationStop" + } + } + }, + "required": true + }, "responses": { - "200": { + "202": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServicesArrayEnveloped" + "$ref": "#/components/schemas/ComputationGet" } } } @@ -224,35 +252,34 @@ } } }, - "/v0/running_interactive_services": { + "/v2/computations/{project_id}/tasks/-/logfile": { "get": { "tags": [ - "services" + "computations" ], - "summary": "List Running Interactive Services", - "description": "Lists of running interactive services", - "operationId": "list_running_interactive_services_v0_running_interactive_services_get", + "summary": "Gets computation task logs file after is done", + "description": "Returns download links to log-files of each task in a computation.\nEach log is only available when the corresponding task is done", + "operationId": "get_all_tasks_log_files_v2_computations__project_id__tasks___logfile_get", "parameters": [ { - "description": "The ID of the user that starts the service", "required": true, "schema": { - "title": "User Id", "type": "string", - "description": "The ID of the user that starts the service" + "format": "uuid", + "title": "Project Id" }, - "name": "user_id", - "in": "query" + "name": "project_id", + "in": "path" }, { - "description": "The ID of the project in which the service starts", "required": true, "schema": { - "title": "Project Id", - "type": "string", - "description": "The ID of the project in which the service starts" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "project_id", + "name": "user_id", "in": "query" } ], @@ -262,7 +289,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RunningServicesDetailsArrayEnveloped" + "items": { + "$ref": "#/components/schemas/TaskLogFileGet" + }, + "type": "array", + "title": "Response Get All Tasks Log Files V2 Computations Project Id Tasks Logfile Get" } } } @@ -278,97 +309,57 @@ } } } - }, - "post": { + } + }, + "/v2/computations/{project_id}/tasks/{node_uuid}/logfile": { + "get": { "tags": [ - "services" + "computations" ], - "summary": "Start Interactive Service", - "description": "Starts an interactive service in the platform", - "operationId": "start_interactive_service_v0_running_interactive_services_post", + "summary": "Gets computation task logs file after is done", + "description": "Returns a link to download logs file of a give task.\nThe log is only available when the task is done", + "operationId": "get_task_log_file_v2_computations__project_id__tasks__node_uuid__logfile_get", "parameters": [ { - "description": "The ID of the user that starts the service", - "required": true, - "schema": { - "title": "User Id", - "type": "string", - "description": "The ID of the user that starts the service" - }, - "name": "user_id", - "in": "query" - }, - { - "description": "The ID of the project in which the service starts", "required": true, "schema": { - "title": "Project Id", "type": "string", - "description": "The ID of the project in which the service starts" + "format": "uuid", + "title": "Project Id" }, "name": "project_id", - "in": "query" - }, - { - "description": "distinctive name for the node based on the docker registry path", - "required": true, - "schema": { - "title": "Service Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", - "type": "string", - "description": "distinctive name for the node based on the docker registry path" - }, - "example": [ - "simcore/services/comp/itis/sleeper", - "simcore/services/dynamic/3dviewer" - ], - "name": "service_key", - "in": "query" + "in": "path" }, { - "description": "The tag/version of the service", "required": true, "schema": { - "title": "Service Tag", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", "type": "string", - "description": "The tag/version of the service" + "format": "uuid", + "title": "Node Uuid" }, - "example": "1.0.0", - "name": "service_tag", - "in": "query" + "name": "node_uuid", + "in": "path" }, { - "description": "The uuid to assign the service with", "required": true, "schema": { - "title": "Service Uuid", - "type": "string", - "description": "The uuid to assign the service with" - }, - "name": "service_uuid", - "in": "query" - }, - { - "description": "predefined basepath for the backend service otherwise uses root", - "required": false, - "schema": { - "title": "Service Base Path", - "type": "string", - "description": "predefined basepath for the backend service otherwise uses root", - "default": "" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "example": "/x/EycCXbU0H/", - "name": "service_base_path", + "name": "user_id", "in": "query" } ], "responses": { - "201": { + "200": { "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/TaskLogFileGet" + } } } }, @@ -385,27 +376,50 @@ } } }, - "/v0/running_interactive_services/{service_uuid}": { - "delete": { + "/v2/dynamic_services": { + "get": { "tags": [ - "services" + "dynamic services" ], - "summary": "Stop Interactive Service", - "operationId": "stop_interactive_service_v0_running_interactive_services__service_uuid__delete", + "summary": "returns a list of running interactive services filtered by user_id and/or project_idboth legacy (director-v0) and modern (director-v2)", + "operationId": "list_tracked_dynamic_services_v2_dynamic_services_get", "parameters": [ { - "required": true, + "required": false, "schema": { - "title": "Service Uuid", - "type": "string" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "service_uuid", - "in": "path" + "name": "user_id", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "format": "uuid", + "title": "Project Id" + }, + "name": "project_id", + "in": "query" } ], "responses": { - "204": { - "description": "Successful Response" + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/RunningDynamicServiceDetails" + }, + "type": "array", + "title": "Response List Tracked Dynamic Services V2 Dynamic Services Get" + } + } + } }, "422": { "description": "Validation Error", @@ -418,20 +432,47 @@ } } } - } - }, - "/v2/computations": { + }, "post": { "tags": [ - "computations" + "dynamic services" + ], + "summary": "creates & starts the dynamic service", + "operationId": "create_dynamic_service_v2_dynamic_services_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "X-Dynamic-Sidecar-Request-Dns" + }, + "name": "x-dynamic-sidecar-request-dns", + "in": "header" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "X-Dynamic-Sidecar-Request-Scheme" + }, + "name": "x-dynamic-sidecar-request-scheme", + "in": "header" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "X-Simcore-User-Agent" + }, + "name": "x-simcore-user-agent", + "in": "header" + } ], - "summary": "Create and optionally start a new computation", - "operationId": "create_computation_v2_computations_post", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ComputationCreate" + "$ref": "#/components/schemas/DynamicServiceCreate" } } }, @@ -443,7 +484,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ComputationGet" + "$ref": "#/components/schemas/RunningDynamicServiceDetails" } } } @@ -461,34 +502,23 @@ } } }, - "/v2/computations/{project_id}": { + "/v2/dynamic_services/{node_uuid}": { "get": { "tags": [ - "computations" + "dynamic services" ], - "summary": "Returns a computation pipeline state", - "operationId": "get_computation_v2_computations__project_id__get", + "summary": "assembles the status for the dynamic-sidecar", + "operationId": "get_dynamic_sidecar_status_v2_dynamic_services__node_uuid__get", "parameters": [ { "required": true, "schema": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, - "name": "project_id", + "name": "node_uuid", "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], "responses": { @@ -497,7 +527,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ComputationGet" + "$ref": "#/components/schemas/RunningDynamicServiceDetails" } } } @@ -516,32 +546,32 @@ }, "delete": { "tags": [ - "computations" + "dynamic services" ], - "summary": "Deletes a computation pipeline", - "operationId": "delete_computation_v2_computations__project_id__delete", + "summary": "stops previously spawned dynamic-sidecar", + "operationId": "stop_dynamic_service_v2_dynamic_services__node_uuid__delete", "parameters": [ { "required": true, "schema": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, - "name": "project_id", + "name": "node_uuid", "in": "path" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Can Save", + "default": true + }, + "name": "can_save", + "in": "query" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ComputationDelete" - } - } - }, - "required": true - }, "responses": { "204": { "description": "Successful Response" @@ -559,22 +589,22 @@ } } }, - "/v2/computations/{project_id}:stop": { + "/v2/dynamic_services/{node_uuid}:retrieve": { "post": { "tags": [ - "computations" + "dynamic services" ], - "summary": "Stops a computation pipeline", - "operationId": "stop_computation_v2_computations__project_id__stop_post", + "summary": "Calls the dynamic service's retrieve endpoint with optional port_keys", + "operationId": "service_retrieve_data_on_ports_v2_dynamic_services__node_uuid__retrieve_post", "parameters": [ { "required": true, "schema": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, - "name": "project_id", + "name": "node_uuid", "in": "path" } ], @@ -582,19 +612,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ComputationStop" + "$ref": "#/components/schemas/RetrieveDataIn" } } }, "required": true }, "responses": { - "202": { + "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ComputationGet" + "$ref": "#/components/schemas/RetrieveDataOutEnveloped" } } } @@ -612,51 +642,28 @@ } } }, - "/v2/computations/{project_id}/tasks/-/logfile": { - "get": { + "/v2/dynamic_services/{node_uuid}:restart": { + "post": { "tags": [ - "computations" + "dynamic services" ], - "summary": "Gets computation task logs file after is done", - "description": "Returns download links to log-files of each task in a computation.\nEach log is only available when the corresponding task is done", - "operationId": "get_all_tasks_log_files_v2_computations__project_id__tasks___logfile_get", + "summary": "Calls the dynamic service's restart containers endpoint", + "operationId": "service_restart_containers_v2_dynamic_services__node_uuid__restart_post", "parameters": [ { "required": true, "schema": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, - "name": "project_id", + "name": "node_uuid", "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Get All Tasks Log Files V2 Computations Project Id Tasks Logfile Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TaskLogFileGet" - } - } - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -671,57 +678,28 @@ } } }, - "/v2/computations/{project_id}/tasks/{node_uuid}/logfile": { - "get": { + "/v2/dynamic_services/projects/{project_id}/-/networks": { + "patch": { "tags": [ - "computations" + "dynamic services" ], - "summary": "Gets computation task logs file after is done", - "description": "Returns a link to download logs file of a give task.\nThe log is only available when the task is done", - "operationId": "get_task_log_file_v2_computations__project_id__tasks__node_uuid__logfile_get", + "summary": "Updates the project networks according to the current project's workbench", + "operationId": "update_projects_networks_v2_dynamic_services_projects__project_id____networks_patch", "parameters": [ { "required": true, "schema": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Project Id" }, "name": "project_id", "in": "path" - }, - { - "required": true, - "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" - }, - "name": "node_uuid", - "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TaskLogFileGet" - } - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -736,34 +714,24 @@ } } }, - "/v2/dynamic_services": { + "/v2/clusters": { "get": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "returns a list of running interactive services filtered by user_id and/or project_idboth legacy (director-v0) and modern (director-v2)", - "operationId": "list_tracked_dynamic_services_v2_dynamic_services_get", + "summary": "Lists clusters for user", + "operationId": "list_clusters_v2_clusters_get", "parameters": [ { - "required": false, + "required": true, "schema": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "name": "user_id", "in": "query" - }, - { - "required": false, - "schema": { - "title": "Project Id", - "type": "string", - "format": "uuid" - }, - "name": "project_id", - "in": "query" } ], "responses": { @@ -772,11 +740,11 @@ "content": { "application/json": { "schema": { - "title": "Response List Tracked Dynamic Services V2 Dynamic Services Get", - "type": "array", "items": { - "$ref": "#/components/schemas/RunningDynamicServiceDetails" - } + "$ref": "#/components/schemas/ClusterGet" + }, + "type": "array", + "title": "Response List Clusters V2 Clusters Get" } } } @@ -795,44 +763,28 @@ }, "post": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "creates & starts the dynamic service", - "operationId": "create_dynamic_service_v2_dynamic_services_post", + "summary": "Create a new cluster for a user", + "operationId": "create_cluster_v2_clusters_post", "parameters": [ { "required": true, "schema": { - "title": "X-Dynamic-Sidecar-Request-Dns", - "type": "string" - }, - "name": "x-dynamic-sidecar-request-dns", - "in": "header" - }, - { - "required": true, - "schema": { - "title": "X-Dynamic-Sidecar-Request-Scheme", - "type": "string" - }, - "name": "x-dynamic-sidecar-request-scheme", - "in": "header" - }, - { - "required": true, - "schema": { - "title": "X-Simcore-User-Agent", - "type": "string" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "x-simcore-user-agent", - "in": "header" + "name": "user_id", + "in": "query" } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DynamicServiceCreate" + "$ref": "#/components/schemas/ClusterCreate" } } }, @@ -844,7 +796,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RunningDynamicServiceDetails" + "$ref": "#/components/schemas/ClusterGet" } } } @@ -862,23 +814,55 @@ } } }, - "/v2/dynamic_services/{node_uuid}": { + "/v2/clusters/default": { "get": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "assembles the status for the dynamic-sidecar", - "operationId": "get_dynamic_sidecar_status_v2_dynamic_services__node_uuid__get", + "summary": "Returns the default cluster", + "operationId": "get_default_cluster_v2_clusters_default_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClusterGet" + } + } + } + } + } + } + }, + "/v2/clusters/{cluster_id}": { + "get": { + "tags": [ + "clusters" + ], + "summary": "Get one cluster for user", + "operationId": "get_cluster_v2_clusters__cluster_id__get", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" + "type": "integer", + "minimum": 0, + "title": "Cluster Id" }, - "name": "node_uuid", + "name": "cluster_id", "in": "path" + }, + { + "required": true, + "schema": { + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 + }, + "name": "user_id", + "in": "query" } ], "responses": { @@ -887,7 +871,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RunningDynamicServiceDetails" + "$ref": "#/components/schemas/ClusterGet" } } } @@ -906,29 +890,30 @@ }, "delete": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "stops previously spawned dynamic-sidecar", - "operationId": "stop_dynamic_service_v2_dynamic_services__node_uuid__delete", + "summary": "Remove a cluster for user", + "operationId": "delete_cluster_v2_clusters__cluster_id__delete", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" + "type": "integer", + "minimum": 0, + "title": "Cluster Id" }, - "name": "node_uuid", + "name": "cluster_id", "in": "path" }, { - "required": false, + "required": true, "schema": { - "title": "Can Save", - "type": "boolean", - "default": true + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "can_save", + "name": "user_id", "in": "query" } ], @@ -947,32 +932,41 @@ } } } - } - }, - "/v2/dynamic_services/{node_uuid}:retrieve": { - "post": { + }, + "patch": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "Calls the dynamic service's retrieve endpoint with optional port_keys", - "operationId": "service_retrieve_data_on_ports_v2_dynamic_services__node_uuid__retrieve_post", + "summary": "Modify a cluster for user", + "operationId": "update_cluster_v2_clusters__cluster_id__patch", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" + "type": "integer", + "minimum": 0, + "title": "Cluster Id" }, - "name": "node_uuid", + "name": "cluster_id", "in": "path" + }, + { + "required": true, + "schema": { + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 + }, + "name": "user_id", + "in": "query" } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RetrieveDataIn" + "$ref": "#/components/schemas/ClusterPatch" } } }, @@ -984,7 +978,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RetrieveDataOutEnveloped" + "$ref": "#/components/schemas/ClusterGet" } } } @@ -1002,64 +996,36 @@ } } }, - "/v2/dynamic_services/{node_uuid}:restart": { - "post": { + "/v2/clusters/default/details": { + "get": { "tags": [ - "dynamic services" + "clusters" ], - "summary": "Calls the dynamic service's restart containers endpoint", - "operationId": "service_restart_containers_v2_dynamic_services__node_uuid__restart_post", + "summary": "Returns the cluster details", + "operationId": "get_default_cluster_details_v2_clusters_default_details_get", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 }, - "name": "node_uuid", - "in": "path" + "name": "user_id", + "in": "query" } ], "responses": { - "204": { - "description": "Successful Response" - }, - "422": { - "description": "Validation Error", + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ClusterDetailsGet" } } } - } - } - } - }, - "/v2/dynamic_services/projects/{project_id}/-/networks": { - "patch": { - "tags": [ - "dynamic services" - ], - "summary": "Updates the project networks according to the current project's workbench", - "operationId": "update_projects_networks_v2_dynamic_services_projects__project_id____networks_patch", - "parameters": [ - { - "required": true, - "schema": { - "title": "Project Id", - "type": "string", - "format": "uuid" - }, - "name": "project_id", - "in": "path" - } - ], - "responses": { - "204": { - "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -1074,20 +1040,30 @@ } } }, - "/v2/clusters": { + "/v2/clusters/{cluster_id}/details": { "get": { "tags": [ "clusters" ], - "summary": "Lists clusters for user", - "operationId": "list_clusters_v2_clusters_get", + "summary": "Returns the cluster details", + "operationId": "get_cluster_details_v2_clusters__cluster_id__details_get", "parameters": [ { "required": true, "schema": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "minimum": 0, + "title": "Cluster Id" + }, + "name": "cluster_id", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "name": "user_id", @@ -1100,11 +1076,7 @@ "content": { "application/json": { "schema": { - "title": "Response List Clusters V2 Clusters Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/ClusterGet" - } + "$ref": "#/components/schemas/ClusterDetailsGet" } } } @@ -1120,46 +1092,28 @@ } } } - }, + } + }, + "/v2/clusters:ping": { "post": { "tags": [ "clusters" ], - "summary": "Create a new cluster for a user", - "operationId": "create_cluster_v2_clusters_post", - "parameters": [ - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" - } - ], + "summary": "Test cluster connection", + "operationId": "test_cluster_connection_v2_clusters_ping_post", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClusterCreate" + "$ref": "#/components/schemas/ClusterPing" } } }, "required": true }, "responses": { - "201": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterGet" - } - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -1174,41 +1128,34 @@ } } }, - "/v2/clusters/default": { - "get": { + "/v2/clusters/default:ping": { + "post": { "tags": [ "clusters" ], - "summary": "Returns the default cluster", - "operationId": "get_default_cluster_v2_clusters_default_get", + "summary": "Test cluster connection", + "operationId": "test_default_cluster_connection_v2_clusters_default_ping_post", "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterGet" - } - } - } + "204": { + "description": "Successful Response" } } } }, - "/v2/clusters/{cluster_id}": { - "get": { + "/v2/clusters/{cluster_id}:ping": { + "post": { "tags": [ "clusters" ], - "summary": "Get one cluster for user", - "operationId": "get_cluster_v2_clusters__cluster_id__get", + "summary": "Test cluster connection", + "operationId": "test_specific_cluster_connection_v2_clusters__cluster_id__ping_post", "parameters": [ { "required": true, "schema": { - "title": "Cluster Id", + "type": "integer", "minimum": 0, - "type": "integer" + "title": "Cluster Id" }, "name": "cluster_id", "in": "path" @@ -1216,9 +1163,9 @@ { "required": true, "schema": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "name": "user_id", @@ -1226,15 +1173,8 @@ } ], "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterGet" - } - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -1247,36 +1187,37 @@ } } } - }, - "delete": { + } + }, + "/v2/dynamic_scheduler/services/{node_uuid}/observation": { + "patch": { "tags": [ - "clusters" + "dynamic scheduler" ], - "summary": "Remove a cluster for user", - "operationId": "delete_cluster_v2_clusters__cluster_id__delete", + "summary": "Enable/disable observation of the service", + "operationId": "update_service_observation_v2_dynamic_scheduler_services__node_uuid__observation_patch", "parameters": [ { "required": true, "schema": { - "title": "Cluster Id", - "minimum": 0, - "type": "integer" + "type": "string", + "format": "uuid", + "title": "Node Uuid" }, - "name": "cluster_id", + "name": "node_uuid", "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObservationItem" + } + } + }, + "required": true + }, "responses": { "204": { "description": "Successful Response" @@ -1292,100 +1233,41 @@ } } } - }, - "patch": { + } + }, + "/v2/dynamic_scheduler/services/{node_uuid}/containers": { + "delete": { "tags": [ - "clusters" + "dynamic scheduler" ], - "summary": "Modify a cluster for user", - "operationId": "update_cluster_v2_clusters__cluster_id__patch", + "summary": "Removes the service's user services", + "operationId": "delete_service_containers_v2_dynamic_scheduler_services__node_uuid__containers_delete", "parameters": [ { "required": true, "schema": { - "title": "Cluster Id", - "minimum": 0, - "type": "integer" + "type": "string", + "format": "uuid", + "title": "Node Uuid" }, - "name": "cluster_id", + "name": "node_uuid", "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterPatch" - } - } - }, - "required": true - }, "responses": { - "200": { + "202": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClusterGet" + "type": "string", + "title": "Response Delete Service Containers V2 Dynamic Scheduler Services Node Uuid Containers Delete" } } } }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v2/clusters/default/details": { - "get": { - "tags": [ - "clusters" - ], - "summary": "Returns the cluster details", - "operationId": "get_default_cluster_details_v2_clusters_default_details_get", - "parameters": [ - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterDetailsGet" - } - } - } + "409": { + "description": "Task already running, cannot start a new one" }, "422": { "description": "Validation Error", @@ -1400,80 +1282,39 @@ } } }, - "/v2/clusters/{cluster_id}/details": { - "get": { + "/v2/dynamic_scheduler/services/{node_uuid}/state:save": { + "post": { "tags": [ - "clusters" + "dynamic scheduler" ], - "summary": "Returns the cluster details", - "operationId": "get_cluster_details_v2_clusters__cluster_id__details_get", + "summary": "Starts the saving of the state for the service", + "operationId": "save_service_state_v2_dynamic_scheduler_services__node_uuid__state_save_post", "parameters": [ { "required": true, "schema": { - "title": "Cluster Id", - "minimum": 0, - "type": "integer" + "type": "string", + "format": "uuid", + "title": "Node Uuid" }, - "name": "cluster_id", + "name": "node_uuid", "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" } ], "responses": { - "200": { + "202": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClusterDetailsGet" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "type": "string", + "title": "Response Save Service State V2 Dynamic Scheduler Services Node Uuid State Save Post" } } } - } - } - } - }, - "/v2/clusters:ping": { - "post": { - "tags": [ - "clusters" - ], - "summary": "Test cluster connection", - "operationId": "test_cluster_connection_v2_clusters_ping_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClusterPing" - } - } }, - "required": true - }, - "responses": { - "204": { - "description": "Successful Response" + "409": { + "description": "Task already running, cannot start a new one" }, "422": { "description": "Validation Error", @@ -1488,127 +1329,20 @@ } } }, - "/v2/clusters/default:ping": { - "post": { - "tags": [ - "clusters" - ], - "summary": "Test cluster connection", - "operationId": "test_default_cluster_connection_v2_clusters_default_ping_post", - "responses": { - "204": { - "description": "Successful Response" - } - } - } - }, - "/v2/clusters/{cluster_id}:ping": { + "/v2/dynamic_scheduler/services/{node_uuid}/outputs:push": { "post": { - "tags": [ - "clusters" - ], - "summary": "Test cluster connection", - "operationId": "test_specific_cluster_connection_v2_clusters__cluster_id__ping_post", - "parameters": [ - { - "required": true, - "schema": { - "title": "Cluster Id", - "minimum": 0, - "type": "integer" - }, - "name": "cluster_id", - "in": "path" - }, - { - "required": true, - "schema": { - "title": "User Id", - "exclusiveMinimum": true, - "type": "integer", - "minimum": 0 - }, - "name": "user_id", - "in": "query" - } - ], - "responses": { - "204": { - "description": "Successful Response" - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v2/dynamic_scheduler/services/{node_uuid}/observation": { - "patch": { - "tags": [ - "dynamic scheduler" - ], - "summary": "Enable/disable observation of the service", - "operationId": "update_service_observation_v2_dynamic_scheduler_services__node_uuid__observation_patch", - "parameters": [ - { - "required": true, - "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" - }, - "name": "node_uuid", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObservationItem" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "Successful Response" - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v2/dynamic_scheduler/services/{node_uuid}/containers": { - "delete": { "tags": [ "dynamic scheduler" ], - "summary": "Removes the service's user services", - "operationId": "delete_service_containers_v2_dynamic_scheduler_services__node_uuid__containers_delete", + "summary": "Starts the pushing of the outputs for the service", + "operationId": "push_service_outputs_v2_dynamic_scheduler_services__node_uuid__outputs_push_post", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, "name": "node_uuid", "in": "path" @@ -1620,8 +1354,8 @@ "content": { "application/json": { "schema": { - "title": "Response Delete Service Containers V2 Dynamic Scheduler Services Node Uuid Containers Delete", - "type": "string" + "type": "string", + "title": "Response Push Service Outputs V2 Dynamic Scheduler Services Node Uuid Outputs Push Post" } } } @@ -1642,20 +1376,20 @@ } } }, - "/v2/dynamic_scheduler/services/{node_uuid}/state:save": { - "post": { + "/v2/dynamic_scheduler/services/{node_uuid}/docker-resources": { + "delete": { "tags": [ "dynamic scheduler" ], - "summary": "Starts the saving of the state for the service", - "operationId": "save_service_state_v2_dynamic_scheduler_services__node_uuid__state_save_post", + "summary": "Removes the service's sidecar, proxy and docker networks & volumes", + "operationId": "delete_service_docker_resources_v2_dynamic_scheduler_services__node_uuid__docker_resources_delete", "parameters": [ { "required": true, "schema": { - "title": "Node Uuid", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Node Uuid" }, "name": "node_uuid", "in": "path" @@ -1667,102 +1401,8 @@ "content": { "application/json": { "schema": { - "title": "Response Save Service State V2 Dynamic Scheduler Services Node Uuid State Save Post", - "type": "string" - } - } - } - }, - "409": { - "description": "Task already running, cannot start a new one" - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v2/dynamic_scheduler/services/{node_uuid}/outputs:push": { - "post": { - "tags": [ - "dynamic scheduler" - ], - "summary": "Starts the pushing of the outputs for the service", - "operationId": "push_service_outputs_v2_dynamic_scheduler_services__node_uuid__outputs_push_post", - "parameters": [ - { - "required": true, - "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" - }, - "name": "node_uuid", - "in": "path" - } - ], - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Push Service Outputs V2 Dynamic Scheduler Services Node Uuid Outputs Push Post", - "type": "string" - } - } - } - }, - "409": { - "description": "Task already running, cannot start a new one" - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v2/dynamic_scheduler/services/{node_uuid}/docker-resources": { - "delete": { - "tags": [ - "dynamic scheduler" - ], - "summary": "Removes the service's sidecar, proxy and docker networks & volumes", - "operationId": "delete_service_docker_resources_v2_dynamic_scheduler_services__node_uuid__docker_resources_delete", - "parameters": [ - { - "required": true, - "schema": { - "title": "Node Uuid", - "type": "string", - "format": "uuid" - }, - "name": "node_uuid", - "in": "path" - } - ], - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Delete Service Docker Resources V2 Dynamic Scheduler Services Node Uuid Docker Resources Delete", - "type": "string" + "type": "string", + "title": "Response Delete Service Docker Resources V2 Dynamic Scheduler Services Node Uuid Docker Resources Delete" } } } @@ -1786,198 +1426,79 @@ }, "components": { "schemas": { - "Author": { - "title": "Author", - "required": [ - "name", - "email" - ], - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string", - "description": "Name of the author", - "example": "Jim Knopf" - }, - "email": { - "title": "Email", - "type": "string", - "description": "Email address", - "format": "email" - }, - "affiliation": { - "title": "Affiliation", - "type": "string", - "description": "Affiliation of the author" - } - }, - "additionalProperties": false - }, - "Badge": { - "title": "Badge", - "required": [ - "name", - "image", - "url" - ], - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string", - "description": "Name of the subject" - }, - "image": { - "title": "Image", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "description": "Url to the badge", - "format": "uri" - }, - "url": { - "title": "Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "description": "Link to the status", - "format": "uri" - } - }, - "additionalProperties": false - }, - "BootChoice": { - "title": "BootChoice", - "required": [ - "label", - "description" - ], - "type": "object", - "properties": { - "label": { - "title": "Label", - "type": "string" - }, - "description": { - "title": "Description", - "type": "string" - } - } - }, "BootMode": { - "title": "BootMode", + "type": "string", "enum": [ "CPU", "GPU", "MPI" ], - "type": "string", + "title": "BootMode", "description": "An enumeration." }, - "BootOption": { - "title": "BootOption", - "required": [ - "label", - "description", - "default", - "items" - ], - "type": "object", - "properties": { - "label": { - "title": "Label", - "type": "string" - }, - "description": { - "title": "Description", - "type": "string" - }, - "default": { - "title": "Default", - "type": "string" - }, - "items": { - "title": "Items", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/BootChoice" - } - } - } - }, "ClusterAccessRights": { - "title": "ClusterAccessRights", - "required": [ - "read", - "write", - "delete" - ], - "type": "object", "properties": { "read": { - "title": "Read", "type": "boolean", + "title": "Read", "description": "allows to run pipelines on that cluster" }, "write": { - "title": "Write", "type": "boolean", + "title": "Write", "description": "allows to modify the cluster" }, "delete": { - "title": "Delete", "type": "boolean", + "title": "Delete", "description": "allows to delete a cluster" } }, - "additionalProperties": false - }, - "ClusterCreate": { - "title": "ClusterCreate", + "additionalProperties": false, + "type": "object", "required": [ - "name", - "type", - "endpoint", - "authentication" + "read", + "write", + "delete" ], - "type": "object", + "title": "ClusterAccessRights" + }, + "ClusterCreate": { "properties": { "name": { - "title": "Name", "type": "string", + "title": "Name", "description": "The human readable name of the cluster" }, "description": { - "title": "Description", - "type": "string" + "type": "string", + "title": "Description" }, "type": { - "$ref": "#/components/schemas/ClusterType" + "$ref": "#/components/schemas/ClusterTypeInModel" }, "owner": { - "title": "Owner", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "Owner", "minimum": 0 }, "thumbnail": { - "title": "Thumbnail", + "type": "string", "maxLength": 2083, "minLength": 1, - "type": "string", - "description": "url to the image describing this cluster", - "format": "uri" + "format": "uri", + "title": "Thumbnail", + "description": "url to the image describing this cluster" }, "endpoint": { - "title": "Endpoint", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "format": "uri" + "format": "uri", + "title": "Endpoint" }, "authentication": { - "title": "Authentication", "anyOf": [ { "$ref": "#/components/schemas/SimpleAuthentication" @@ -1988,92 +1509,90 @@ { "$ref": "#/components/schemas/JupyterHubTokenAuthentication" } - ] + ], + "title": "Authentication" }, "accessRights": { - "title": "Accessrights", - "type": "object", "additionalProperties": { "$ref": "#/components/schemas/ClusterAccessRights" - } + }, + "type": "object", + "title": "Accessrights" } }, - "additionalProperties": false - }, - "ClusterDetailsGet": { - "title": "ClusterDetailsGet", + "additionalProperties": false, + "type": "object", "required": [ - "scheduler", - "dashboard_link" + "name", + "type", + "endpoint", + "authentication" ], - "type": "object", + "title": "ClusterCreate" + }, + "ClusterDetailsGet": { "properties": { "scheduler": { - "title": "Scheduler", "allOf": [ { "$ref": "#/components/schemas/Scheduler" } ], + "title": "Scheduler", "description": "This contains dask scheduler information given by the underlying dask library" }, "dashboard_link": { - "title": "Dashboard Link", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "description": "Link to this scheduler's dashboard", - "format": "uri" + "format": "uri", + "title": "Dashboard Link", + "description": "Link to this scheduler's dashboard" } - } - }, - "ClusterGet": { - "title": "ClusterGet", + }, + "type": "object", "required": [ - "name", - "type", - "owner", - "endpoint", - "authentication", - "id" + "scheduler", + "dashboard_link" ], - "type": "object", + "title": "ClusterDetailsGet" + }, + "ClusterGet": { "properties": { "name": { - "title": "Name", "type": "string", + "title": "Name", "description": "The human readable name of the cluster" }, "description": { - "title": "Description", - "type": "string" + "type": "string", + "title": "Description" }, "type": { - "$ref": "#/components/schemas/ClusterType" + "$ref": "#/components/schemas/ClusterTypeInModel" }, "owner": { - "title": "Owner", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "Owner", "minimum": 0 }, "thumbnail": { - "title": "Thumbnail", + "type": "string", "maxLength": 2083, "minLength": 1, - "type": "string", - "description": "url to the image describing this cluster", - "format": "uri" + "format": "uri", + "title": "Thumbnail", + "description": "url to the image describing this cluster" }, "endpoint": { - "title": "Endpoint", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "format": "uri" + "format": "uri", + "title": "Endpoint" }, "authentication": { - "title": "Authentication", "anyOf": [ { "$ref": "#/components/schemas/SimpleAuthentication" @@ -2088,61 +1607,69 @@ "$ref": "#/components/schemas/NoAuthentication" } ], + "title": "Authentication", "description": "Dask gateway authentication" }, "accessRights": { - "title": "Accessrights", - "type": "object", "additionalProperties": { "$ref": "#/components/schemas/ClusterAccessRights" - } + }, + "type": "object", + "title": "Accessrights" }, "id": { - "title": "Id", - "minimum": 0, "type": "integer", + "minimum": 0, + "title": "Id", "description": "The cluster ID" } }, - "additionalProperties": false + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "type", + "owner", + "endpoint", + "authentication", + "id" + ], + "title": "ClusterGet" }, "ClusterPatch": { - "title": "ClusterPatch", - "type": "object", "properties": { "name": { - "title": "Name", - "type": "string" + "type": "string", + "title": "Name" }, "description": { - "title": "Description", - "type": "string" + "type": "string", + "title": "Description" }, "type": { - "$ref": "#/components/schemas/ClusterType" + "$ref": "#/components/schemas/ClusterTypeInModel" }, "owner": { - "title": "Owner", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "Owner", "minimum": 0 }, "thumbnail": { - "title": "Thumbnail", + "type": "string", "maxLength": 2083, "minLength": 1, - "type": "string", - "format": "uri" + "format": "uri", + "title": "Thumbnail" }, "endpoint": { - "title": "Endpoint", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "format": "uri" + "format": "uri", + "title": "Endpoint" }, "authentication": { - "title": "Authentication", "anyOf": [ { "$ref": "#/components/schemas/SimpleAuthentication" @@ -2153,35 +1680,31 @@ { "$ref": "#/components/schemas/JupyterHubTokenAuthentication" } - ] + ], + "title": "Authentication" }, "accessRights": { - "title": "Accessrights", - "type": "object", "additionalProperties": { "$ref": "#/components/schemas/ClusterAccessRights" - } + }, + "type": "object", + "title": "Accessrights" } }, - "additionalProperties": false + "additionalProperties": false, + "type": "object", + "title": "ClusterPatch" }, "ClusterPing": { - "title": "ClusterPing", - "required": [ - "endpoint", - "authentication" - ], - "type": "object", "properties": { "endpoint": { - "title": "Endpoint", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "format": "uri" + "format": "uri", + "title": "Endpoint" }, "authentication": { - "title": "Authentication", "anyOf": [ { "$ref": "#/components/schemas/SimpleAuthentication" @@ -2196,112 +1719,128 @@ "$ref": "#/components/schemas/NoAuthentication" } ], + "title": "Authentication", "description": "Dask gateway authentication" } - } + }, + "type": "object", + "required": [ + "endpoint", + "authentication" + ], + "title": "ClusterPing" }, - "ClusterType": { - "title": "ClusterType", + "ClusterTypeInModel": { + "type": "string", "enum": [ "AWS", - "ON_PREMISE" + "ON_PREMISE", + "ON_DEMAND" ], + "title": "ClusterTypeInModel", "description": "An enumeration." }, "ComputationCreate": { - "title": "ComputationCreate", - "required": [ - "user_id", - "project_id", - "product_name" - ], - "type": "object", "properties": { "user_id": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "project_id": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Project Id" }, "start_pipeline": { - "title": "Start Pipeline", "type": "boolean", + "title": "Start Pipeline", "description": "if True the computation pipeline will start right away", "default": false }, "product_name": { - "title": "Product Name", - "type": "string" + "type": "string", + "title": "Product Name" }, "subgraph": { - "title": "Subgraph", - "type": "array", "items": { "type": "string", "format": "uuid" }, + "type": "array", + "title": "Subgraph", "description": "An optional set of nodes that must be executed, if empty the whole pipeline is executed" }, "force_restart": { - "title": "Force Restart", "type": "boolean", + "title": "Force Restart", "description": "if True will force re-running all dependent nodes", "default": false }, "cluster_id": { - "title": "Cluster Id", - "minimum": 0, "type": "integer", + "minimum": 0, + "title": "Cluster Id", "description": "the computation shall use the cluster described by its id, 0 is the default cluster" + }, + "simcore_user_agent": { + "type": "string", + "title": "Simcore User Agent", + "default": "" + }, + "use_on_demand_clusters": { + "type": "boolean", + "title": "Use On Demand Clusters", + "description": "if True, a cluster will be created as necessary (wallet_id cannot be None, and cluster_id must be None)", + "default": false + }, + "wallet_info": { + "allOf": [ + { + "$ref": "#/components/schemas/WalletInfo" + } + ], + "title": "Wallet Info", + "description": "contains information about the wallet used to bill the running service" } - } - }, - "ComputationDelete": { - "title": "ComputationDelete", + }, + "type": "object", "required": [ - "user_id" + "user_id", + "project_id", + "product_name" ], - "type": "object", + "title": "ComputationCreate" + }, + "ComputationDelete": { "properties": { "user_id": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "force": { - "title": "Force", "type": "boolean", + "title": "Force", "description": "if True then the pipeline will be removed even if it is running", "default": false } - } - }, - "ComputationGet": { - "title": "ComputationGet", + }, + "type": "object", "required": [ - "id", - "state", - "pipeline_details", - "iteration", - "cluster_id", - "started", - "stopped", - "submitted", - "url" + "user_id" ], - "type": "object", + "title": "ComputationDelete" + }, + "ComputationGet": { "properties": { "id": { - "title": "Id", "type": "string", - "description": "the id of the computation task", - "format": "uuid" + "format": "uuid", + "title": "Id", + "description": "the id of the computation task" }, "state": { "allOf": [ @@ -2312,176 +1851,180 @@ "description": "the state of the computational task" }, "result": { - "title": "Result", "type": "string", + "title": "Result", "description": "the result of the computational task" }, "pipeline_details": { - "title": "Pipeline Details", "allOf": [ { "$ref": "#/components/schemas/PipelineDetails" } ], + "title": "Pipeline Details", "description": "the details of the generated pipeline" }, "iteration": { - "title": "Iteration", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "Iteration", "description": "the iteration id of the computation task (none if no task ran yet)", "minimum": 0 }, "cluster_id": { - "title": "Cluster Id", - "minimum": 0, "type": "integer", + "minimum": 0, + "title": "Cluster Id", "description": "the cluster on which the computaional task runs/ran (none if no task ran yet)" }, "started": { - "title": "Started", "type": "string", - "description": "the timestamp when the computation was started or None if not started yet", - "format": "date-time" + "format": "date-time", + "title": "Started", + "description": "the timestamp when the computation was started or None if not started yet" }, "stopped": { - "title": "Stopped", "type": "string", - "description": "the timestamp when the computation was stopped or None if not started nor stopped yet", - "format": "date-time" + "format": "date-time", + "title": "Stopped", + "description": "the timestamp when the computation was stopped or None if not started nor stopped yet" }, "submitted": { - "title": "Submitted", "type": "string", - "description": "task last modification timestamp or None if the there is no task", - "format": "date-time" + "format": "date-time", + "title": "Submitted", + "description": "task last modification timestamp or None if the there is no task" }, "url": { - "title": "Url", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "description": "the link where to get the status of the task", - "format": "uri" + "format": "uri", + "title": "Url", + "description": "the link where to get the status of the task" }, "stop_url": { - "title": "Stop Url", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "description": "the link where to stop the task", - "format": "uri" + "format": "uri", + "title": "Stop Url", + "description": "the link where to stop the task" } - } - }, - "ComputationStop": { - "title": "ComputationStop", + }, + "type": "object", "required": [ - "user_id" + "id", + "state", + "pipeline_details", + "iteration", + "cluster_id", + "started", + "stopped", + "submitted", + "url" ], - "type": "object", + "title": "ComputationGet" + }, + "ComputationStop": { "properties": { "user_id": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 } - } - }, - "ContainerSpec": { - "title": "ContainerSpec", + }, + "type": "object", "required": [ - "Command" + "user_id" ], - "type": "object", - "properties": { - "Command": { - "title": "Command", - "maxItems": 2, - "minItems": 1, - "type": "array", - "items": { - "type": "string" - }, - "description": "Used to override the container's command" - } - }, - "additionalProperties": false, - "description": "Implements entries that can be overriden for https://docs.docker.com/engine/api/v1.41/#operation/ServiceCreate\nrequest body: TaskTemplate -> ContainerSpec" + "title": "ComputationStop" }, "DictModel_str__PositiveFloat_": { - "title": "DictModel[str, PositiveFloat]", - "type": "object", "additionalProperties": { - "exclusiveMinimum": true, "type": "number", + "exclusiveMinimum": true, "minimum": 0.0 - } + }, + "type": "object", + "title": "DictModel[str, PositiveFloat]" }, "DynamicServiceCreate": { - "title": "DynamicServiceCreate", - "required": [ - "service_key", - "service_version", - "user_id", - "project_id", - "service_uuid", - "service_resources", - "product_name", - "can_save" - ], - "type": "object", "properties": { "service_key": { - "title": "Service Key", - "pattern": "^simcore/services/dynamic/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", "type": "string", + "pattern": "^simcore/services/dynamic/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "title": "Service Key", "description": "distinctive name for the node based on the docker registry path" }, "service_version": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Service Version", "description": "semantic version number of the node" }, "user_id": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "project_id": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Project Id" }, "service_uuid": { - "title": "Service Uuid", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Service Uuid" }, "service_basepath": { - "title": "Service Basepath", "type": "string", - "description": "predefined path where the dynamic service should be served. If empty, the service shall use the root endpoint.", - "format": "path" + "format": "path", + "title": "Service Basepath", + "description": "predefined path where the dynamic service should be served. If empty, the service shall use the root endpoint." }, "service_resources": { - "title": "Service Resources", - "type": "object" + "additionalProperties": { + "$ref": "#/components/schemas/ImageResources" + }, + "type": "object", + "title": "Service Resources" }, "product_name": { - "title": "Product Name", "type": "string", + "title": "Product Name", "description": "Current product name" }, "can_save": { - "title": "Can Save", "type": "boolean", + "title": "Can Save", "description": "the service data must be saved when closing" + }, + "wallet_info": { + "allOf": [ + { + "$ref": "#/components/schemas/WalletInfo" + } + ], + "title": "Wallet Info", + "description": "contains information about the wallet used to bill the running service" } }, + "type": "object", + "required": [ + "service_key", + "service_version", + "user_id", + "project_id", + "service_uuid", + "service_resources", + "product_name", + "can_save" + ], + "title": "DynamicServiceCreate", "example": { "key": "simcore/services/dynamic/3dviewer", "version": "2.4.5", @@ -2508,70 +2051,74 @@ "CPU" ] } + }, + "wallet_info": { + "wallet_id": 1, + "wallet_name": "My Wallet" } } }, "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", "properties": { "errors": { - "title": "Validation errors", - "type": "array", "items": { "$ref": "#/components/schemas/ValidationError" - } + }, + "type": "array", + "title": "Validation errors" } - } + }, + "type": "object", + "title": "HTTPValidationError" }, "HealthCheckGet": { - "title": "HealthCheckGet", - "required": [ - "timestamp" - ], - "type": "object", "properties": { "timestamp": { - "title": "Timestamp", - "type": "string" + "type": "string", + "title": "Timestamp" } }, + "type": "object", + "required": [ + "timestamp" + ], + "title": "HealthCheckGet", "example": { "timestamp": "simcore_service_directorv2.api.routes.health@2023-07-03T12:59:12.024551+00:00" } }, "ImageResources": { - "title": "ImageResources", - "required": [ - "image", - "resources" - ], - "type": "object", "properties": { "image": { - "title": "Image", - "pattern": "^(?:([a-z0-9-]+(?:\\.[a-z0-9-]+)+(?::\\d+)?|[a-z0-9-]+:\\d+)/)?((?:[a-z0-9][a-z0-9_.-]*/)*[a-z0-9-_]+[a-z0-9])(?::([\\w][\\w.-]{0,127}))?(\\@sha256:[a-fA-F0-9]{32,64})?$", "type": "string", + "pattern": "^(?:([a-z0-9-]+(?:\\.[a-z0-9-]+)+(?::\\d+)?|[a-z0-9-]+:\\d+)/)?((?:[a-z0-9][a-z0-9_.-]*/)*[a-z0-9-_]+[a-z0-9])(?::([\\w][\\w.-]{0,127}))?(\\@sha256:[a-fA-F0-9]{32,64})?$", + "title": "Image", "description": "Used by the frontend to provide a context for the users.Services with a docker-compose spec will have multiple entries.Using the `image:version` instead of the docker-compose spec is more helpful for the end user." }, "resources": { - "title": "Resources", - "type": "object", "additionalProperties": { "$ref": "#/components/schemas/ResourceValue" - } + }, + "type": "object", + "title": "Resources" }, "boot_modes": { - "type": "array", "items": { "$ref": "#/components/schemas/BootMode" }, + "type": "array", "description": "describe how a service shall be booted, using CPU, MPI, openMP or GPU", "default": [ "CPU" ] } }, + "type": "object", + "required": [ + "image", + "resources" + ], + "title": "ImageResources", "example": { "image": "simcore/service/dynamic/pretty-intense:1.0.0", "resources": { @@ -2599,69 +2146,69 @@ } }, "JupyterHubTokenAuthentication": { - "title": "JupyterHubTokenAuthentication", - "required": [ - "api_token" - ], - "type": "object", "properties": { "type": { - "title": "Type", + "type": "string", "enum": [ "jupyterhub" ], - "type": "string", + "title": "Type", "default": "jupyterhub" }, "api_token": { - "title": "Api Token", - "type": "string" + "type": "string", + "title": "Api Token" } }, - "additionalProperties": false + "additionalProperties": false, + "type": "object", + "required": [ + "api_token" + ], + "title": "JupyterHubTokenAuthentication" }, "KerberosAuthentication": { - "title": "KerberosAuthentication", - "type": "object", "properties": { "type": { - "title": "Type", + "type": "string", "enum": [ "kerberos" ], - "type": "string", + "title": "Type", "default": "kerberos" } }, - "additionalProperties": false + "additionalProperties": false, + "type": "object", + "title": "KerberosAuthentication" }, "Meta": { - "title": "Meta", - "required": [ - "name", - "version" - ], - "type": "object", "properties": { "name": { - "title": "Name", - "type": "string" + "type": "string", + "title": "Name" }, "version": { - "title": "Version", + "type": "string", "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string" + "title": "Version" }, "released": { - "title": "Released", - "type": "object", "additionalProperties": { - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string" + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$" }, + "type": "object", + "title": "Released", "description": "Maps every route's path tag with a released version" } }, + "type": "object", + "required": [ + "name", + "version" + ], + "title": "Meta", "example": { "name": "simcore_service_foo", "version": "2.4.45", @@ -2672,71 +2219,36 @@ } }, "NoAuthentication": { - "title": "NoAuthentication", - "type": "object", "properties": { "type": { - "title": "Type", + "type": "string", "enum": [ "none" ], - "type": "string", + "title": "Type", "default": "none" } }, - "additionalProperties": false - }, - "NodeRequirements": { - "title": "NodeRequirements", - "required": [ - "CPU", - "RAM" - ], + "additionalProperties": false, "type": "object", - "properties": { - "CPU": { - "title": "Cpu", - "exclusiveMinimum": true, - "type": "number", - "description": "defines the required (maximum) CPU shares for running the services", - "minimum": 0.0 - }, - "GPU": { - "title": "Gpu", - "minimum": 0, - "type": "integer", - "description": "defines the required (maximum) GPU for running the services" - }, - "RAM": { - "title": "Ram", - "type": "integer", - "description": "defines the required (maximum) amount of RAM for running the services" - }, - "VRAM": { - "title": "Vram", - "type": "integer", - "description": "defines the required (maximum) amount of VRAM for running the services" - } - } + "title": "NoAuthentication" }, "NodeState": { - "title": "NodeState", - "type": "object", "properties": { "modified": { - "title": "Modified", "type": "boolean", + "title": "Modified", "description": "true if the node's outputs need to be re-computed", "default": true }, "dependencies": { - "title": "Dependencies", - "uniqueItems": true, - "type": "array", "items": { "type": "string", "format": "uuid" }, + "type": "array", + "uniqueItems": true, + "title": "Dependencies", "description": "contains the node inputs dependencies if they need to be computed first" }, "currentStatus": { @@ -2749,77 +2261,72 @@ "default": "NOT_STARTED" }, "progress": { - "title": "Progress", + "type": "number", "maximum": 1.0, "minimum": 0.0, - "type": "number", + "title": "Progress", "description": "current progress of the task if available (None if not started or not a computational task)", "default": 0 } }, - "additionalProperties": false + "additionalProperties": false, + "type": "object", + "title": "NodeState" }, "ObservationItem": { - "title": "ObservationItem", - "required": [ - "is_disabled" - ], - "type": "object", "properties": { "is_disabled": { - "title": "Is Disabled", - "type": "boolean" + "type": "boolean", + "title": "Is Disabled" } - } - }, - "PipelineDetails": { - "title": "PipelineDetails", + }, + "type": "object", "required": [ - "adjacency_list", - "progress", - "node_states" + "is_disabled" ], - "type": "object", + "title": "ObservationItem" + }, + "PipelineDetails": { "properties": { "adjacency_list": { - "title": "Adjacency List", - "type": "object", "additionalProperties": { - "type": "array", "items": { "type": "string", "format": "uuid" - } + }, + "type": "array" }, + "type": "object", + "title": "Adjacency List", "description": "The adjacency list of the current pipeline in terms of {NodeID: [successor NodeID]}" }, "progress": { - "title": "Progress", + "type": "number", "maximum": 1.0, "minimum": 0.0, - "type": "number", + "title": "Progress", "description": "the progress of the pipeline (None if there are no computational tasks)" }, "node_states": { - "title": "Node States", - "type": "object", "additionalProperties": { "$ref": "#/components/schemas/NodeState" }, + "type": "object", + "title": "Node States", "description": "The states of each of the computational nodes in the pipeline" } - } - }, - "ResourceValue": { - "title": "ResourceValue", + }, + "type": "object", "required": [ - "limit", - "reservation" + "adjacency_list", + "progress", + "node_states" ], - "type": "object", + "title": "PipelineDetails" + }, + "ResourceValue": { "properties": { "limit": { - "title": "Limit", "anyOf": [ { "type": "integer" @@ -2830,10 +2337,10 @@ { "type": "string" } - ] + ], + "title": "Limit" }, "reservation": { - "title": "Reservation", "anyOf": [ { "type": "integer" @@ -2844,101 +2351,96 @@ { "type": "string" } - ] + ], + "title": "Reservation" } - } - }, - "RetrieveDataIn": { - "title": "RetrieveDataIn", + }, + "type": "object", "required": [ - "port_keys" + "limit", + "reservation" ], - "type": "object", + "title": "ResourceValue" + }, + "RetrieveDataIn": { "properties": { "port_keys": { - "title": "Port Keys", - "type": "array", "items": { - "pattern": "^[-_a-zA-Z0-9]+$", - "type": "string" + "type": "string", + "pattern": "^[-_a-zA-Z0-9]+$" }, + "type": "array", + "title": "Port Keys", "description": "The port keys to retrieve data from" } - } - }, - "RetrieveDataOut": { - "title": "RetrieveDataOut", + }, + "type": "object", "required": [ - "size_bytes" + "port_keys" ], - "type": "object", + "title": "RetrieveDataIn" + }, + "RetrieveDataOut": { "properties": { "size_bytes": { - "title": "Size Bytes", "type": "integer", + "title": "Size Bytes", "description": "The amount of data transferred by the retrieve call" } - } - }, - "RetrieveDataOutEnveloped": { - "title": "RetrieveDataOutEnveloped", + }, + "type": "object", "required": [ - "data" + "size_bytes" ], - "type": "object", + "title": "RetrieveDataOut" + }, + "RetrieveDataOutEnveloped": { "properties": { "data": { "$ref": "#/components/schemas/RetrieveDataOut" } - } - }, - "RunningDynamicServiceDetails": { - "title": "RunningDynamicServiceDetails", + }, + "type": "object", "required": [ - "service_key", - "service_version", - "user_id", - "project_id", - "service_uuid", - "service_host", - "service_port", - "service_state" + "data" ], - "type": "object", + "title": "RetrieveDataOutEnveloped" + }, + "RunningDynamicServiceDetails": { "properties": { "service_key": { - "title": "Service Key", - "pattern": "^simcore/services/dynamic/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", "type": "string", + "pattern": "^simcore/services/dynamic/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "title": "Service Key", "description": "distinctive name for the node based on the docker registry path" }, "service_version": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Service Version", "description": "semantic version number of the node" }, "user_id": { - "title": "User Id", - "exclusiveMinimum": true, "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", "minimum": 0 }, "project_id": { - "title": "Project Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Project Id" }, "service_uuid": { - "title": "Service Uuid", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Service Uuid" }, "service_basepath": { - "title": "Service Basepath", "type": "string", - "description": "predefined path where the dynamic service should be served. If empty, the service shall use the root endpoint.", - "format": "path" + "format": "path", + "title": "Service Basepath", + "description": "predefined path where the dynamic service should be served. If empty, the service shall use the root endpoint." }, "boot_type": { "allOf": [ @@ -2950,524 +2452,110 @@ "default": "V0" }, "service_host": { - "title": "Service Host", "type": "string", + "title": "Service Host", "description": "the service swarm internal host name" }, "service_port": { - "title": "Service Port", + "type": "integer", "exclusiveMaximum": true, "exclusiveMinimum": true, - "type": "integer", + "title": "Service Port", "description": "the service swarm internal port", "maximum": 65535, "minimum": 0 }, "published_port": { - "title": "Published Port", + "type": "integer", "exclusiveMaximum": true, "exclusiveMinimum": true, - "type": "integer", + "title": "Published Port", "description": "the service swarm published port if any", "deprecated": true, "maximum": 65535, "minimum": 0 }, "entry_point": { - "title": "Entry Point", "type": "string", + "title": "Entry Point", "description": "if empty the service entrypoint is on the root endpoint.", "deprecated": true }, "service_state": { "allOf": [ { - "$ref": "#/components/schemas/ServiceState" - } - ], - "description": "service current state" - }, - "service_message": { - "title": "Service Message", - "type": "string", - "description": "additional information related to service state" - } - } - }, - "RunningServiceDetails": { - "title": "RunningServiceDetails", - "required": [ - "entry_point", - "service_uuid", - "service_key", - "service_version", - "service_host", - "service_basepath", - "service_state", - "service_message" - ], - "type": "object", - "properties": { - "published_port": { - "title": "Published Port", - "exclusiveMaximum": true, - "exclusiveMinimum": true, - "type": "integer", - "description": "The ports where the service provides its interface on the docker swarm", - "deprecated": true, - "maximum": 65535, - "minimum": 0 - }, - "entry_point": { - "title": "Entry Point", - "type": "string", - "description": "The entry point where the service provides its interface" - }, - "service_uuid": { - "title": "Service Uuid", - "pattern": "^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$", - "type": "string", - "description": "The node UUID attached to the service" - }, - "service_key": { - "title": "Service Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", - "type": "string", - "description": "distinctive name for the node based on the docker registry path", - "example": [ - "simcore/services/comp/itis/sleeper", - "simcore/services/dynamic/3dviewer" - ] - }, - "service_version": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string", - "description": "service version number", - "example": [ - "1.0.0", - "0.0.1" - ] - }, - "service_host": { - "title": "Service Host", - "type": "string", - "description": "service host name within the network" - }, - "service_port": { - "title": "Service Port", - "exclusiveMaximum": true, - "exclusiveMinimum": true, - "type": "integer", - "description": "port to access the service within the network", - "default": 80, - "maximum": 65535, - "minimum": 0 - }, - "service_basepath": { - "title": "Service Basepath", - "type": "string", - "description": "the service base entrypoint where the service serves its contents" - }, - "service_state": { - "allOf": [ - { - "$ref": "#/components/schemas/ServiceState" - } - ], - "description": "the service state * 'pending' - The service is waiting for resources to start * 'pulling' - The service is being pulled from the registry * 'starting' - The service is starting * 'running' - The service is running * 'complete' - The service completed * 'failed' - The service failed to start * 'stopping' - The service is stopping" - }, - "service_message": { - "title": "Service Message", - "type": "string", - "description": "the service message" - } - } - }, - "RunningServicesDetailsArray": { - "title": "RunningServicesDetailsArray", - "type": "array", - "items": { - "$ref": "#/components/schemas/RunningServiceDetails" - } - }, - "RunningServicesDetailsArrayEnveloped": { - "title": "RunningServicesDetailsArrayEnveloped", - "required": [ - "data" - ], - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/RunningServicesDetailsArray" - } - } - }, - "RunningState": { - "title": "RunningState", - "enum": [ - "UNKNOWN", - "PUBLISHED", - "NOT_STARTED", - "PENDING", - "STARTED", - "RETRY", - "SUCCESS", - "FAILED", - "ABORTED" - ], - "type": "string", - "description": "State of execution of a project's computational workflow\n\nSEE StateType for task state" - }, - "Scheduler": { - "title": "Scheduler", - "required": [ - "status" - ], - "type": "object", - "properties": { - "status": { - "title": "Status", - "type": "string", - "description": "The running status of the scheduler" - }, - "workers": { - "title": "Workers", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Worker" - } - } - } - }, - "SelectBox": { - "title": "SelectBox", - "required": [ - "structure" - ], - "type": "object", - "properties": { - "structure": { - "title": "Structure", - "minItems": 1, - "type": "array", - "items": { - "$ref": "#/components/schemas/Structure" - } - } - }, - "additionalProperties": false - }, - "ServiceBootType": { - "title": "ServiceBootType", - "enum": [ - "V0", - "V2" - ], - "type": "string", - "description": "An enumeration." - }, - "ServiceBuildDetails": { - "title": "ServiceBuildDetails", - "required": [ - "build_date", - "vcs_ref", - "vcs_url" - ], - "type": "object", - "properties": { - "build_date": { - "title": "Build Date", - "type": "string" - }, - "vcs_ref": { - "title": "Vcs Ref", - "type": "string" - }, - "vcs_url": { - "title": "Vcs Url", - "type": "string" - } - } - }, - "ServiceDockerData": { - "title": "ServiceDockerData", - "required": [ - "name", - "description", - "key", - "version", - "type", - "authors", - "contact", - "inputs", - "outputs" - ], - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string", - "description": "short, human readable name for the node", - "example": "Fast Counter" - }, - "thumbnail": { - "title": "Thumbnail", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "description": "url to the thumbnail", - "format": "uri" - }, - "description": { - "title": "Description", - "type": "string", - "description": "human readable description of the purpose of the node" - }, - "key": { - "title": "Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", - "type": "string", - "description": "distinctive name for the node based on the docker registry path" - }, - "version": { - "title": "Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string", - "description": "service version number" - }, - "integration-version": { - "title": "Integration-Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "type": "string", - "description": "integration version number" - }, - "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ServiceType" - } - ], - "description": "service type" - }, - "badges": { - "title": "Badges", - "type": "array", - "items": { - "$ref": "#/components/schemas/Badge" - } - }, - "authors": { - "title": "Authors", - "minItems": 1, - "type": "array", - "items": { - "$ref": "#/components/schemas/Author" - } - }, - "contact": { - "title": "Contact", - "type": "string", - "description": "email to correspond to the authors about the node", - "format": "email" - }, - "inputs": { - "title": "Inputs", - "type": "object", - "description": "definition of the inputs of this node" - }, - "outputs": { - "title": "Outputs", - "type": "object", - "description": "definition of the outputs of this node" - }, - "boot-options": { - "title": "Boot-Options", - "type": "object", - "description": "Service defined boot options. These get injected in the service as env variables." - }, - "min-visible-inputs": { - "title": "Min-Visible-Inputs", - "minimum": 0, - "type": "integer", - "description": "The number of 'data type inputs' displayed by default in the UI. When None all 'data type inputs' are displayed." - } - }, - "additionalProperties": false, - "description": "Static metadata for a service injected in the image labels\n\nThis is one to one with node-meta-v0.0.1.json" - }, - "ServiceExtras": { - "title": "ServiceExtras", - "required": [ - "node_requirements" - ], - "type": "object", - "properties": { - "node_requirements": { - "$ref": "#/components/schemas/NodeRequirements" - }, - "service_build_details": { - "$ref": "#/components/schemas/ServiceBuildDetails" - }, - "container_spec": { - "$ref": "#/components/schemas/ContainerSpec" - } - } - }, - "ServiceExtrasEnveloped": { - "title": "ServiceExtrasEnveloped", - "required": [ - "data" - ], - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ServiceExtras" - } - } - }, - "ServiceInput": { - "title": "ServiceInput", - "required": [ - "label", - "description", - "type" - ], - "type": "object", - "properties": { - "displayOrder": { - "title": "Displayorder", - "type": "number", - "description": "DEPRECATED: new display order is taken from the item position. This will be removed.", - "deprecated": true - }, - "label": { - "title": "Label", - "type": "string", - "description": "short name for the property", - "example": "Age" - }, - "description": { - "title": "Description", - "type": "string", - "description": "description of the property", - "example": "Age in seconds since 1970" - }, - "type": { - "title": "Type", - "pattern": "^(number|integer|boolean|string|ref_contentSchema|data:([^/\\s,]+/[^/\\s,]+|\\[[^/\\s,]+/[^/\\s,]+(,[^/\\s]+/[^/,\\s]+)*\\]))$", - "type": "string", - "description": "data type expected on this input glob matching for data type is allowed" - }, - "contentSchema": { - "title": "Contentschema", - "type": "object", - "description": "jsonschema of this input/output. Required when type='ref_contentSchema'" - }, - "fileToKeyMap": { - "title": "Filetokeymap", - "type": "object", - "description": "Place the data associated with the named keys in files" - }, - "unit": { - "title": "Unit", - "type": "string", - "description": "Units, when it refers to a physical quantity" - }, - "defaultValue": { - "title": "Defaultvalue", - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "widget": { - "title": "Widget", - "allOf": [ - { - "$ref": "#/components/schemas/Widget" + "$ref": "#/components/schemas/ServiceState" } ], - "description": "custom widget to use instead of the default one determined from the data-type" + "description": "service current state" + }, + "service_message": { + "type": "string", + "title": "Service Message", + "description": "additional information related to service state" } }, - "additionalProperties": false, - "description": "Metadata on a service input port" - }, - "ServiceOutput": { - "title": "ServiceOutput", + "type": "object", "required": [ - "label", - "description", - "type" + "service_key", + "service_version", + "user_id", + "project_id", + "service_uuid", + "service_host", + "service_port", + "service_state" ], - "type": "object", + "title": "RunningDynamicServiceDetails" + }, + "RunningState": { + "type": "string", + "enum": [ + "UNKNOWN", + "PUBLISHED", + "NOT_STARTED", + "PENDING", + "WAITING_FOR_RESOURCES", + "STARTED", + "SUCCESS", + "FAILED", + "ABORTED", + "WAITING_FOR_CLUSTER" + ], + "title": "RunningState", + "description": "State of execution of a project's computational workflow\n\nSEE StateType for task state" + }, + "Scheduler": { "properties": { - "displayOrder": { - "title": "Displayorder", - "type": "number", - "description": "DEPRECATED: new display order is taken from the item position. This will be removed.", - "deprecated": true - }, - "label": { - "title": "Label", - "type": "string", - "description": "short name for the property", - "example": "Age" - }, - "description": { - "title": "Description", - "type": "string", - "description": "description of the property", - "example": "Age in seconds since 1970" - }, - "type": { - "title": "Type", - "pattern": "^(number|integer|boolean|string|ref_contentSchema|data:([^/\\s,]+/[^/\\s,]+|\\[[^/\\s,]+/[^/\\s,]+(,[^/\\s]+/[^/,\\s]+)*\\]))$", + "status": { "type": "string", - "description": "data type expected on this input glob matching for data type is allowed" - }, - "contentSchema": { - "title": "Contentschema", - "type": "object", - "description": "jsonschema of this input/output. Required when type='ref_contentSchema'" + "title": "Status", + "description": "The running status of the scheduler" }, - "fileToKeyMap": { - "title": "Filetokeymap", + "workers": { + "additionalProperties": { + "$ref": "#/components/schemas/Worker" + }, "type": "object", - "description": "Place the data associated with the named keys in files" - }, - "unit": { - "title": "Unit", - "type": "string", - "description": "Units, when it refers to a physical quantity" - }, - "widget": { - "title": "Widget", - "allOf": [ - { - "$ref": "#/components/schemas/Widget" - } - ], - "description": "custom widget to use instead of the default one determined from the data-type", - "deprecated": true + "title": "Workers" } }, - "additionalProperties": false, - "description": "Base class for service input/outputs" + "type": "object", + "required": [ + "status" + ], + "title": "Scheduler" + }, + "ServiceBootType": { + "type": "string", + "enum": [ + "V0", + "V2" + ], + "title": "ServiceBootType", + "description": "An enumeration." }, "ServiceState": { - "title": "ServiceState", "enum": [ "pending", "pulling", @@ -3477,173 +2565,92 @@ "failed", "stopping" ], + "title": "ServiceState", "description": "An enumeration." }, - "ServiceType": { - "title": "ServiceType", - "enum": [ - "computational", - "dynamic", - "frontend", - "backend" - ], - "type": "string", - "description": "An enumeration." - }, - "ServicesArrayEnveloped": { - "title": "ServicesArrayEnveloped", - "required": [ - "data" - ], - "type": "object", - "properties": { - "data": { - "title": "Data", - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceDockerData" - } - } - } - }, "SimpleAuthentication": { - "title": "SimpleAuthentication", - "required": [ - "username", - "password" - ], - "type": "object", "properties": { "type": { - "title": "Type", + "type": "string", "enum": [ "simple" ], - "type": "string", + "title": "Type", "default": "simple" }, "username": { - "title": "Username", - "type": "string" + "type": "string", + "title": "Username" }, "password": { - "title": "Password", "type": "string", "format": "password", + "title": "Password", "writeOnly": true } }, - "additionalProperties": false - }, - "Structure": { - "title": "Structure", + "additionalProperties": false, + "type": "object", "required": [ - "key", - "label" + "username", + "password" ], - "type": "object", - "properties": { - "key": { - "title": "Key", - "anyOf": [ - { - "type": "string" - }, - { - "type": "boolean" - }, - { - "type": "number" - } - ] - }, - "label": { - "title": "Label", - "type": "string" - } - }, - "additionalProperties": false + "title": "SimpleAuthentication" }, "TaskCounts": { - "title": "TaskCounts", - "type": "object", "properties": { "error": { - "title": "Error", "type": "integer", + "title": "Error", "default": 0 }, "memory": { - "title": "Memory", "type": "integer", + "title": "Memory", "default": 0 }, "executing": { - "title": "Executing", "type": "integer", + "title": "Executing", "default": 0 } - } + }, + "type": "object", + "title": "TaskCounts" }, "TaskLogFileGet": { - "title": "TaskLogFileGet", - "required": [ - "task_id" - ], - "type": "object", "properties": { "task_id": { - "title": "Task Id", "type": "string", - "format": "uuid" + "format": "uuid", + "title": "Task Id" }, "download_link": { - "title": "Download Link", + "type": "string", "maxLength": 65536, "minLength": 1, - "type": "string", - "description": "Presigned link for log file or None if still not available", - "format": "uri" + "format": "uri", + "title": "Download Link", + "description": "Presigned link for log file or None if still not available" } - } - }, - "TextArea": { - "title": "TextArea", + }, + "type": "object", "required": [ - "minHeight" + "task_id" ], - "type": "object", - "properties": { - "minHeight": { - "title": "Minheight", - "exclusiveMinimum": true, - "type": "integer", - "description": "minimum Height of the textarea", - "minimum": 0 - } - }, - "additionalProperties": false + "title": "TaskLogFileGet" }, "UsedResources": { - "title": "UsedResources", - "type": "object", "additionalProperties": { - "minimum": 0.0, - "type": "number" - } + "type": "number", + "minimum": 0.0 + }, + "type": "object", + "title": "UsedResources" }, "ValidationError": { - "title": "ValidationError", - "required": [ - "loc", - "msg", - "type" - ], - "type": "object", "properties": { "loc": { - "title": "Location", - "type": "array", "items": { "anyOf": [ { @@ -3653,76 +2660,56 @@ "type": "integer" } ] - } + }, + "type": "array", + "title": "Location" }, "msg": { - "title": "Message", - "type": "string" + "type": "string", + "title": "Message" }, "type": { - "title": "Error Type", - "type": "string" + "type": "string", + "title": "Error Type" } - } - }, - "Widget": { - "title": "Widget", + }, + "type": "object", "required": [ - "type", - "details" + "loc", + "msg", + "type" ], - "type": "object", + "title": "ValidationError" + }, + "WalletInfo": { "properties": { - "type": { - "allOf": [ - { - "$ref": "#/components/schemas/WidgetType" - } - ], - "description": "type of the property" + "wallet_id": { + "type": "integer", + "exclusiveMinimum": true, + "title": "Wallet Id", + "minimum": 0 }, - "details": { - "title": "Details", - "anyOf": [ - { - "$ref": "#/components/schemas/TextArea" - }, - { - "$ref": "#/components/schemas/SelectBox" - } - ] + "wallet_name": { + "type": "string", + "title": "Wallet Name" } }, - "additionalProperties": false - }, - "WidgetType": { - "title": "WidgetType", - "enum": [ - "TextArea", - "SelectBox" + "type": "object", + "required": [ + "wallet_id", + "wallet_name" ], - "type": "string", - "description": "An enumeration." + "title": "WalletInfo" }, "Worker": { - "title": "Worker", - "required": [ - "id", - "name", - "resources", - "used_resources", - "memory_limit", - "metrics" - ], - "type": "object", "properties": { "id": { - "title": "Id", - "type": "string" + "type": "string", + "title": "Id" }, "name": { - "title": "Name", - "type": "string" + "type": "string", + "title": "Name" }, "resources": { "$ref": "#/components/schemas/DictModel_str__PositiveFloat_" @@ -3731,49 +2718,59 @@ "$ref": "#/components/schemas/UsedResources" }, "memory_limit": { - "title": "Memory Limit", - "type": "integer" + "type": "integer", + "title": "Memory Limit" }, "metrics": { "$ref": "#/components/schemas/WorkerMetrics" } - } - }, - "WorkerMetrics": { - "title": "WorkerMetrics", + }, + "type": "object", "required": [ - "cpu", - "memory", - "num_fds", - "task_counts" + "id", + "name", + "resources", + "used_resources", + "memory_limit", + "metrics" ], - "type": "object", + "title": "Worker" + }, + "WorkerMetrics": { "properties": { "cpu": { - "title": "Cpu", "type": "number", + "title": "Cpu", "description": "consumed % of cpus" }, "memory": { - "title": "Memory", "type": "integer", + "title": "Memory", "description": "consumed memory" }, "num_fds": { - "title": "Num Fds", "type": "integer", + "title": "Num Fds", "description": "consumed file descriptors" }, "task_counts": { - "title": "Task Counts", "allOf": [ { "$ref": "#/components/schemas/TaskCounts" } ], + "title": "Task Counts", "description": "task details" } - } + }, + "type": "object", + "required": [ + "cpu", + "memory", + "num_fds", + "task_counts" + ], + "title": "WorkerMetrics" } } } diff --git a/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py b/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py index e1bc265ea51..ffe2b0cf395 100644 --- a/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py +++ b/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py @@ -226,7 +226,8 @@ async def create_computation( # noqa: C901, PLR0912 wallet_id = None wallet_name = None pricing_plan_id = None - pricing_detail_id = None + pricing_unit_id = None + pricing_unit_cost_id = None if computation.wallet_info: wallet_id = computation.wallet_info.wallet_id wallet_name = computation.wallet_info.wallet_name @@ -235,8 +236,9 @@ async def create_computation( # noqa: C901, PLR0912 # NOTE: MD/SAN -> add real service version/key and store in DB, issue: https://github.com/ITISFoundation/osparc-issues/issues/1131 ( pricing_plan_id, - pricing_detail_id, - ) = await resource_usage_api.get_default_pricing_plan_and_pricing_detail_for_service( + pricing_unit_id, + pricing_unit_cost_id, + ) = await resource_usage_api.get_default_service_pricing_plan_and_pricing_unit( computation.product_name, ServiceKey("simcore/services/comp/itis/sleeper"), ServiceVersion("2.1.6"), @@ -258,7 +260,8 @@ async def create_computation( # noqa: C901, PLR0912 wallet_id=wallet_id, wallet_name=wallet_name, pricing_plan_id=pricing_plan_id, - pricing_detail_id=pricing_detail_id, + pricing_unit_id=pricing_unit_id, + pricing_unit_cost_id=pricing_unit_cost_id, ), use_on_demand_clusters=computation.use_on_demand_clusters, ) diff --git a/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py b/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py index d5a5b856328..e6f54e66df4 100644 --- a/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py +++ b/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py @@ -22,7 +22,8 @@ class RunMetadataDict(TypedDict, total=False): wallet_id: int | None wallet_name: str | None pricing_plan_id: int | None - pricing_detail_id: int | None + pricing_unit_id: int | None + pricing_unit_cost_id: int | None class CompRunsAtDB(BaseModel): diff --git a/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py b/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py index 0d340c714f0..effa63cd6b4 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py @@ -381,7 +381,8 @@ async def _process_started_tasks( wallet_id=run_metadata.get("wallet_id"), wallet_name=run_metadata.get("wallet_name"), pricing_plan_id=run_metadata.get("pricing_plan_id"), - pricing_detail_id=run_metadata.get("pricing_detail_id"), + pricing_unit_id=run_metadata.get("pricing_unit_id"), + pricing_unit_cost_id=run_metadata.get("pricing_unit_cost_id"), product_name=run_metadata.get( "product_name", UNDEFINED_STR_METADATA ), diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py index ff3457c9638..a0d6ebb4216 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py @@ -127,15 +127,17 @@ async def progress_create_containers( wallet_id = None wallet_name = None pricing_plan_id = None - pricing_detail_id = None + pricing_unit_id = None + pricing_unit_cost_id = None if scheduler_data.wallet_info: wallet_id = scheduler_data.wallet_info.wallet_id wallet_name = scheduler_data.wallet_info.wallet_name resource_usage_api = ResourceUsageApi.get_from_state(app) ( pricing_plan_id, - pricing_detail_id, - ) = await resource_usage_api.get_default_pricing_plan_and_pricing_detail_for_service( + pricing_unit_id, + pricing_unit_cost_id, + ) = await resource_usage_api.get_default_service_pricing_plan_and_pricing_unit( scheduler_data.product_name, scheduler_data.key, scheduler_data.version ) @@ -143,7 +145,8 @@ async def progress_create_containers( wallet_id=wallet_id, wallet_name=wallet_name, pricing_plan_id=pricing_plan_id, - pricing_detail_id=pricing_detail_id, + pricing_unit_id=pricing_unit_id, + pricing_unit_cost_id=pricing_unit_cost_id, product_name=scheduler_data.product_name, simcore_user_agent=scheduler_data.request_simcore_user_agent, user_email=user_email, diff --git a/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py b/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py index 389d0365c39..a7177ec610d 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py @@ -8,9 +8,15 @@ import httpx from fastapi import FastAPI -from models_library.api_schemas_webserver.resource_usage import PricingPlanGet +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + ServicePricingPlanGet, +) from models_library.products import ProductName -from models_library.resource_tracker import PricingDetailId, PricingPlanId +from models_library.resource_tracker import ( + PricingPlanId, + PricingUnitCostId, + PricingUnitId, +) from models_library.services import ServiceKey, ServiceVersion from pydantic import parse_obj_as @@ -63,38 +69,37 @@ async def is_healhy(self) -> bool: # pricing plans methods # - async def get_pricing_plans_for_service( + async def get_default_service_pricing_plan( self, product_name: ProductName, service_key: ServiceKey, service_version: ServiceVersion, - ) -> list[PricingPlanGet]: + ) -> ServicePricingPlanGet: response = await self.client.get( - "/pricing-plans", + f"/services/{service_key}/{service_version}/pricing-plan", params={ "product_name": product_name, - "service_key": service_key, - "service_version": service_version, }, ) response.raise_for_status() - return parse_obj_as(list[PricingPlanGet], response.json()) + return parse_obj_as(ServicePricingPlanGet, response.json()) - async def get_default_pricing_plan_and_pricing_detail_for_service( + async def get_default_service_pricing_plan_and_pricing_unit( self, product_name: ProductName, service_key: ServiceKey, service_version: ServiceVersion, - ) -> tuple[PricingPlanId, PricingDetailId]: - pricing_plans = await self.get_pricing_plans_for_service( + ) -> tuple[PricingPlanId, PricingUnitId, PricingUnitCostId]: + pricing_plan = await self.get_default_service_pricing_plan( product_name, service_key, service_version ) - if pricing_plans: - default_pricing_plan = pricing_plans[0] - default_pricing_detail = pricing_plans[0].details[0] + if pricing_plan: + default_pricing_plan = pricing_plan + default_pricing_unit = pricing_plan.pricing_units[0] return ( default_pricing_plan.pricing_plan_id, - default_pricing_detail.pricing_detail_id, + default_pricing_unit.pricing_unit_id, + default_pricing_unit.current_cost_per_unit_id, ) raise ValueError( f"No default pricing plan provided for requested service key: {service_key} version: {service_version} product: {product_name}" diff --git a/services/director-v2/src/simcore_service_director_v2/utils/rabbitmq.py b/services/director-v2/src/simcore_service_director_v2/utils/rabbitmq.py index 752d2353096..f9e2aa3968a 100644 --- a/services/director-v2/src/simcore_service_director_v2/utils/rabbitmq.py +++ b/services/director-v2/src/simcore_service_director_v2/utils/rabbitmq.py @@ -74,7 +74,8 @@ async def publish_service_resource_tracking_started( # noqa: PLR0913 wallet_id: WalletID | None, wallet_name: str | None, pricing_plan_id: int | None, - pricing_detail_id: int | None, + pricing_unit_id: int | None, + pricing_unit_cost_id: int | None, product_name: str, simcore_user_agent: str, user_id: UserID, @@ -94,7 +95,8 @@ async def publish_service_resource_tracking_started( # noqa: PLR0913 wallet_id=wallet_id, wallet_name=wallet_name, pricing_plan_id=pricing_plan_id, - pricing_detail_id=pricing_detail_id, + pricing_unit_id=pricing_unit_id, + pricing_unit_cost_id=pricing_unit_cost_id, product_name=product_name, simcore_user_agent=simcore_user_agent, user_id=user_id, diff --git a/services/director-v2/tests/unit/with_dbs/test_utils_rabbitmq.py b/services/director-v2/tests/unit/with_dbs/test_utils_rabbitmq.py index c8da9e89218..616619bfa4d 100644 --- a/services/director-v2/tests/unit/with_dbs/test_utils_rabbitmq.py +++ b/services/director-v2/tests/unit/with_dbs/test_utils_rabbitmq.py @@ -184,7 +184,8 @@ async def test_publish_service_resource_tracking_started( wallet_id=faker.pyint(min_value=1), wallet_name=faker.pystr(), pricing_plan_id=None, - pricing_detail_id=None, + pricing_unit_id=None, + pricing_unit_cost_id=None, product_name=osparc_product_name, simcore_user_agent=simcore_user_agent, user_id=user["id"], diff --git a/services/dynamic-sidecar/openapi.json b/services/dynamic-sidecar/openapi.json index 5d1c2a46dab..5b2e269316f 100644 --- a/services/dynamic-sidecar/openapi.json +++ b/services/dynamic-sidecar/openapi.json @@ -848,9 +848,13 @@ "type": "integer", "title": "Pricing Plan Id" }, - "pricing_detail_id": { + "pricing_unit_id": { "type": "integer", - "title": "Pricing Detail Id" + "title": "Pricing Unit Id" + }, + "pricing_unit_cost_id": { + "type": "integer", + "title": "Pricing Unit Cost Id" }, "product_name": { "type": "string", @@ -911,7 +915,8 @@ "wallet_id": 1, "wallet_name": "a private wallet for me", "pricing_plan_id": 1, - "pricing_detail_id": 1, + "pricing_unit_id": 1, + "pricing_unit_detail_id": 1, "product_name": "osparc", "simcore_user_agent": "undefined", "user_email": "test@test.com", diff --git a/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/resource_tracking/_core.py b/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/resource_tracking/_core.py index 40bcebf0921..90a226999bc 100644 --- a/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/resource_tracking/_core.py +++ b/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/resource_tracking/_core.py @@ -115,7 +115,8 @@ async def send_service_started( service_resources=metrics_params.service_resources, service_additional_metadata=metrics_params.service_additional_metadata, pricing_plan_id=metrics_params.pricing_plan_id, - pricing_detail_id=metrics_params.pricing_detail_id, + pricing_unit_id=metrics_params.pricing_unit_id, + pricing_unit_cost_id=metrics_params.pricing_unit_cost_id, ) await post_resource_tracking_message(app, message) diff --git a/services/resource-usage-tracker/README.md b/services/resource-usage-tracker/README.md index ba0fd70d8b1..50efdf6221e 100644 --- a/services/resource-usage-tracker/README.md +++ b/services/resource-usage-tracker/README.md @@ -1,4 +1,13 @@ # resource usage tracker -Service that collects and stores computational resources usage used in osparc-simcore +Service that collects and stores computational resources usage used in osparc-simcore. Also takes care of computation of used osparc credits. + + +## Credit computation (Collections) +- **PricingPlan**: + Describe the overall billing plan/package. The pricing plan can be connected to one or more services. A specific pricing plan might be defined also for billing storage costs. +- **PricingUnit**: + Specifies the various units/tiers within a pricing plan, that denote different options (resources and costs) for running services/storage costs. For example: a specific pricing plan might offer three tiers based on resources: SMALL, MEDIUM, and LARGE. +- **PricingUnitCreditCost**: + Defines the credit cost for each unit, which can change over time, allowing for pricing flexibility. diff --git a/services/resource-usage-tracker/openapi.json b/services/resource-usage-tracker/openapi.json index 29416e057c1..ad5b5c0b4bc 100644 --- a/services/resource-usage-tracker/openapi.json +++ b/services/resource-usage-tracker/openapi.json @@ -48,7 +48,7 @@ "/v1/services/-/usages": { "get": { "tags": [ - "resource-usage-tracker" + "usages" ], "summary": "List Usage Services", "description": "Returns a list of tracked containers for a given user and product", @@ -145,7 +145,7 @@ "/v1/credit-transactions/credits:sum": { "post": { "tags": [ - "resource-usage-tracker" + "credit-transactions" ], "summary": "Sum total available credits in the wallet", "operationId": "get_credit_transactions_sum_v1_credit_transactions_credits_sum_post", @@ -198,7 +198,7 @@ "/v1/credit-transactions": { "post": { "tags": [ - "resource-usage-tracker" + "credit-transactions" ], "summary": "Top up credits for specific wallet", "operationId": "create_credit_transaction_v1_credit_transactions_post", @@ -236,14 +236,35 @@ } } }, - "/v1/pricing-plans": { + "/v1/services/{service_key}/{service_version}/pricing-plan": { "get": { "tags": [ - "resource-usage-tracker" + "pricing-plans" ], - "summary": "Retrieve all pricing plans with pricing details for a specific product.", - "operationId": "get_pricing_plans_v1_pricing_plans_get", + "summary": "Get Service Default Pricing Plan", + "description": "Returns a default pricing plan with pricing details for a specified service", + "operationId": "get_service_default_pricing_plan", "parameters": [ + { + "required": true, + "schema": { + "title": "Service Key", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "type": "string" + }, + "name": "service_key", + "in": "path" + }, + { + "required": true, + "schema": { + "title": "Service Version", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "type": "string" + }, + "name": "service_version", + "in": "path" + }, { "required": true, "schema": { @@ -252,25 +273,70 @@ }, "name": "product_name", "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServicePricingPlanGet" + } + } + } }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}": { + "get": { + "tags": [ + "pricing-plans" + ], + "summary": "Get Pricing Plan Unit", + "description": "Returns a list of service pricing plans with pricing details for a specified service", + "operationId": "list_service_pricing_plans", + "parameters": [ { - "required": false, + "required": true, "schema": { - "title": "Service Key", - "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", - "type": "string" + "title": "Pricing Plan Id", + "exclusiveMinimum": true, + "type": "integer", + "minimum": 0 }, - "name": "service_key", - "in": "query" + "name": "pricing_plan_id", + "in": "path" }, { - "required": false, + "required": true, "schema": { - "title": "Service Version", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Pricing Unit Id", + "exclusiveMinimum": true, + "type": "integer", + "minimum": 0 + }, + "name": "pricing_unit_id", + "in": "path" + }, + { + "required": true, + "schema": { + "title": "Product Name", "type": "string" }, - "name": "service_version", + "name": "product_name", "in": "query" } ], @@ -280,11 +346,7 @@ "content": { "application/json": { "schema": { - "title": "Response Get Pricing Plans V1 Pricing Plans Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/PricingPlanGet" - } + "$ref": "#/components/schemas/PricingUnitGet" } } } @@ -374,6 +436,17 @@ }, "description": "Response Create Credit Transaction V1 Credit Transactions Post" }, + "CreditTransactionStatus": { + "title": "CreditTransactionStatus", + "enum": [ + "PENDING", + "BILLED", + "NOT_BILLED", + "REQUIRES_MANUAL_REVIEW" + ], + "type": "string", + "description": "An enumeration." + }, "HTTPValidationError": { "title": "HTTPValidationError", "type": "object", @@ -454,70 +527,77 @@ } } }, - "PricingDetailMinimalGet": { - "title": "PricingDetailMinimalGet", + "PricingPlanClassification": { + "title": "PricingPlanClassification", + "enum": [ + "TIER" + ], + "type": "string", + "description": "An enumeration." + }, + "PricingUnitGet": { + "title": "PricingUnitGet", "required": [ - "pricingDetailId", - "unitName", - "costPerUnit", - "validFrom", - "simcoreDefault" + "pricing_unit_id", + "unit_name", + "current_cost_per_unit", + "current_cost_per_unit_id", + "default", + "specific_info" ], "type": "object", "properties": { - "pricingDetailId": { - "title": "Pricingdetailid", + "pricing_unit_id": { + "title": "Pricing Unit Id", "exclusiveMinimum": true, "type": "integer", "minimum": 0 }, - "unitName": { - "title": "Unitname", + "unit_name": { + "title": "Unit Name", "type": "string" }, - "costPerUnit": { - "title": "Costperunit", + "current_cost_per_unit": { + "title": "Current Cost Per Unit", "type": "number" }, - "validFrom": { - "title": "Validfrom", - "type": "string", - "format": "date-time" + "current_cost_per_unit_id": { + "title": "Current Cost Per Unit Id", + "exclusiveMinimum": true, + "type": "integer", + "minimum": 0 }, - "simcoreDefault": { - "title": "Simcoredefault", + "default": { + "title": "Default", "type": "boolean" + }, + "specific_info": { + "title": "Specific Info", + "type": "object" } } }, - "PricingPlanClassification": { - "title": "PricingPlanClassification", - "enum": [ - "TIER" - ], - "type": "string", - "description": "An enumeration." - }, - "PricingPlanGet": { - "title": "PricingPlanGet", + "ServicePricingPlanGet": { + "title": "ServicePricingPlanGet", "required": [ - "pricingPlanId", - "name", + "pricing_plan_id", + "display_name", "description", "classification", - "createdAt", - "details" + "created_at", + "pricing_plan_key", + "pricing_units" ], "type": "object", "properties": { - "pricingPlanId": { - "title": "Pricingplanid", + "pricing_plan_id": { + "title": "Pricing Plan Id", "exclusiveMinimum": true, "type": "integer", "minimum": 0 }, - "name": { - "title": "Name", + "display_name": { + "title": "Display Name", "type": "string" }, "description": { @@ -527,16 +607,20 @@ "classification": { "$ref": "#/components/schemas/PricingPlanClassification" }, - "createdAt": { - "title": "Createdat", + "created_at": { + "title": "Created At", "type": "string", "format": "date-time" }, - "details": { - "title": "Details", + "pricing_plan_key": { + "title": "Pricing Plan Key", + "type": "string" + }, + "pricing_units": { + "title": "Pricing Units", "type": "array", "items": { - "$ref": "#/components/schemas/PricingDetailMinimalGet" + "$ref": "#/components/schemas/PricingUnitGet" } } } @@ -627,6 +711,13 @@ }, "service_run_status": { "$ref": "#/components/schemas/ServiceRunStatus" + }, + "credit_cost": { + "title": "Credit Cost", + "type": "number" + }, + "transaction_status": { + "$ref": "#/components/schemas/CreditTransactionStatus" } } }, diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/_resource_tracker.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/_resource_tracker.py index c037efcdeb6..57bec238747 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/_resource_tracker.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/_resource_tracker.py @@ -7,14 +7,15 @@ CreditTransactionCreated, WalletTotalCredits, ) -from models_library.api_schemas_webserver.resource_usage import ( - PricingPlanGet, - ServiceRunGet, +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, + ServicePricingPlanGet, ) +from models_library.api_schemas_resource_usage_tracker.service_runs import ServiceRunGet from models_library.resource_tracker import CreditTransactionId from ..models.pagination import LimitOffsetPage, LimitOffsetParamsWithDefault -from ..models.resource_tracker_service_run import ServiceRunPage +from ..models.resource_tracker_service_runs import ServiceRunPage from ..services import ( resource_tracker_credit_transactions, resource_tracker_pricing_plans, @@ -37,6 +38,7 @@ response_model=LimitOffsetPage[ServiceRunGet], operation_id="list_usage_services", description="Returns a list of tracked containers for a given user and product", + tags=["usages"], ) async def list_usage_services( page_params: Annotated[LimitOffsetParamsWithDefault, Depends()], @@ -61,6 +63,7 @@ async def list_usage_services( "/credit-transactions/credits:sum", response_model=WalletTotalCredits, summary="Sum total available credits in the wallet", + tags=["credit-transactions"], ) async def get_credit_transactions_sum( wallet_total_credits: Annotated[ @@ -78,6 +81,7 @@ async def get_credit_transactions_sum( response_model=CreditTransactionCreated, summary="Top up credits for specific wallet", status_code=status.HTTP_201_CREATED, + tags=["credit-transactions"], ) async def create_credit_transaction( transaction_id: Annotated[ @@ -94,14 +98,32 @@ async def create_credit_transaction( @router.get( - "/pricing-plans", - response_model=list[PricingPlanGet], - summary="Retrieve all pricing plans with pricing details for a specific product.", + "/services/{service_key:path}/{service_version}/pricing-plan", + response_model=ServicePricingPlanGet, + operation_id="get_service_default_pricing_plan", + description="Returns a default pricing plan with pricing details for a specified service", + tags=["pricing-plans"], ) -async def get_pricing_plans( - pricing_plans: Annotated[ - list[PricingPlanGet], - Depends(resource_tracker_pricing_plans.list_pricing_plans), +async def get_service_default_pricing_plan( + service_pricing_plans: Annotated[ + ServicePricingPlanGet, + Depends(resource_tracker_pricing_plans.get_service_default_pricing_plan), ], ): - return pricing_plans + return service_pricing_plans + + +@router.get( + "/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}", + response_model=PricingUnitGet, + operation_id="list_service_pricing_plans", + description="Returns a list of service pricing plans with pricing details for a specified service", + tags=["pricing-plans"], +) +async def get_pricing_plan_unit( + pricing_unit: Annotated[ + PricingUnitGet, + Depends(resource_tracker_pricing_plans.get_pricing_unit), + ] +): + return pricing_unit diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/routes.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/routes.py index e854907a222..64a3d2ba05f 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/routes.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/routes.py @@ -12,5 +12,5 @@ def setup_api_routes(app: FastAPI): api_router = APIRouter(prefix=f"/{API_VTAG}") api_router.include_router(_meta.router, tags=["meta"]) - api_router.include_router(_resource_tracker.router, tags=["resource-usage-tracker"]) + api_router.include_router(_resource_tracker.router) app.include_router(api_router) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/core/errors.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/core/errors.py index 66ac894b52d..a754ad11310 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/core/errors.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/core/errors.py @@ -15,3 +15,7 @@ class CreateServiceRunError(ResourceUsageTrackerRuntimeError): class CreateTransactionError(ResourceUsageTrackerRuntimeError): msg_template: str = "Error during creation of new transaction record in DB: {msg}" + + +class ResourceUsageTrackerCustomRuntimeError(ResourceUsageTrackerRuntimeError): + msg_template: str = "Error: {msg}" diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_credit_transactions.py index 3f1bfd7c30b..a264f90d375 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_credit_transactions.py @@ -4,9 +4,11 @@ from models_library.products import ProductName from models_library.resource_tracker import ( CreditClassification, + CreditTransactionId, CreditTransactionStatus, - PricingDetailId, PricingPlanId, + PricingUnitCostId, + PricingUnitId, ServiceRunId, ) from models_library.users import UserID @@ -19,7 +21,8 @@ class CreditTransactionCreate(BaseModel): wallet_id: WalletID wallet_name: str pricing_plan_id: PricingPlanId | None - pricing_detail_id: PricingDetailId | None + pricing_unit_id: PricingUnitId | None + pricing_unit_cost_id: PricingUnitCostId | None user_id: UserID user_email: str osparc_credits: Decimal @@ -41,3 +44,26 @@ class CreditTransactionCreditsAndStatusUpdate(BaseModel): service_run_id: ServiceRunId osparc_credits: Decimal transaction_status: CreditTransactionStatus + + +class CreditTransactionDB(BaseModel): + transaction_id: CreditTransactionId + product_name: ProductName + wallet_id: WalletID + wallet_name: str + pricing_plan_id: PricingPlanId | None + pricing_unit_id: PricingUnitId | None + pricing_unit_cost_id: PricingUnitCostId | None + user_id: UserID + user_email: str + osparc_credits: Decimal + transaction_status: CreditTransactionStatus + transaction_classification: CreditClassification + service_run_id: ServiceRunId | None + payment_transaction_id: str | None + created: datetime + last_heartbeat_at: datetime + modified: datetime + + class Config: + orm_mode = True diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_details.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_details.py deleted file mode 100644 index 8b7f9fac879..00000000000 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_details.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import datetime -from decimal import Decimal - -from models_library.resource_tracker import PricingDetailId, PricingPlanId -from pydantic import BaseModel - - -class PricingDetailDB(BaseModel): - pricing_detail_id: PricingDetailId - pricing_plan_id: PricingPlanId - unit_name: str - cost_per_unit: Decimal - valid_from: datetime - valid_to: datetime | None - specific_info: dict - created: datetime - simcore_default: bool - - class Config: - orm_mode = True diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_plans.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_plans.py index a7a85a17017..bba9d92708e 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_plans.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_plans.py @@ -4,13 +4,21 @@ from pydantic import BaseModel -class PricingPlanDB(BaseModel): +class PricingPlansDB(BaseModel): pricing_plan_id: PricingPlanId - name: str + display_name: str description: str classification: PricingPlanClassification is_active: bool created: datetime + pricing_plan_key: str + + class Config: + orm_mode = True + + +class PricingPlansWithServiceDefaultPlanDB(PricingPlansDB): + service_default_plan: bool class Config: orm_mode = True diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_unit_costs.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_unit_costs.py new file mode 100644 index 00000000000..139477c4957 --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_unit_costs.py @@ -0,0 +1,27 @@ +from datetime import datetime +from decimal import Decimal + +from models_library.resource_tracker import ( + PricingPlanId, + PricingUnitCostId, + PricingUnitId, +) +from pydantic import BaseModel + + +class PricingUnitCostsDB(BaseModel): + pricing_unit_cost_id: PricingUnitCostId + pricing_plan_id: PricingPlanId + pricing_plan_key: str + pricing_unit_id: PricingUnitId + pricing_unit_name: str + cost_per_unit: Decimal + valid_from: datetime + valid_to: datetime | None + specific_info: dict + created: datetime + comment: str + modified: datetime + + class Config: + orm_mode = True diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_units.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_units.py new file mode 100644 index 00000000000..b7bc16eaec1 --- /dev/null +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_pricing_units.py @@ -0,0 +1,24 @@ +from datetime import datetime +from decimal import Decimal + +from models_library.resource_tracker import ( + PricingPlanId, + PricingUnitCostId, + PricingUnitId, +) +from pydantic import BaseModel + + +class PricingUnitsDB(BaseModel): + pricing_unit_id: PricingUnitId + pricing_plan_id: PricingPlanId + unit_name: str + default: bool + specific_info: dict + created: datetime + modified: datetime + current_cost_per_unit: Decimal + current_cost_per_unit_id: PricingUnitCostId + + class Config: + orm_mode = True diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_run.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_runs.py similarity index 76% rename from services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_run.py rename to services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_runs.py index 4f55da2ec0b..ead50e04465 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_run.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_runs.py @@ -2,13 +2,15 @@ from decimal import Decimal from typing import NamedTuple -from models_library.api_schemas_webserver.resource_usage import ServiceRunGet +from models_library.api_schemas_resource_usage_tracker.service_runs import ServiceRunGet from models_library.products import ProductName from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID from models_library.resource_tracker import ( - PricingDetailId, + CreditTransactionStatus, PricingPlanId, + PricingUnitCostId, + PricingUnitId, ResourceTrackerServiceType, ServiceRunId, ServiceRunStatus, @@ -25,8 +27,9 @@ class ServiceRunCreate(BaseModel): wallet_id: WalletID | None wallet_name: str | None pricing_plan_id: PricingPlanId | None - pricing_detail_id: PricingDetailId | None - pricing_detail_cost_per_unit: Decimal | None + pricing_unit_id: PricingUnitId | None + pricing_unit_cost_id: PricingUnitCostId | None + pricing_unit_cost: Decimal | None simcore_user_agent: str user_id: UserID user_email: str @@ -61,8 +64,9 @@ class ServiceRunDB(BaseModel): wallet_id: WalletID | None wallet_name: str | None pricing_plan_id: PricingPlanId | None - pricing_detail_id: PricingDetailId | None - pricing_detail_cost_per_unit: Decimal | None + pricing_unit_id: PricingUnitId | None + pricing_unit_cost_id: PricingUnitCostId | None + pricing_unit_cost: Decimal | None user_id: UserID user_email: str project_id: ProjectID @@ -76,15 +80,19 @@ class ServiceRunDB(BaseModel): started_at: datetime stopped_at: datetime | None service_run_status: ServiceRunStatus + modified: datetime + last_heartbeat_at: datetime class Config: orm_mode = True -class ServiceRunOnUpdateDB(BaseModel): - pricing_plan_id: PricingPlanId - pricing_detail_id: PricingDetailId - started_at: datetime +class ServiceRunWithCreditsDB(ServiceRunDB): + osparc_credits: Decimal | None + transaction_status: CreditTransactionStatus | None + + class Config: + orm_mode = True class ServiceRunPage(NamedTuple): diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/modules/db/repositories/resource_tracker.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/modules/db/repositories/resource_tracker.py index cf139cac639..f2758cdd4b6 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/modules/db/repositories/resource_tracker.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/modules/db/repositories/resource_tracker.py @@ -10,8 +10,9 @@ from models_library.resource_tracker import ( CreditTransactionId, CreditTransactionStatus, - PricingDetailId, PricingPlanId, + PricingUnitCostId, + PricingUnitId, ServiceRunId, ServiceRunStatus, ) @@ -22,33 +23,46 @@ from simcore_postgres_database.models.resource_tracker_credit_transactions import ( resource_tracker_credit_transactions, ) -from simcore_postgres_database.models.resource_tracker_pricing_details import ( - resource_tracker_pricing_details, -) from simcore_postgres_database.models.resource_tracker_pricing_plan_to_service import ( resource_tracker_pricing_plan_to_service, ) from simcore_postgres_database.models.resource_tracker_pricing_plans import ( resource_tracker_pricing_plans, ) +from simcore_postgres_database.models.resource_tracker_pricing_unit_costs import ( + resource_tracker_pricing_unit_costs, +) +from simcore_postgres_database.models.resource_tracker_pricing_units import ( + resource_tracker_pricing_units, +) from simcore_postgres_database.models.resource_tracker_service_runs import ( resource_tracker_service_runs, ) +from simcore_service_resource_usage_tracker.models.resource_tracker_pricing_unit_costs import ( + PricingUnitCostsDB, +) from sqlalchemy.dialects.postgresql import ARRAY, INTEGER -from ....core.errors import CreateServiceRunError, CreateTransactionError +from ....core.errors import ( + CreateServiceRunError, + CreateTransactionError, + ResourceUsageTrackerCustomRuntimeError, +) from ....models.resource_tracker_credit_transactions import ( CreditTransactionCreate, CreditTransactionCreditsAndStatusUpdate, CreditTransactionCreditsUpdate, ) -from ....models.resource_tracker_pricing_details import PricingDetailDB -from ....models.resource_tracker_pricing_plans import PricingPlanDB -from ....models.resource_tracker_service_run import ( +from ....models.resource_tracker_pricing_plans import ( + PricingPlansWithServiceDefaultPlanDB, +) +from ....models.resource_tracker_pricing_units import PricingUnitsDB +from ....models.resource_tracker_service_runs import ( ServiceRunCreate, ServiceRunDB, ServiceRunLastHeartbeatUpdate, ServiceRunStoppedAtUpdate, + ServiceRunWithCreditsDB, ) from ._base import BaseRepository @@ -60,31 +74,6 @@ class ResourceTrackerRepository(BaseRepository): # Service Run ############### - @staticmethod - def _service_runs_select_stmt(): - return sa.select( - resource_tracker_service_runs.c.product_name, - resource_tracker_service_runs.c.service_run_id, - resource_tracker_service_runs.c.wallet_id, - resource_tracker_service_runs.c.wallet_name, - resource_tracker_service_runs.c.pricing_plan_id, - resource_tracker_service_runs.c.pricing_detail_id, - resource_tracker_service_runs.c.pricing_detail_cost_per_unit, - resource_tracker_service_runs.c.user_id, - resource_tracker_service_runs.c.user_email, - resource_tracker_service_runs.c.project_id, - resource_tracker_service_runs.c.project_name, - resource_tracker_service_runs.c.node_id, - resource_tracker_service_runs.c.node_name, - resource_tracker_service_runs.c.service_key, - resource_tracker_service_runs.c.service_version, - resource_tracker_service_runs.c.service_type, - resource_tracker_service_runs.c.service_resources, - resource_tracker_service_runs.c.started_at, - resource_tracker_service_runs.c.stopped_at, - resource_tracker_service_runs.c.service_run_status, - ) - async def create_service_run(self, data: ServiceRunCreate) -> ServiceRunId: async with self.db_engine.begin() as conn: insert_stmt = ( @@ -95,8 +84,9 @@ async def create_service_run(self, data: ServiceRunCreate) -> ServiceRunId: wallet_id=data.wallet_id, wallet_name=data.wallet_name, pricing_plan_id=data.pricing_plan_id, - pricing_detail_id=data.pricing_detail_id, - pricing_detail_cost_per_unit=data.pricing_detail_cost_per_unit, + pricing_unit_id=data.pricing_unit_id, + pricing_unit_cost_id=data.pricing_unit_cost_id, + pricing_unit_cost=data.pricing_unit_cost, simcore_user_agent=data.simcore_user_agent, user_id=data.user_id, user_email=data.user_email, @@ -183,118 +173,89 @@ async def update_service_run_stopped_at( return None return ServiceRunDB.from_orm(row) - async def list_service_runs_by_user_and_product( - self, user_id: UserID, product_name: ProductName, offset: int, limit: int - ) -> list[ServiceRunDB]: - async with self.db_engine.begin() as conn: - query = ( - self._service_runs_select_stmt() - .where( - (resource_tracker_service_runs.c.user_id == user_id) - & (resource_tracker_service_runs.c.product_name == product_name) - ) - .order_by(resource_tracker_service_runs.c.started_at.desc()) - .offset(offset) - .limit(limit) - ) - result = await conn.execute(query) - - return [ServiceRunDB.from_orm(row) for row in result.fetchall()] - - async def total_service_runs_by_user_and_product( - self, user_id: UserID, product_name: ProductName - ) -> PositiveInt: - async with self.db_engine.begin() as conn: - query = ( - sa.select(sa.func.count()) - .select_from(resource_tracker_service_runs) - .where( - (resource_tracker_service_runs.c.user_id == user_id) - & (resource_tracker_service_runs.c.product_name == product_name) - ) - ) - result = await conn.execute(query) - row = result.first() - return cast(PositiveInt, row[0]) if row else 0 - - async def list_service_runs_by_user_and_product_and_wallet( + async def list_service_runs_by_product_and_user_and_wallet( self, - user_id: UserID, product_name: ProductName, - wallet_id: WalletID, + user_id: UserID | None, + wallet_id: WalletID | None, offset: int, limit: int, - ) -> list[ServiceRunDB]: + ) -> list[ServiceRunWithCreditsDB]: async with self.db_engine.begin() as conn: query = ( - self._service_runs_select_stmt() - .where( - (resource_tracker_service_runs.c.user_id == user_id) - & (resource_tracker_service_runs.c.product_name == product_name) - & (resource_tracker_service_runs.c.wallet_id == wallet_id) + sa.select( + resource_tracker_service_runs.c.product_name, + resource_tracker_service_runs.c.service_run_id, + resource_tracker_service_runs.c.wallet_id, + resource_tracker_service_runs.c.wallet_name, + resource_tracker_service_runs.c.pricing_plan_id, + resource_tracker_service_runs.c.pricing_unit_id, + resource_tracker_service_runs.c.pricing_unit_cost_id, + resource_tracker_service_runs.c.pricing_unit_cost, + resource_tracker_service_runs.c.user_id, + resource_tracker_service_runs.c.user_email, + resource_tracker_service_runs.c.project_id, + resource_tracker_service_runs.c.project_name, + resource_tracker_service_runs.c.node_id, + resource_tracker_service_runs.c.node_name, + resource_tracker_service_runs.c.service_key, + resource_tracker_service_runs.c.service_version, + resource_tracker_service_runs.c.service_type, + resource_tracker_service_runs.c.service_resources, + resource_tracker_service_runs.c.started_at, + resource_tracker_service_runs.c.stopped_at, + resource_tracker_service_runs.c.service_run_status, + resource_tracker_service_runs.c.modified, + resource_tracker_service_runs.c.last_heartbeat_at, + resource_tracker_credit_transactions.c.osparc_credits, + resource_tracker_credit_transactions.c.transaction_status, + ) + .select_from( + resource_tracker_service_runs.join( + resource_tracker_credit_transactions, + resource_tracker_service_runs.c.service_run_id + == resource_tracker_credit_transactions.c.service_run_id, + isouter=True, + ) ) + .where(resource_tracker_service_runs.c.product_name == product_name) .order_by(resource_tracker_service_runs.c.started_at.desc()) .offset(offset) .limit(limit) ) - result = await conn.execute(query) - return [ServiceRunDB.from_orm(row) for row in result.fetchall()] - - async def total_service_runs_by_user_and_product_and_wallet( - self, user_id: UserID, product_name: ProductName, wallet_id: WalletID - ) -> PositiveInt: - async with self.db_engine.begin() as conn: - query = ( - sa.select(sa.func.count()) - .select_from(resource_tracker_service_runs) - .where( - (resource_tracker_service_runs.c.user_id == user_id) - & (resource_tracker_service_runs.c.product_name == product_name) - & (resource_tracker_service_runs.c.wallet_id == wallet_id) + if user_id: + query = query.where(resource_tracker_service_runs.c.user_id == user_id) + if wallet_id: + query = query.where( + resource_tracker_service_runs.c.wallet_id == wallet_id ) - ) + result = await conn.execute(query) - row = result.first() - return cast(PositiveInt, row[0]) if row else 0 + return [ServiceRunWithCreditsDB.from_orm(row) for row in result.fetchall()] - async def list_service_runs_by_product_and_wallet( + async def total_service_runs_by_product_and_user_and_wallet( self, product_name: ProductName, - wallet_id: WalletID, - offset: int, - limit: int, - ) -> list[ServiceRunDB]: - async with self.db_engine.begin() as conn: - query = ( - self._service_runs_select_stmt() - .where( - (resource_tracker_service_runs.c.wallet_id == wallet_id) - & (resource_tracker_service_runs.c.product_name == product_name) - ) - .order_by(resource_tracker_service_runs.c.started_at.desc()) - .offset(offset) - .limit(limit) - ) - result = await conn.execute(query) - - return [ServiceRunDB.from_orm(row) for row in result.fetchall()] - - async def total_service_runs_by_product_and_wallet( - self, product_name: ProductName, wallet_id: WalletID + user_id: UserID | None, + wallet_id: WalletID | None, ) -> PositiveInt: async with self.db_engine.begin() as conn: query = ( sa.select(sa.func.count()) .select_from(resource_tracker_service_runs) - .where( - (resource_tracker_service_runs.c.wallet_id == wallet_id) - & (resource_tracker_service_runs.c.product_name == product_name) - ) + .where(resource_tracker_service_runs.c.product_name == product_name) ) - result = await conn.execute(query) + if user_id: + query = query.where(resource_tracker_service_runs.c.user_id == user_id) + if wallet_id: + query = query.where( + resource_tracker_service_runs.c.wallet_id == wallet_id + ) + + result = await conn.execute(query) row = result.first() return cast(PositiveInt, row[0]) if row else 0 @@ -313,7 +274,8 @@ async def create_credit_transaction( wallet_id=data.wallet_id, wallet_name=data.wallet_name, pricing_plan_id=data.pricing_plan_id, - pricing_detail_id=data.pricing_detail_id, + pricing_unit_id=data.pricing_unit_id, + pricing_unit_cost_id=data.pricing_unit_cost_id, user_id=data.user_id, user_email=data.user_email, osparc_credits=data.osparc_credits, @@ -425,49 +387,12 @@ async def sum_credit_transactions_by_product_and_wallet( # Pricing plans ################################# - async def list_active_pricing_plans_by_product( - self, product_name: ProductName - ) -> list[PricingPlanDB]: - async with self.db_engine.begin() as conn: - query = ( - sa.select( - resource_tracker_pricing_plans.c.pricing_plan_id, - resource_tracker_pricing_plans.c.name, - resource_tracker_pricing_plans.c.description, - resource_tracker_pricing_plans.c.classification, - resource_tracker_pricing_plans.c.is_active, - resource_tracker_pricing_plans.c.created, - ) - .where( - (resource_tracker_pricing_plans.c.product_name == product_name) - & (resource_tracker_pricing_plans.c.is_active.is_(True)) - ) - .order_by(resource_tracker_pricing_plans.c.created.asc()) - ) - result = await conn.execute(query) - - return [PricingPlanDB.from_orm(row) for row in result.fetchall()] - - async def get_pricing_plan(self, pricing_plan_id: PricingPlanId) -> PricingPlanDB: - async with self.db_engine.begin() as conn: - query = sa.select( - resource_tracker_pricing_plans.c.pricing_plan_id, - resource_tracker_pricing_plans.c.name, - resource_tracker_pricing_plans.c.description, - resource_tracker_pricing_plans.c.classification, - resource_tracker_pricing_plans.c.is_active, - resource_tracker_pricing_plans.c.created, - ).where(resource_tracker_pricing_plans.c.pricing_plan_id == pricing_plan_id) - result = await conn.execute(query) - row = result.first() - return PricingPlanDB.from_orm(row) - - async def list_active_pricing_plans_by_product_and_service( + async def list_active_service_pricing_plans_by_product_and_service( self, product_name: ProductName, service_key: ServiceKey, service_version: ServiceVersion, - ) -> list[PricingPlanDB]: + ) -> list[PricingPlansWithServiceDefaultPlanDB]: # NOTE: consilidate with utils_services_environmnets.py def _version(column_or_value): # converts version value string to array[integer] that can be compared @@ -490,10 +415,7 @@ def _version(column_or_value): resource_tracker_pricing_plan_to_service.c.service_key == service_key ) - & ( - resource_tracker_pricing_plan_to_service.c.product - == product_name - ) + & (resource_tracker_pricing_plans.c.product_name == product_name) & (resource_tracker_pricing_plans.c.is_active.is_(True)) ) .order_by( @@ -512,11 +434,13 @@ def _version(column_or_value): query = sa.select( resource_tracker_pricing_plans.c.pricing_plan_id, - resource_tracker_pricing_plans.c.name, + resource_tracker_pricing_plans.c.display_name, resource_tracker_pricing_plans.c.description, resource_tracker_pricing_plans.c.classification, resource_tracker_pricing_plans.c.is_active, resource_tracker_pricing_plans.c.created, + resource_tracker_pricing_plans.c.pricing_plan_key, + resource_tracker_pricing_plan_to_service.c.service_default_plan, ) query = query.where( ( @@ -527,61 +451,157 @@ def _version(column_or_value): resource_tracker_pricing_plan_to_service.c.service_key == latest_service_key ) - & (resource_tracker_pricing_plan_to_service.c.product == product_name) + & (resource_tracker_pricing_plans.c.product_name == product_name) & (resource_tracker_pricing_plans.c.is_active.is_(True)) ).order_by( resource_tracker_pricing_plan_to_service.c.pricing_plan_id.desc() ) result = await conn.execute(query) - return [PricingPlanDB.from_orm(row) for row in result.fetchall()] + return [ + PricingPlansWithServiceDefaultPlanDB.from_orm(row) + for row in result.fetchall() + ] ################################# - # Pricing details + # Pricing units ################################# - async def get_pricing_detail_cost_per_unit( + @staticmethod + def _pricing_units_select_stmt(): + return sa.select( + resource_tracker_pricing_units.c.pricing_unit_id, + resource_tracker_pricing_units.c.pricing_plan_id, + resource_tracker_pricing_units.c.unit_name, + resource_tracker_pricing_units.c.default, + resource_tracker_pricing_units.c.specific_info, + resource_tracker_pricing_units.c.created, + resource_tracker_pricing_units.c.modified, + resource_tracker_pricing_unit_costs.c.cost_per_unit.label( + "current_cost_per_unit" + ), + resource_tracker_pricing_unit_costs.c.pricing_unit_cost_id.label( + "current_cost_per_unit_id" + ), + ) + + async def list_pricing_units_by_pricing_plan( self, - pricing_detail_id: PricingDetailId, - ) -> Decimal: + pricing_plan_id: PricingPlanId, + ) -> list[PricingUnitsDB]: async with self.db_engine.begin() as conn: - query = sa.select(resource_tracker_pricing_details.c.cost_per_unit).where( - resource_tracker_pricing_details.c.pricing_detail_id - == pricing_detail_id + query = ( + self._pricing_units_select_stmt() + .select_from( + resource_tracker_pricing_units.join( + resource_tracker_pricing_unit_costs, + ( + ( + resource_tracker_pricing_units.c.pricing_plan_id + == resource_tracker_pricing_unit_costs.c.pricing_plan_id + ) + & ( + resource_tracker_pricing_units.c.pricing_unit_id + == resource_tracker_pricing_unit_costs.c.pricing_unit_id + ) + ), + ) + ) + .where( + ( + resource_tracker_pricing_units.c.pricing_plan_id + == pricing_plan_id + ) + & (resource_tracker_pricing_unit_costs.c.valid_to.is_(None)) + ) + .order_by(resource_tracker_pricing_unit_costs.c.cost_per_unit.asc()) ) result = await conn.execute(query) - row = result.first() - if row is None: - raise ValueError - return Decimal(row[0]) + return [PricingUnitsDB.from_orm(row) for row in result.fetchall()] - async def list_pricing_details_by_pricing_plan( + async def get_pricing_unit( self, + product_name: ProductName, pricing_plan_id: PricingPlanId, - ) -> list[PricingDetailDB]: + pricing_unit_id: PricingUnitId, + ) -> PricingUnitsDB: async with self.db_engine.begin() as conn: query = ( - sa.select( - resource_tracker_pricing_details.c.pricing_detail_id, - resource_tracker_pricing_details.c.pricing_plan_id, - resource_tracker_pricing_details.c.unit_name, - resource_tracker_pricing_details.c.cost_per_unit, - resource_tracker_pricing_details.c.valid_from, - resource_tracker_pricing_details.c.valid_to, - resource_tracker_pricing_details.c.specific_info, - resource_tracker_pricing_details.c.created, - resource_tracker_pricing_details.c.simcore_default, + self._pricing_units_select_stmt() + .select_from( + resource_tracker_pricing_units.join( + resource_tracker_pricing_unit_costs, + ( + ( + resource_tracker_pricing_units.c.pricing_plan_id + == resource_tracker_pricing_unit_costs.c.pricing_plan_id + ) + & ( + resource_tracker_pricing_units.c.pricing_unit_id + == resource_tracker_pricing_unit_costs.c.pricing_unit_id + ) + ), + ).join( + resource_tracker_pricing_plans, + ( + resource_tracker_pricing_plans.c.pricing_plan_id + == resource_tracker_pricing_units.c.pricing_plan_id + ), + ) ) .where( ( - resource_tracker_pricing_details.c.pricing_plan_id + resource_tracker_pricing_units.c.pricing_plan_id == pricing_plan_id ) - & (resource_tracker_pricing_details.c.valid_to.is_(None)) + & ( + resource_tracker_pricing_units.c.pricing_unit_id + == pricing_unit_id + ) + & (resource_tracker_pricing_unit_costs.c.valid_to.is_(None)) + & (resource_tracker_pricing_plans.c.product_name == product_name) ) - .order_by(resource_tracker_pricing_details.c.created.asc()) ) result = await conn.execute(query) - return [PricingDetailDB.from_orm(row) for row in result.fetchall()] + row = result.first() + if row is None: + raise ResourceUsageTrackerCustomRuntimeError( + msg=f"Pricing unit id {pricing_unit_id} not found" + ) + return PricingUnitsDB.from_orm(row) + + ################################# + # Pricing unit-costs + ################################# + + async def get_pricing_unit_cost_by_id( + self, pricing_unit_cost_id: PricingUnitCostId + ) -> PricingUnitCostsDB: + async with self.db_engine.begin() as conn: + query = sa.select( + resource_tracker_pricing_unit_costs.c.pricing_unit_cost_id, + resource_tracker_pricing_unit_costs.c.pricing_plan_id, + resource_tracker_pricing_unit_costs.c.pricing_plan_key, + resource_tracker_pricing_unit_costs.c.pricing_unit_id, + resource_tracker_pricing_unit_costs.c.pricing_unit_name, + resource_tracker_pricing_unit_costs.c.cost_per_unit, + resource_tracker_pricing_unit_costs.c.valid_from, + resource_tracker_pricing_unit_costs.c.valid_to, + resource_tracker_pricing_unit_costs.c.specific_info, + resource_tracker_pricing_unit_costs.c.created, + resource_tracker_pricing_unit_costs.c.comment, + resource_tracker_pricing_unit_costs.c.modified, + ).where( + resource_tracker_pricing_unit_costs.c.pricing_unit_cost_id + == pricing_unit_cost_id + ) + result = await conn.execute(query) + + row = result.first() + if row is None: + raise ResourceUsageTrackerCustomRuntimeError( + msg=f"Pricing unit cosd id {pricing_unit_cost_id} not found in the resource_tracker_pricing_unit_costs table" + ) + return PricingUnitCostsDB.from_orm(row) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_process_messages.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_process_messages.py index 855003d3471..81fe03c49f4 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_process_messages.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_process_messages.py @@ -1,6 +1,6 @@ import logging from collections.abc import Awaitable, Callable -from datetime import datetime, timezone +from datetime import datetime from decimal import Decimal from fastapi import FastAPI @@ -12,7 +12,6 @@ RabbitResourceTrackingStartedMessage, RabbitResourceTrackingStoppedMessage, SimcorePlatformStatus, - WalletCreditsMessage, ) from models_library.resource_tracker import ( CreditClassification, @@ -28,23 +27,24 @@ CreditTransactionCreditsAndStatusUpdate, CreditTransactionCreditsUpdate, ) -from .models.resource_tracker_service_run import ( +from .models.resource_tracker_service_runs import ( ServiceRunCreate, ServiceRunLastHeartbeatUpdate, ServiceRunStoppedAtUpdate, ) from .modules.db.repositories.resource_tracker import ResourceTrackerRepository from .modules.rabbitmq import RabbitMQClient, get_rabbitmq_client -from .resource_tracker_utils import make_negative +from .resource_tracker_utils import ( + make_negative, + sum_credit_transactions_and_publish_to_rabbitmq, +) _logger = logging.getLogger(__name__) -async def process_message( - app: FastAPI, data: bytes # pylint: disable=unused-argument -) -> bool: +async def process_message(app: FastAPI, data: bytes) -> bool: rabbit_message = parse_raw_as(RabbitResourceTrackingMessages, data) - + _logger.info("Process msg service_run_id: %s", rabbit_message.service_run_id) resource_tacker_repo: ResourceTrackerRepository = ResourceTrackerRepository( db_engine=app.state.engine ) @@ -53,8 +53,6 @@ async def process_message( await RABBIT_MSG_TYPE_TO_PROCESS_HANDLER[rabbit_message.message_type]( resource_tacker_repo, rabbit_message, rabbitmq_client ) - - _logger.debug("%s", data) return True @@ -69,13 +67,12 @@ async def _process_start_event( else ResourceTrackerServiceType.DYNAMIC_SERVICE ) - pricing_detail_cost_per_unit = None - if msg.pricing_detail_id: - pricing_detail_cost_per_unit = ( - await resource_tracker_repo.get_pricing_detail_cost_per_unit( - msg.pricing_detail_id - ) + pricing_unit_cost = None + if msg.pricing_unit_cost_id: + pricing_unit_cost_db = await resource_tracker_repo.get_pricing_unit_cost_by_id( + pricing_unit_cost_id=msg.pricing_unit_cost_id ) + pricing_unit_cost = pricing_unit_cost_db.cost_per_unit create_service_run = ServiceRunCreate( product_name=msg.product_name, @@ -83,8 +80,9 @@ async def _process_start_event( wallet_id=msg.wallet_id, wallet_name=msg.wallet_name, pricing_plan_id=msg.pricing_plan_id, - pricing_detail_id=msg.pricing_detail_id, - pricing_detail_cost_per_unit=pricing_detail_cost_per_unit, + pricing_unit_id=msg.pricing_unit_id, + pricing_unit_cost_id=msg.pricing_unit_cost_id, + pricing_unit_cost=pricing_unit_cost, simcore_user_agent=msg.simcore_user_agent, user_id=msg.user_id, user_email=msg.user_email, @@ -109,7 +107,8 @@ async def _process_start_event( wallet_id=msg.wallet_id, wallet_name=msg.wallet_name, pricing_plan_id=msg.pricing_plan_id, - pricing_detail_id=msg.pricing_detail_id, + pricing_unit_id=msg.pricing_unit_id, + pricing_unit_cost_id=msg.pricing_unit_cost_id, user_id=msg.user_id, user_email=msg.user_email, osparc_credits=Decimal(0.0), @@ -123,18 +122,9 @@ async def _process_start_event( await resource_tracker_repo.create_credit_transaction(transaction_create) # Publish wallet total credits to RabbitMQ - wallet_total_credits = ( - await resource_tracker_repo.sum_credit_transactions_by_product_and_wallet( - msg.product_name, - msg.wallet_id, - ) + await sum_credit_transactions_and_publish_to_rabbitmq( + resource_tracker_repo, rabbitmq_client, msg.product_name, msg.wallet_id ) - publish_message = WalletCreditsMessage.construct( - wallet_id=msg.wallet_id, - created_at=datetime.now(tz=timezone.utc), - credits=wallet_total_credits.available_osparc_credits, - ) - await rabbitmq_client.publish(publish_message.channel_name, publish_message) async def _process_heartbeat_event( @@ -153,12 +143,12 @@ async def _process_heartbeat_event( _logger.info("Nothing to update: %s", msg) return - if running_service.wallet_id and running_service.pricing_detail_cost_per_unit: + if running_service.wallet_id and running_service.pricing_unit_cost: # Compute currently used credits computed_credits = await _compute_service_run_credit_costs( running_service.started_at, msg.created_at, - running_service.pricing_detail_cost_per_unit, + running_service.pricing_unit_cost, ) # Update credits in the transaction table update_credit_transaction = CreditTransactionCreditsUpdate( @@ -170,18 +160,12 @@ async def _process_heartbeat_event( update_credit_transaction ) # Publish wallet total credits to RabbitMQ - wallet_total_credits = ( - await resource_tracker_repo.sum_credit_transactions_by_product_and_wallet( - running_service.product_name, - running_service.wallet_id, - ) - ) - publish_message = WalletCreditsMessage.construct( - wallet_id=running_service.wallet_id, - created_at=datetime.now(tz=timezone.utc), - credits=wallet_total_credits.available_osparc_credits, + await sum_credit_transactions_and_publish_to_rabbitmq( + resource_tracker_repo, + rabbitmq_client, + running_service.product_name, + running_service.wallet_id, ) - await rabbitmq_client.publish(publish_message.channel_name, publish_message) async def _process_stop_event( @@ -205,12 +189,12 @@ async def _process_stop_event( _logger.error("Nothing to update. This should not happen investigate.") return - if running_service.wallet_id and running_service.pricing_detail_cost_per_unit: + if running_service.wallet_id and running_service.pricing_unit_cost: # Compute currently used credits computed_credits = await _compute_service_run_credit_costs( running_service.started_at, msg.created_at, - running_service.pricing_detail_cost_per_unit, + running_service.pricing_unit_cost, ) # Update credits in the transaction table and close the transaction update_credit_transaction = CreditTransactionCreditsAndStatusUpdate( @@ -224,18 +208,12 @@ async def _process_stop_event( update_credit_transaction ) # Publish wallet total credits to RabbitMQ - wallet_total_credits = ( - await resource_tracker_repo.sum_credit_transactions_by_product_and_wallet( - running_service.product_name, - running_service.wallet_id, - ) - ) - publish_message = WalletCreditsMessage.construct( - wallet_id=running_service.wallet_id, - created_at=datetime.now(tz=timezone.utc), - credits=wallet_total_credits.available_osparc_credits, + await sum_credit_transactions_and_publish_to_rabbitmq( + resource_tracker_repo, + rabbitmq_client, + running_service.product_name, + running_service.wallet_id, ) - await rabbitmq_client.publish(publish_message.channel_name, publish_message) RABBIT_MSG_TYPE_TO_PROCESS_HANDLER: dict[str, Callable[..., Awaitable[None]],] = { diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_utils.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_utils.py index 70e5c421023..43c86493f80 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_utils.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/resource_tracker_utils.py @@ -1,7 +1,35 @@ import logging +from datetime import datetime, timezone + +from models_library.products import ProductName +from models_library.rabbitmq_messages import WalletCreditsMessage +from models_library.wallets import WalletID +from servicelib.rabbitmq import RabbitMQClient + +from .modules.db.repositories.resource_tracker import ResourceTrackerRepository _logger = logging.getLogger(__name__) def make_negative(n): return -abs(n) + + +async def sum_credit_transactions_and_publish_to_rabbitmq( + resource_tracker_repo: ResourceTrackerRepository, + rabbitmq_client: RabbitMQClient, + product_name: ProductName, + wallet_id: WalletID, +): + wallet_total_credits = ( + await resource_tracker_repo.sum_credit_transactions_by_product_and_wallet( + product_name, + wallet_id, + ) + ) + publish_message = WalletCreditsMessage.construct( + wallet_id=wallet_id, + created_at=datetime.now(tz=timezone.utc), + credits=wallet_total_credits.available_osparc_credits, + ) + await rabbitmq_client.publish(publish_message.channel_name, publish_message) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_credit_transactions.py index 1703f0d1821..44bc2a70954 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_credit_transactions.py @@ -1,4 +1,3 @@ -from datetime import datetime, timezone from typing import Annotated from fastapi import Depends @@ -7,7 +6,6 @@ WalletTotalCredits, ) from models_library.products import ProductName -from models_library.rabbitmq_messages import WalletCreditsMessage from models_library.resource_tracker import ( CreditClassification, CreditTransactionId, @@ -20,26 +18,7 @@ from ..models.resource_tracker_credit_transactions import CreditTransactionCreate from ..modules.db.repositories.resource_tracker import ResourceTrackerRepository from ..modules.rabbitmq import get_rabbitmq_client_from_request - - -async def _sum_credit_transactions_and_publish_to_rabbitmq( - resource_tracker_repo: ResourceTrackerRepository, - credit_transaction_create_body: CreditTransactionCreateBody, - wallet_id: WalletID, - rabbitmq_client: RabbitMQClient, -): - wallet_total_credits = ( - await resource_tracker_repo.sum_credit_transactions_by_product_and_wallet( - credit_transaction_create_body.product_name, - credit_transaction_create_body.wallet_id, - ) - ) - publish_message = WalletCreditsMessage.construct( - wallet_id=wallet_id, - created_at=datetime.now(tz=timezone.utc), - credits=wallet_total_credits.available_osparc_credits, - ) - await rabbitmq_client.publish(publish_message.channel_name, publish_message) +from ..resource_tracker_utils import sum_credit_transactions_and_publish_to_rabbitmq async def create_credit_transaction( @@ -56,7 +35,8 @@ async def create_credit_transaction( wallet_id=credit_transaction_create_body.wallet_id, wallet_name=credit_transaction_create_body.wallet_name, pricing_plan_id=None, - pricing_detail_id=None, + pricing_unit_id=None, + pricing_unit_cost_id=None, user_id=credit_transaction_create_body.user_id, user_email=credit_transaction_create_body.user_email, osparc_credits=credit_transaction_create_body.osparc_credits, @@ -71,11 +51,11 @@ async def create_credit_transaction( transaction_create ) - await _sum_credit_transactions_and_publish_to_rabbitmq( + await sum_credit_transactions_and_publish_to_rabbitmq( resource_tracker_repo, - credit_transaction_create_body, - credit_transaction_create_body.wallet_id, rabbitmq_client, + credit_transaction_create_body.product_name, + credit_transaction_create_body.wallet_id, ) return transaction_id diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_pricing_plans.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_pricing_plans.py index 597277f251f..e7baa753c02 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_pricing_plans.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_pricing_plans.py @@ -1,60 +1,86 @@ from typing import Annotated -from fastapi import Depends, Query -from models_library.api_schemas_webserver.resource_usage import ( - PricingDetailMinimalGet, - PricingPlanGet, +from fastapi import Depends +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, + ServicePricingPlanGet, ) from models_library.products import ProductName +from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.services import ServiceKey, ServiceVersion from ..api.dependencies import get_repository +from ..core.errors import ResourceUsageTrackerCustomRuntimeError from ..modules.db.repositories.resource_tracker import ResourceTrackerRepository -async def list_pricing_plans( +async def get_service_default_pricing_plan( product_name: ProductName, + service_key: ServiceKey, + service_version: ServiceVersion, resource_tracker_repo: Annotated[ ResourceTrackerRepository, Depends(get_repository(ResourceTrackerRepository)) ], - service_key: ServiceKey = Query(None), - service_version: ServiceVersion = Query(None), -) -> list[PricingPlanGet]: - if service_key is None and service_version is None: - active_pricing_plans = ( - await resource_tracker_repo.list_active_pricing_plans_by_product( - product_name - ) - ) - else: - active_pricing_plans = await resource_tracker_repo.list_active_pricing_plans_by_product_and_service( - product_name, service_key, service_version +) -> ServicePricingPlanGet: + active_service_pricing_plans = await resource_tracker_repo.list_active_service_pricing_plans_by_product_and_service( + product_name, service_key, service_version + ) + + default_pricing_plan = None + for active_service_pricing_plan in active_service_pricing_plans: + if active_service_pricing_plan.service_default_plan is True: + default_pricing_plan = active_service_pricing_plan + break + + if default_pricing_plan is None: + raise ResourceUsageTrackerCustomRuntimeError( + msg="No default pricing plan for the specified service" ) - list_of_pricing_plan_get = [] - for active_pricing_plan in active_pricing_plans: - list_of_pricing_detail_db = ( - await resource_tracker_repo.list_pricing_details_by_pricing_plan( - active_pricing_plan.pricing_plan_id - ) + pricing_plan_unit_db = ( + await resource_tracker_repo.list_pricing_units_by_pricing_plan( + pricing_plan_id=default_pricing_plan.pricing_plan_id ) - list_of_pricing_plan_get.append( - PricingPlanGet( - pricing_plan_id=active_pricing_plan.pricing_plan_id, - name=active_pricing_plan.name, - description=active_pricing_plan.description, - classification=active_pricing_plan.classification, - created_at=active_pricing_plan.created, - details=[ - PricingDetailMinimalGet( - pricing_detail_id=detail.pricing_detail_id, - unit_name=detail.unit_name, - cost_per_unit=detail.cost_per_unit, - valid_from=detail.valid_from, - simcore_default=detail.simcore_default, - ) - for detail in list_of_pricing_detail_db - ], + ) + + return ServicePricingPlanGet( + pricing_plan_id=default_pricing_plan.pricing_plan_id, + display_name=default_pricing_plan.display_name, + description=default_pricing_plan.description, + classification=default_pricing_plan.classification, + created_at=default_pricing_plan.created, + pricing_plan_key=default_pricing_plan.pricing_plan_key, + pricing_units=[ + PricingUnitGet( + pricing_unit_id=unit.pricing_unit_id, + unit_name=unit.unit_name, + current_cost_per_unit=unit.current_cost_per_unit, + current_cost_per_unit_id=unit.current_cost_per_unit_id, + default=unit.default, + specific_info=unit.specific_info, ) - ) - return list_of_pricing_plan_get + for unit in pricing_plan_unit_db + ], + ) + + +async def get_pricing_unit( + product_name: ProductName, + pricing_plan_id: PricingPlanId, + pricing_unit_id: PricingUnitId, + resource_tracker_repo: Annotated[ + ResourceTrackerRepository, Depends(get_repository(ResourceTrackerRepository)) + ], +) -> PricingUnitGet: + pricing_unit = await resource_tracker_repo.get_pricing_unit( + product_name, pricing_plan_id, pricing_unit_id + ) + + return PricingUnitGet( + pricing_unit_id=pricing_unit.pricing_unit_id, + unit_name=pricing_unit.unit_name, + current_cost_per_unit=pricing_unit.current_cost_per_unit, + current_cost_per_unit_id=pricing_unit.current_cost_per_unit_id, + default=pricing_unit.default, + specific_info=pricing_unit.specific_info, + ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_service_runs.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_service_runs.py index bbc7b6027bf..568ddcdaabd 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_service_runs.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/resource_tracker_service_runs.py @@ -1,15 +1,19 @@ from typing import Annotated from fastapi import Depends, Query -from models_library.api_schemas_webserver.resource_usage import ServiceRunGet +from models_library.api_schemas_resource_usage_tracker.service_runs import ServiceRunGet from models_library.products import ProductName from models_library.users import UserID from models_library.wallets import WalletID from pydantic import PositiveInt from ..api.dependencies import get_repository +from ..core.errors import ResourceUsageTrackerCustomRuntimeError from ..models.pagination import LimitOffsetParamsWithDefault -from ..models.resource_tracker_service_run import ServiceRunDB, ServiceRunPage +from ..models.resource_tracker_service_runs import ( + ServiceRunPage, + ServiceRunWithCreditsDB, +) from ..modules.db.repositories.resource_tracker import ResourceTrackerRepository @@ -25,41 +29,37 @@ async def list_service_runs( ) -> ServiceRunPage: # Situation when we want to see all usage of a specific user if wallet_id is None and access_all_wallet_usage is None: - total_service_runs: PositiveInt = ( - await resource_tacker_repo.total_service_runs_by_user_and_product( - user_id, product_name - ) + total_service_runs: PositiveInt = await resource_tacker_repo.total_service_runs_by_product_and_user_and_wallet( + product_name, user_id, None ) service_runs_db_model: list[ - ServiceRunDB - ] = await resource_tacker_repo.list_service_runs_by_user_and_product( - user_id, product_name, page_params.offset, page_params.limit + ServiceRunWithCreditsDB + ] = await resource_tacker_repo.list_service_runs_by_product_and_user_and_wallet( + product_name, user_id, None, page_params.offset, page_params.limit ) # Situation when accountant user can see all users usage of the wallet elif wallet_id and access_all_wallet_usage is True: - total_service_runs: PositiveInt = ( # type: ignore[no-redef] - await resource_tacker_repo.total_service_runs_by_product_and_wallet( - product_name, wallet_id - ) + total_service_runs: PositiveInt = await resource_tacker_repo.total_service_runs_by_product_and_user_and_wallet( # type: ignore[no-redef] + product_name, None, wallet_id ) service_runs_db_model: list[ # type: ignore[no-redef] - ServiceRunDB - ] = await resource_tacker_repo.list_service_runs_by_product_and_wallet( - product_name, wallet_id, page_params.offset, page_params.limit + ServiceRunWithCreditsDB + ] = await resource_tacker_repo.list_service_runs_by_product_and_user_and_wallet( + product_name, None, wallet_id, page_params.offset, page_params.limit ) # Situation when regular user can see only his usage of the wallet elif wallet_id and access_all_wallet_usage is False: - total_service_runs: PositiveInt = await resource_tacker_repo.total_service_runs_by_user_and_product_and_wallet( # type: ignore[no-redef] - user_id, product_name, wallet_id + total_service_runs: PositiveInt = await resource_tacker_repo.total_service_runs_by_product_and_user_and_wallet( # type: ignore[no-redef] + product_name, user_id, wallet_id ) service_runs_db_model: list[ # type: ignore[no-redef] - ServiceRunDB - ] = await resource_tacker_repo.list_service_runs_by_user_and_product_and_wallet( - user_id, product_name, wallet_id, page_params.offset, page_params.limit + ServiceRunWithCreditsDB + ] = await resource_tacker_repo.list_service_runs_by_product_and_user_and_wallet( + product_name, user_id, wallet_id, page_params.offset, page_params.limit ) else: msg = "wallet_id and access_all_wallet_usage parameters must be specified together" - raise ValueError(msg) + raise ResourceUsageTrackerCustomRuntimeError(msg=msg) service_runs_api_model: list[ServiceRunGet] = [] for service in service_runs_db_model: @@ -80,6 +80,8 @@ async def list_service_runs( started_at=service.started_at, stopped_at=service.stopped_at, service_run_status=service.service_run_status, + credit_cost=service.osparc_credits, + transaction_status=service.transaction_status, ) ) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/conftest.py b/services/resource-usage-tracker/tests/unit/with_dbs/conftest.py index 8d99005ec9f..b7204c1298d 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/conftest.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/conftest.py @@ -5,6 +5,7 @@ from collections.abc import AsyncIterable, Callable from datetime import datetime, timezone +from random import choice from typing import Any import httpx @@ -29,6 +30,12 @@ ) from simcore_service_resource_usage_tracker.core.application import create_app from simcore_service_resource_usage_tracker.core.settings import ApplicationSettings +from simcore_service_resource_usage_tracker.models.resource_tracker_credit_transactions import ( + CreditTransactionDB, +) +from simcore_service_resource_usage_tracker.models.resource_tracker_service_runs import ( + ServiceRunDB, +) from tenacity._asyncio import AsyncRetrying from tenacity.retry import retry_if_exception_type from tenacity.stop import stop_after_delay @@ -80,7 +87,8 @@ def _creator(**overrides) -> dict[str, Any]: "wallet_id": faker.pyint(), "wallet_name": faker.word(), "pricing_plan_id": faker.pyint(), - "pricing_detail_id": faker.pyint(), + "pricing_unit_id": faker.pyint(), + "pricing_unit_cost_id": faker.pyint(), "simcore_user_agent": faker.word(), "user_id": faker.pyint(), "user_email": faker.email(), @@ -105,6 +113,35 @@ def _creator(**overrides) -> dict[str, Any]: return _creator +@pytest.fixture +def random_resource_tracker_credit_transactions( + faker: Faker, +) -> Callable[..., dict[str, Any]]: + def _creator(**overrides) -> dict[str, Any]: + data = { + "product_name": "osparc", + "wallet_id": faker.pyint(), + "wallet_name": faker.word(), + "pricing_plan_id": faker.pyint(), + "pricing_unit_id": faker.pyint(), + "pricing_unit_cost_id": faker.pyint(), + "user_id": faker.pyint(), + "user_email": faker.email(), + "osparc_credits": -abs(faker.pyfloat()), + "transaction_status": choice(["BILLED", "PENDING", "NOT_BILLED"]), + "transaction_classification": "DEDUCT_SERVICE_RUN", + "service_run_id": faker.uuid4(), + "payment_transaction_id": faker.uuid4(), + "created": datetime.now(tz=timezone.utc), + "last_heartbeat_at": datetime.now(tz=timezone.utc), + "modified": datetime.now(tz=timezone.utc), + } + data.update(overrides) + return data + + return _creator + + @pytest.fixture() def resource_tracker_service_run_db(postgres_db: sa.engine.Engine): with postgres_db.connect() as con: @@ -116,7 +153,7 @@ def resource_tracker_service_run_db(postgres_db: sa.engine.Engine): async def assert_service_runs_db_row( postgres_db, service_run_id: str, status: str | None = None -) -> dict | None: +) -> ServiceRunDB: async for attempt in AsyncRetrying( wait=wait_fixed(0.2), stop=stop_after_delay(10), @@ -124,24 +161,23 @@ async def assert_service_runs_db_row( reraise=True, ): with attempt, postgres_db.connect() as con: - # removes all projects before continuing - con.execute(resource_tracker_service_runs.select()) result = con.execute( sa.select(resource_tracker_service_runs).where( resource_tracker_service_runs.c.service_run_id == service_run_id ) ) - row: dict | None = result.first() + row = result.first() assert row + service_run_db = ServiceRunDB.from_orm(row) if status: - assert row[21] == status - return row - return None + assert service_run_db.service_run_status == status + return service_run_db + raise ValueError async def assert_credit_transactions_db_row( postgres_db, service_run_id: str, modified_at: datetime | None = None -) -> dict | None: +) -> CreditTransactionDB: async for attempt in AsyncRetrying( wait=wait_fixed(0.2), stop=stop_after_delay(10), @@ -149,19 +185,19 @@ async def assert_credit_transactions_db_row( reraise=True, ): with attempt, postgres_db.connect() as con: - con.execute(resource_tracker_credit_transactions.select()) result = con.execute( sa.select(resource_tracker_credit_transactions).where( resource_tracker_credit_transactions.c.service_run_id == service_run_id ) ) - row: dict | None = result.first() + row = result.first() assert row + credit_transaction_db = CreditTransactionDB.from_orm(row) if modified_at: - assert row[15] > modified_at - return row - return None + assert credit_transaction_db.modified > modified_at + return credit_transaction_db + raise ValueError @pytest.fixture @@ -189,7 +225,8 @@ def _creator(**kwargs: dict[str, Any]) -> RabbitResourceTrackingStartedMessage: "wallet_id": faker.pyint(), "wallet_name": faker.pystr(), "pricing_plan_id": faker.pyint(), - "pricing_detail_id": faker.pyint(), + "pricing_unit_id": faker.pyint(), + "pricing_unit_cost_id": faker.pyint(), "product_name": "osparc", "simcore_user_agent": faker.pystr(), "user_id": faker.pyint(), 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_resource_tracker_pricing_plans.py index 0201eaf7e55..4f5b5f80afa 100644 --- 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_resource_tracker_pricing_plans.py @@ -6,15 +6,18 @@ import httpx import pytest import sqlalchemy as sa -from simcore_postgres_database.models.resource_tracker_pricing_details import ( - resource_tracker_pricing_details, -) from simcore_postgres_database.models.resource_tracker_pricing_plan_to_service import ( resource_tracker_pricing_plan_to_service, ) from simcore_postgres_database.models.resource_tracker_pricing_plans import ( resource_tracker_pricing_plans, ) +from simcore_postgres_database.models.resource_tracker_pricing_unit_costs import ( + resource_tracker_pricing_unit_costs, +) +from simcore_postgres_database.models.resource_tracker_pricing_units import ( + resource_tracker_pricing_units, +) from starlette import status from yarl import URL @@ -25,6 +28,10 @@ "adminer", ] +_SERVICE_KEY = "simcore/services/comp/itis/sleeper" +_SERVICE_VERSION = "1.0.16" +_PRICING_PLAN_ID = 1 + @pytest.fixture() def resource_tracker_pricing_tables_db(postgres_db: sa.engine.Engine) -> Iterator[None]: @@ -32,77 +39,141 @@ def resource_tracker_pricing_tables_db(postgres_db: sa.engine.Engine) -> Iterato con.execute( resource_tracker_pricing_plans.insert().values( product_name="osparc", - name="test_name", + display_name="ISolve Thermal", description="", classification="TIER", is_active=True, + pricing_plan_key="isolve-thermal", ) ) con.execute( - resource_tracker_pricing_details.insert().values( - pricing_plan_id=1, + resource_tracker_pricing_units.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, unit_name="S", + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ), + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, + pricing_plan_key="isolve-thermal", + pricing_unit_id=1, + pricing_unit_name="S", cost_per_unit=Decimal(5), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=True, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( - pricing_plan_id=1, + resource_tracker_pricing_units.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, unit_name="M", + default=True, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ), + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, + pricing_plan_key="isolve-thermal", + pricing_unit_id=2, + pricing_unit_name="M", cost_per_unit=Decimal(15.6), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( - pricing_plan_id=1, + resource_tracker_pricing_units.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, unit_name="L", + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ), + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, + pricing_plan_key="isolve-thermal", + pricing_unit_id=3, + pricing_unit_name="L", + cost_per_unit=Decimal(17.7), + valid_from=datetime.now(tz=timezone.utc), + valid_to=datetime.now(tz=timezone.utc), + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=_PRICING_PLAN_ID, + pricing_plan_key="isolve-thermal", + pricing_unit_id=3, + pricing_unit_name="L", cost_per_unit=Decimal(28.9), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( resource_tracker_pricing_plan_to_service.insert().values( - pricing_plan_id=1, - product="osparc", - service_key="simcore/services/comp/itis/sleeper", - service_version="1.0.16", + pricing_plan_id=_PRICING_PLAN_ID, + service_key=_SERVICE_KEY, + service_version=_SERVICE_VERSION, + service_default_plan=True, ) ) yield con.execute(resource_tracker_pricing_plan_to_service.delete()) - con.execute(resource_tracker_pricing_details.delete()) + con.execute(resource_tracker_pricing_units.delete()) con.execute(resource_tracker_pricing_plans.delete()) + con.execute(resource_tracker_pricing_unit_costs.delete()) -async def test_pricing_plans_get( +async def test_get_default_pricing_plan_for_service( mocked_redis_server: None, mocked_setup_rabbitmq: mock.Mock, postgres_db: sa.engine.Engine, resource_tracker_pricing_tables_db: None, async_client: httpx.AsyncClient, ): - url = URL("/v1/pricing-plans") - + url = URL(f"/v1/services/{_SERVICE_KEY}/{_SERVICE_VERSION}/pricing-plan") response = await async_client.get(f'{url.with_query({"product_name": "osparc"})}') assert response.status_code == status.HTTP_200_OK data = response.json() - assert len(data) == 1 - assert len(data[0]["details"]) == 3 + assert data + assert len(data["pricing_units"]) == 3 + assert data["pricing_units"][0]["unit_name"] == "S" + assert data["pricing_units"][1]["unit_name"] == "M" + assert data["pricing_units"][2]["unit_name"] == "L" - response = await async_client.get( - f'{url.with_query({"product_name": "osparc", "service_key": "simcore/services/comp/itis/sleeper", "service_version": "1.0.16"})}' - ) + _PRICING_UNIT_ID = 2 + url = URL(f"/v1/pricing-plans/{_PRICING_PLAN_ID}/pricing-units/{_PRICING_UNIT_ID}") + response = await async_client.get(f'{url.with_query({"product_name": "osparc"})}') assert response.status_code == status.HTTP_200_OK data = response.json() - assert len(data) == 1 - assert len(data[0]["details"]) == 3 + assert data + assert data["pricing_unit_id"] == _PRICING_UNIT_ID 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_resource_tracker_service_runs__list_billable.py new file mode 100644 index 00000000000..2b5010c8418 --- /dev/null +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_resource_tracker_service_runs__list_billable.py @@ -0,0 +1,89 @@ +from collections.abc import Iterator +from unittest import mock + +import httpx +import pytest +import sqlalchemy as sa +from models_library.resource_tracker import CreditTransactionStatus +from simcore_postgres_database.models.resource_tracker_credit_transactions import ( + resource_tracker_credit_transactions, +) +from simcore_postgres_database.models.resource_tracker_service_runs import ( + resource_tracker_service_runs, +) +from starlette import status +from yarl import URL + +pytest_simcore_core_services_selection = [ + "postgres", +] +pytest_simcore_ops_services_selection = [ + "adminer", +] + +_USER_ID = 1 +_SERVICE_RUN_ID = "12345" + + +@pytest.fixture() +def resource_tracker_setup_db( + postgres_db: sa.engine.Engine, + random_resource_tracker_service_run, + random_resource_tracker_credit_transactions, +) -> Iterator[None]: + with postgres_db.connect() as con: + con.execute(resource_tracker_service_runs.delete()) + con.execute(resource_tracker_credit_transactions.delete()) + result = con.execute( + resource_tracker_service_runs.insert() + .values( + **random_resource_tracker_service_run( + user_id=_USER_ID, + service_run_id=_SERVICE_RUN_ID, + product_name="osparc", + ) + ) + .returning(resource_tracker_service_runs) + ) + row = result.first() + assert row + + result = con.execute( + resource_tracker_credit_transactions.insert() + .values( + **random_resource_tracker_credit_transactions( + user_id=_USER_ID, + service_run_id=_SERVICE_RUN_ID, + product_name="osparc", + ) + ) + .returning(resource_tracker_credit_transactions) + ) + row = result.first() + assert row + + yield + + con.execute(resource_tracker_credit_transactions.delete()) + con.execute(resource_tracker_service_runs.delete()) + + +async def test_list_service_runs_which_was_billed( + mocked_redis_server: None, + mocked_setup_rabbitmq: mock.Mock, + postgres_db: sa.engine.Engine, + resource_tracker_setup_db: dict, + async_client: httpx.AsyncClient, +): + url = URL("/v1/services/-/usages") + + response = await async_client.get( + f'{url.with_query({"product_name": "osparc", "user_id": _USER_ID})}' + ) + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert len(data["items"]) == 1 + assert data["total"] == 1 + + assert data["items"][0]["credit_cost"] < 0 + assert data["items"][0]["transaction_status"] in list(CreditTransactionStatus) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message.py index 3cf8600d151..c8985e764ad 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message.py @@ -37,25 +37,29 @@ async def test_process_event_functions( publisher = rabbitmq_client("publisher") msg = random_rabbit_message_start( - wallet_id=None, wallet_name=None, pricing_plan_id=None, pricing_detail_id=None + wallet_id=None, + wallet_name=None, + pricing_plan_id=None, + pricing_unit_id=None, + pricing_unit_cost_id=None, ) resource_tacker_repo: ResourceTrackerRepository = ResourceTrackerRepository( db_engine=engine ) await _process_start_event(resource_tacker_repo, msg, publisher) output = await assert_service_runs_db_row(postgres_db, msg.service_run_id) - assert output[20] is None # stopped_at - assert output[21] == "RUNNING" # status - first_occurence_of_last_heartbeat_at = output[23] # last_heartbeat_at + assert output.stopped_at is None + assert output.service_run_status == "RUNNING" + first_occurence_of_last_heartbeat_at = output.last_heartbeat_at heartbeat_msg = RabbitResourceTrackingHeartbeatMessage( service_run_id=msg.service_run_id, created_at=datetime.now(tz=timezone.utc) ) await _process_heartbeat_event(resource_tacker_repo, heartbeat_msg, publisher) output = await assert_service_runs_db_row(postgres_db, msg.service_run_id) - assert output[20] is None # stopped_at - assert output[21] == "RUNNING" # status - first_occurence_of_last_heartbeat_at < output[23] # last_heartbeat_at + assert output.stopped_at is None + assert output.service_run_status == "RUNNING" + first_occurence_of_last_heartbeat_at < output.last_heartbeat_at stopped_msg = RabbitResourceTrackingStoppedMessage( service_run_id=msg.service_run_id, @@ -64,5 +68,5 @@ async def test_process_event_functions( ) await _process_stop_event(resource_tacker_repo, stopped_msg, publisher) output = await assert_service_runs_db_row(postgres_db, msg.service_run_id) - assert output[20] is not None # stopped_at - assert output[21] == "SUCCESS" # status + assert output.stopped_at is not None + assert output.service_run_status == "SUCCESS" diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening.py index d06efefdb33..c80652d3ffe 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening.py @@ -32,7 +32,11 @@ async def test_process_events_via_rabbit( ): publisher = rabbitmq_client("publisher") msg = random_rabbit_message_start( - wallet_id=None, wallet_name=None, pricing_plan_id=None, pricing_detail_id=None + wallet_id=None, + wallet_name=None, + pricing_plan_id=None, + pricing_unit_id=None, + pricing_unit_cost_id=None, ) await publisher.publish(RabbitResourceTrackingBaseMessage.get_channel_name(), msg) await assert_service_runs_db_row(postgres_db, msg.service_run_id, "RUNNING") diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing_triggered_by_listening.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening_with_billing.py similarity index 51% rename from services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing_triggered_by_listening.py rename to services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening_with_billing.py index 52d1036e495..b22bb8d9248 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing_triggered_by_listening.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_triggered_by_listening_with_billing.py @@ -1,5 +1,6 @@ import asyncio from datetime import datetime, timezone +from decimal import Decimal from typing import Callable, Iterator import pytest @@ -11,8 +12,8 @@ SimcorePlatformStatus, ) from servicelib.rabbitmq import RabbitMQClient -from simcore_postgres_database.models.resource_tracker_pricing_details import ( - resource_tracker_pricing_details, +from simcore_postgres_database.models.resource_tracker_credit_transactions import ( + resource_tracker_credit_transactions, ) from simcore_postgres_database.models.resource_tracker_pricing_plan_to_service import ( resource_tracker_pricing_plan_to_service, @@ -20,6 +21,12 @@ from simcore_postgres_database.models.resource_tracker_pricing_plans import ( resource_tracker_pricing_plans, ) +from simcore_postgres_database.models.resource_tracker_pricing_unit_costs import ( + resource_tracker_pricing_unit_costs, +) +from simcore_postgres_database.models.resource_tracker_pricing_units import ( + resource_tracker_pricing_units, +) from .conftest import assert_service_runs_db_row @@ -38,56 +45,104 @@ def resource_tracker_pricing_tables_db(postgres_db: sa.engine.Engine) -> Iterato con.execute( resource_tracker_pricing_plans.insert().values( product_name="osparc", - name="test_name", + display_name="ISolve Thermal", description="", classification="TIER", is_active=True, + pricing_plan_key="isolve-thermal", ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="S", - cost_per_unit=500, + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=1, + pricing_unit_name="S", + cost_per_unit=Decimal(500), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=True, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="M", - cost_per_unit=1000, + default=True, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=2, + pricing_unit_name="M", + cost_per_unit=Decimal(1000), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="L", - cost_per_unit=1500, + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=3, + pricing_unit_name="L", + cost_per_unit=Decimal(1500), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( resource_tracker_pricing_plan_to_service.insert().values( pricing_plan_id=1, - product="osparc", service_key="simcore/services/comp/itis/sleeper", service_version="1.0.16", + service_default_plan=True, ) ) yield con.execute(resource_tracker_pricing_plan_to_service.delete()) - con.execute(resource_tracker_pricing_details.delete()) + con.execute(resource_tracker_pricing_units.delete()) con.execute(resource_tracker_pricing_plans.delete()) + con.execute(resource_tracker_pricing_unit_costs.delete()) + con.execute(resource_tracker_credit_transactions.delete()) async def test_process_events_via_rabbit( @@ -101,7 +156,11 @@ async def test_process_events_via_rabbit( ): publisher = rabbitmq_client("publisher") msg = random_rabbit_message_start( - wallet_id=1, wallet_name="what ever", pricing_plan_id=1, pricing_detail_id=1 + wallet_id=1, + wallet_name="what ever", + pricing_plan_id=1, + pricing_unit_id=1, + pricing_unit_cost_id=1, ) await publisher.publish(RabbitResourceTrackingBaseMessage.get_channel_name(), msg) await asyncio.sleep(3) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing.py index 7e985084311..97c237b1955 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_process_rabbitmq_message_with_billing.py @@ -14,15 +14,18 @@ from simcore_postgres_database.models.resource_tracker_credit_transactions import ( resource_tracker_credit_transactions, ) -from simcore_postgres_database.models.resource_tracker_pricing_details import ( - resource_tracker_pricing_details, -) from simcore_postgres_database.models.resource_tracker_pricing_plan_to_service import ( resource_tracker_pricing_plan_to_service, ) from simcore_postgres_database.models.resource_tracker_pricing_plans import ( resource_tracker_pricing_plans, ) +from simcore_postgres_database.models.resource_tracker_pricing_unit_costs import ( + resource_tracker_pricing_unit_costs, +) +from simcore_postgres_database.models.resource_tracker_pricing_units import ( + resource_tracker_pricing_units, +) from simcore_service_resource_usage_tracker.modules.db.repositories.resource_tracker import ( ResourceTrackerRepository, ) @@ -49,56 +52,103 @@ def resource_tracker_pricing_tables_db(postgres_db: sa.engine.Engine) -> Iterato con.execute( resource_tracker_pricing_plans.insert().values( product_name="osparc", - name="test_name", + display_name="ISolve Thermal", description="", classification="TIER", is_active=True, + pricing_plan_key="isolve-thermal", ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="S", - cost_per_unit=Decimal(1500), + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=1, + pricing_unit_name="S", + cost_per_unit=Decimal(500), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=True, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="M", - cost_per_unit=Decimal(1500), + default=True, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=2, + pricing_unit_name="M", + cost_per_unit=Decimal(1000), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( - resource_tracker_pricing_details.insert().values( + resource_tracker_pricing_units.insert().values( pricing_plan_id=1, unit_name="L", + default=False, + specific_info={}, + created=datetime.now(tz=timezone.utc), + modified=datetime.now(tz=timezone.utc), + ) + ) + con.execute( + resource_tracker_pricing_unit_costs.insert().values( + pricing_plan_id=1, + pricing_plan_key="isolve-thermal", + pricing_unit_id=3, + pricing_unit_name="L", cost_per_unit=Decimal(1500), valid_from=datetime.now(tz=timezone.utc), - ), - simcore_default=False, - specific_info={}, + valid_to=None, + specific_info={}, + created=datetime.now(tz=timezone.utc), + comment="", + modified=datetime.now(tz=timezone.utc), + ) ) con.execute( resource_tracker_pricing_plan_to_service.insert().values( pricing_plan_id=1, - product="osparc", service_key="simcore/services/comp/itis/sleeper", service_version="1.0.16", + service_default_plan=True, ) ) yield con.execute(resource_tracker_pricing_plan_to_service.delete()) - con.execute(resource_tracker_pricing_details.delete()) + con.execute(resource_tracker_pricing_units.delete()) con.execute(resource_tracker_pricing_plans.delete()) + con.execute(resource_tracker_pricing_unit_costs.delete()) con.execute(resource_tracker_credit_transactions.delete()) @@ -115,18 +165,22 @@ async def test_process_event_functions( publisher = rabbitmq_client("publisher") msg = random_rabbit_message_start( - wallet_id=1, wallet_name="test", pricing_plan_id=1, pricing_detail_id=1 + wallet_id=1, + wallet_name="test", + pricing_plan_id=1, + pricing_unit_id=1, + pricing_unit_cost_id=1, ) resource_tacker_repo: ResourceTrackerRepository = ResourceTrackerRepository( db_engine=engine ) await _process_start_event(resource_tacker_repo, msg, publisher) output = await assert_credit_transactions_db_row(postgres_db, msg.service_run_id) - assert output[8] == 0.0 - assert output[9] == "PENDING" - assert output[10] == "DEDUCT_SERVICE_RUN" - first_occurence_of_last_heartbeat_at = output[14] - modified_at = output[15] + assert output.osparc_credits == 0.0 + assert output.transaction_status == "PENDING" + assert output.transaction_classification == "DEDUCT_SERVICE_RUN" + first_occurence_of_last_heartbeat_at = output.last_heartbeat_at + modified_at = output.modified await asyncio.sleep(0) heartbeat_msg = RabbitResourceTrackingHeartbeatMessage( @@ -136,11 +190,11 @@ async def test_process_event_functions( output = await assert_credit_transactions_db_row( postgres_db, msg.service_run_id, modified_at ) - first_credits_used = output[8] + first_credits_used = output.osparc_credits assert first_credits_used < 0.0 - assert output[9] == "PENDING" - assert first_occurence_of_last_heartbeat_at < output[14] - modified_at = output[15] + assert output.transaction_status == "PENDING" + assert first_occurence_of_last_heartbeat_at < output.last_heartbeat_at + modified_at = output.modified await asyncio.sleep( 2 @@ -154,5 +208,5 @@ async def test_process_event_functions( output = await assert_credit_transactions_db_row( postgres_db, msg.service_run_id, modified_at ) - assert output[8] < first_credits_used - assert output[9] == "BILLED" + assert output.osparc_credits < first_credits_used + assert output.transaction_status == "BILLED" diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 2650c5af23f..1c4c8b049f9 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -223,7 +223,7 @@ qx.Class.define("osparc.data.Resources", { endpoints: { getPage: { method: "GET", - url: statics.API + "/resource-usage/services?offset={offset}&limit={limit}" + url: statics.API + "/services/-/resource-usages?offset={offset}&limit={limit}" } } }, @@ -232,7 +232,7 @@ qx.Class.define("osparc.data.Resources", { endpoints: { getPage: { method: "GET", - url: statics.API + "/resource-usage/services?wallet_id={walletId}&offset={offset}&limit={limit}" + url: statics.API + "/services/-/resource-usages?wallet_id={walletId}&offset={offset}&limit={limit}" } } }, 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 684bca41c8e..127645835f3 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 @@ -799,6 +799,35 @@ paths: schema: title: Response Get Service Resources type: object + /v0/catalog/services/{service_key}/{service_version}/pricing-plan: + get: + tags: + - catalog + - pricing-plans + summary: Retrieve default pricing plan for provided service + operationId: get_service_pricing_plan + parameters: + - required: true + schema: + title: Service Key + pattern: ^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$ + type: string + name: service_key + in: path + - required: true + schema: + title: Service Version + pattern: ^(0|[1-9]\d*)(\.(0|[1-9]\d*)){2}(-(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*)(\.(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*))*)?(\+[-\da-zA-Z]+(\.[-\da-zA-Z-]+)*)?$ + type: string + name: service_version + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_ServicePricingPlanGet_' /v0/clusters: get: tags: @@ -2867,7 +2896,7 @@ paths: responses: '204': description: Successful Response - /v0/resource-usage/services: + /v0/services/-/resource-usages: get: tags: - usage @@ -2905,6 +2934,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_models_library.api_schemas_webserver.resource_usage.ServiceRunGet__' + /v0/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}: + get: + tags: + - pricing-plans + summary: Retrieve detail information about pricing unit + operationId: get_pricing_plan_unit + parameters: + - required: true + schema: + title: Pricing Plan Id + exclusiveMinimum: true + type: integer + minimum: 0 + name: pricing_plan_id + in: path + - required: true + schema: + title: Pricing Unit Id + exclusiveMinimum: true + type: integer + minimum: 0 + name: pricing_unit_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_PricingUnitGet_' /v0/storage/locations: get: tags: @@ -5166,6 +5225,14 @@ components: $ref: '#/components/schemas/PresignedLink' error: title: Error + Envelope_PricingUnitGet_: + title: Envelope[PricingUnitGet] + type: object + properties: + data: + $ref: '#/components/schemas/PricingUnitGet' + error: + title: Error Envelope_ProfileGet_: title: Envelope[ProfileGet] type: object @@ -5238,6 +5305,14 @@ components: $ref: '#/components/schemas/ServiceInputGet' error: title: Error + Envelope_ServicePricingPlanGet_: + title: Envelope[ServicePricingPlanGet] + type: object + properties: + data: + $ref: '#/components/schemas/ServicePricingPlanGet' + error: + title: Error Envelope_StatusDiagnosticsGet_: title: Envelope[StatusDiagnosticsGet] type: object @@ -7182,6 +7257,35 @@ components: minLength: 1 type: string format: uri + PricingPlanClassification: + title: PricingPlanClassification + enum: + - TIER + type: string + description: An enumeration. + PricingUnitGet: + title: PricingUnitGet + required: + - pricingUnitId + - unitName + - currentCostPerUnit + - default + type: object + properties: + pricingUnitId: + title: Pricingunitid + exclusiveMinimum: true + type: integer + minimum: 0 + unitName: + title: Unitname + type: string + currentCostPerUnit: + title: Currentcostperunit + type: number + default: + title: Default + type: boolean ProfileGet: title: ProfileGet required: @@ -8371,6 +8475,43 @@ components: unitLong: seconds unitShort: sec keyId: output_2 + ServicePricingPlanGet: + title: ServicePricingPlanGet + required: + - pricingPlanId + - displayName + - description + - classification + - createdAt + - pricingPlanKey + - pricingUnits + type: object + properties: + pricingPlanId: + title: Pricingplanid + exclusiveMinimum: true + type: integer + minimum: 0 + displayName: + title: Displayname + type: string + description: + title: Description + type: string + classification: + $ref: '#/components/schemas/PricingPlanClassification' + createdAt: + title: Createdat + type: string + format: date-time + pricingPlanKey: + title: Pricingplankey + type: string + pricingUnits: + title: Pricingunits + type: array + items: + $ref: '#/components/schemas/PricingUnitGet' ServiceRunGet: title: ServiceRunGet required: diff --git a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py b/services/web/server/src/simcore_service_webserver/catalog/_handlers.py index 69b5a67ec21..d1a32268b8a 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_handlers.py @@ -17,6 +17,7 @@ ServiceOutputKey, ServiceUpdate, ) +from models_library.api_schemas_webserver.resource_usage import ServicePricingPlanGet from models_library.services import ServiceKey, ServiceVersion from models_library.services_resources import ( ServiceResourcesDict, @@ -31,6 +32,7 @@ from .._meta import API_VTAG from ..login.decorators import login_required +from ..resource_usage.api import get_default_service_pricing_plan from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _api, client @@ -318,3 +320,25 @@ async def get_service_resources(request: Request): return await asyncio.get_event_loop().run_in_executor( None, envelope_json_response, data ) + + +@routes.get( + f"{VTAG}/catalog/services/{{service_key}}/{{service_version}}/pricing-plan", + name="get_service_pricing_plan", +) +@login_required +@permission_required("services.catalog.*") +async def get_service_pricing_plan(request: Request): + ctx = CatalogRequestContext.create(request) + path_params = parse_request_path_parameters_as(ServicePathParams, request) + + service_pricing_plan: ServicePricingPlanGet = ( + await get_default_service_pricing_plan( + app=request.app, + product_name=ctx.product_name, + service_key=path_params.service_key, + service_version=path_params.service_version, + ) + ) + + return envelope_json_response(service_pricing_plan) diff --git a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py index c0df92189c1..af995313cce 100644 --- a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py @@ -19,9 +19,7 @@ from ..db import plugin from ..director_v2 import api as director_v2_api from ..login.decorators import login_required -from ..resource_usage.resource_usage_tracker_client import ( - is_resource_usage_tracking_service_responsive, -) +from ..resource_usage._client import is_resource_usage_tracking_service_responsive from ..security.decorators import permission_required from ..storage import api as storage_api from ..utils import TaskInfoDict, get_task_info, get_tracemalloc_info diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/resource_usage_tracker_client.py b/services/web/server/src/simcore_service_webserver/resource_usage/_client.py similarity index 79% rename from services/web/server/src/simcore_service_webserver/resource_usage/resource_usage_tracker_client.py rename to services/web/server/src/simcore_service_webserver/resource_usage/_client.py index 939c0108c29..8b8e3cc4f14 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/resource_usage_tracker_client.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_client.py @@ -2,6 +2,7 @@ """ import logging +import urllib.parse from datetime import datetime from decimal import Decimal @@ -14,9 +15,14 @@ from models_library.api_schemas_resource_usage_tracker.credit_transactions import ( WalletTotalCredits, ) +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, + ServicePricingPlanGet, +) +from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.users import UserID from models_library.wallets import WalletID -from pydantic import NonNegativeInt +from pydantic import NonNegativeInt, parse_obj_as from servicelib.aiohttp.client_session import get_client_session from settings_library.resource_usage_tracker import ResourceUsageTrackerSettings from yarl import URL @@ -76,21 +82,45 @@ async def list_service_runs_by_user_and_product_and_wallet( return body -async def list_pricing_plans_by_product_and_service( +async def get_default_service_pricing_plan( app: web.Application, product_name: str, service_key: str, service_version: str -) -> dict: +) -> ServicePricingPlanGet: settings: ResourceUsageTrackerSettings = get_plugin_settings(app) - url = (URL(settings.api_base_url) / "pricing-plans").with_query( + url = URL( + f"{settings.api_base_url}/services/{urllib.parse.quote_plus(service_key)}/{service_version}/pricing-plan" + ).with_query( { "product_name": product_name, - "service_key": service_key, - "service_version": service_version, } ) with handle_client_exceptions(app) as session: async with session.get(url) as response: body: dict = await response.json() - return body + return parse_obj_as(ServicePricingPlanGet, body) + + +async def get_pricing_plan_unit( + app: web.Application, + product_name: str, + pricing_plan_id: PricingPlanId, + pricing_unit_id: PricingUnitId, +) -> PricingUnitGet: + settings: ResourceUsageTrackerSettings = get_plugin_settings(app) + url = ( + URL(settings.api_base_url) + / "pricing-plans" + / str(pricing_plan_id) + / "pricing-units" + / str(pricing_unit_id) + ).with_query( + { + "product_name": product_name, + } + ) + with handle_client_exceptions(app) as session: + async with session.get(url) as response: + body: dict = await response.json() + return parse_obj_as(PricingUnitGet, body) async def sum_total_available_credits_in_the_wallet( diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_api.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_api.py new file mode 100644 index 00000000000..5277d79a1e2 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_api.py @@ -0,0 +1,52 @@ +from aiohttp import web +from models_library.api_schemas_resource_usage_tracker import ( + pricing_plans as rut_api_schemas, +) +from models_library.api_schemas_webserver import resource_usage as webserver_api_schemas +from models_library.products import ProductName +from models_library.resource_tracker import PricingPlanId, PricingUnitId +from models_library.services import ServiceKey, ServiceVersion +from pydantic import parse_obj_as + +from . import _client as resource_tracker_client + + +async def get_default_service_pricing_plan( + app: web.Application, + product_name: ProductName, + service_key: ServiceKey, + service_version: ServiceVersion, +) -> webserver_api_schemas.ServicePricingPlanGet: + data: rut_api_schemas.ServicePricingPlanGet = ( + await resource_tracker_client.get_default_service_pricing_plan( + app=app, + product_name=product_name, + service_key=service_key, + service_version=service_version, + ) + ) + + return parse_obj_as(webserver_api_schemas.ServicePricingPlanGet, data) + + +async def get_pricing_plan_unit( + app: web.Application, + product_name: ProductName, + pricing_plan_id: PricingPlanId, + pricing_unit_id: PricingUnitId, +) -> webserver_api_schemas.PricingUnitGet: + data: rut_api_schemas.PricingUnitGet = ( + await resource_tracker_client.get_pricing_plan_unit( + app=app, + product_name=product_name, + pricing_plan_id=pricing_plan_id, + pricing_unit_id=pricing_unit_id, + ) + ) + + return webserver_api_schemas.PricingUnitGet( + pricing_unit_id=data.pricing_unit_id, + unit_name=data.unit_name, + current_cost_per_unit=data.current_cost_per_unit, + default=data.default, + ) diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py new file mode 100644 index 00000000000..0440e39c430 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py @@ -0,0 +1,77 @@ +import functools + +from aiohttp import web +from models_library.api_schemas_webserver.resource_usage import PricingUnitGet +from models_library.resource_tracker import PricingPlanId, PricingUnitId +from models_library.users import UserID +from pydantic import BaseModel, Extra, Field +from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as +from servicelib.aiohttp.typing_extension import Handler +from servicelib.request_keys import RQT_USERID_KEY + +from .._constants import RQ_PRODUCT_KEY +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 ..wallets.errors import WalletAccessForbiddenError +from . import _pricing_plans_api as api + +# +# API components/schemas +# + + +def _handle_resource_usage_exceptions(handler: Handler): + @functools.wraps(handler) + async def wrapper(request: web.Request) -> web.StreamResponse: + try: + return await handler(request) + + except WalletAccessForbiddenError as exc: + raise web.HTTPForbidden(reason=f"{exc}") from exc + + return wrapper + + +class _RequestContext(BaseModel): + user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[pydantic-alias] + product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[pydantic-alias] + + +# +# API handlers +# + +routes = web.RouteTableDef() + + +class _GetPricingPlanUnitPathParams(BaseModel): + pricing_plan_id: PricingPlanId + pricing_unit_id: PricingUnitId + + class Config: + extra = Extra.forbid + + +@routes.get( + f"/{VTAG}/pricing-plans/{{pricing_plan_id}}/pricing-units/{{pricing_unit_id}}", + name="get_pricing_plan_unit", +) +@login_required +@permission_required("resource-usage.read") +@_handle_resource_usage_exceptions +async def get_pricing_plan_unit(request: web.Request): + req_ctx = _RequestContext.parse_obj(request) + path_params = parse_request_path_parameters_as( + _GetPricingPlanUnitPathParams, request + ) + + pricing_unit_get: PricingUnitGet = await api.get_pricing_plan_unit( + app=request.app, + product_name=req_ctx.product_name, + pricing_plan_id=path_params.pricing_plan_id, + pricing_unit_id=path_params.pricing_unit_id, + ) + + return envelope_json_response(pricing_unit_get, web.HTTPOk) diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_api.py b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_api.py index 66b594a7785..0f17a0cb09f 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_api.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_api.py @@ -6,7 +6,7 @@ from pydantic import NonNegativeInt from ..wallets import api as wallet_api -from . import resource_usage_tracker_client as resource_tracker_client +from . import _client as resource_tracker_client async def list_usage_services( diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py index 7b9d580d425..8c97694c85e 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py @@ -46,7 +46,7 @@ class _RequestContext(BaseModel): product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[pydantic-alias] -class _ListServicesPathParams(BaseModel): +class _ListServicesResourceUsagesPathParams(BaseModel): limit: int = Field( default=DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, description="maximum number of items to return (pagination)", @@ -69,13 +69,15 @@ class Config: routes = web.RouteTableDef() -@routes.get(f"/{VTAG}/resource-usage/services", name="list_resource_usage_services") +@routes.get(f"/{VTAG}/services/-/resource-usages", name="list_resource_usage_services") @login_required @permission_required("resource-usage.read") @_handle_resource_usage_exceptions async def list_resource_usage_services(request: web.Request): req_ctx = _RequestContext.parse_obj(request) - query_params = parse_request_query_parameters_as(_ListServicesPathParams, request) + query_params = parse_request_query_parameters_as( + _ListServicesResourceUsagesPathParams, request + ) services: dict = await api.list_usage_services( app=request.app, diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/api.py b/services/web/server/src/simcore_service_webserver/resource_usage/api.py index dd054521a01..33283c6ca2e 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/api.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/api.py @@ -10,14 +10,15 @@ from models_library.users import UserID from models_library.wallets import WalletID -from . import resource_usage_tracker_client +from . import _client +from ._pricing_plans_api import get_default_service_pricing_plan async def get_wallet_total_available_credits( app: web.Application, product_name: ProductName, wallet_id: WalletID ) -> WalletTotalCredits: available_credits: WalletTotalCredits = ( - await resource_usage_tracker_client.sum_total_available_credits_in_the_wallet( + await _client.sum_total_available_credits_in_the_wallet( app, product_name, wallet_id ) ) @@ -35,7 +36,7 @@ async def add_credits_to_wallet( payment_id: PaymentID, created_at: datetime, ) -> None: - await resource_usage_tracker_client.add_credits_to_wallet( + await _client.add_credits_to_wallet( app=app, product_name=product_name, wallet_id=wallet_id, @@ -46,3 +47,6 @@ async def add_credits_to_wallet( payment_transaction_id=payment_id, created_at=created_at, ) + + +__all__ = ("get_default_service_pricing_plan",) diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/plugin.py b/services/web/server/src/simcore_service_webserver/resource_usage/plugin.py index 68e5e80d9c1..cafcb3755e0 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/plugin.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/plugin.py @@ -9,7 +9,7 @@ from ..rabbitmq import setup_rabbitmq from ..wallets.plugin import setup_wallets -from . import _service_runs_handlers +from . import _pricing_plans_handlers, _service_runs_handlers from ._observer import setup_resource_usage_observer_events _logger = logging.getLogger(__name__) @@ -30,3 +30,4 @@ def setup_resource_tracker(app: web.Application): setup_resource_usage_observer_events(app) app.router.add_routes(_service_runs_handlers.routes) + app.router.add_routes(_pricing_plans_handlers.routes) diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py new file mode 100644 index 00000000000..c486b0d90c3 --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_pricing_plans.py @@ -0,0 +1,79 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments +# pylint: disable=too-many-statements + + +import re + +import pytest +from aiohttp import web +from aiohttp.test_utils import TestClient +from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, + ServicePricingPlanGet, +) +from models_library.utils.fastapi_encoders import jsonable_encoder +from pydantic import parse_obj_as +from pytest_simcore.aioresponses_mocker import AioResponsesMock +from pytest_simcore.helpers.utils_assert import assert_status +from pytest_simcore.helpers.utils_login import UserInfoDict +from settings_library.resource_usage_tracker import ResourceUsageTrackerSettings +from simcore_service_webserver.db.models import UserRole +from simcore_service_webserver.resource_usage.settings import get_plugin_settings + + +@pytest.fixture +def mock_rut_api_responses( + client: TestClient, aioresponses_mocker: AioResponsesMock +) -> AioResponsesMock: + assert client.app + settings: ResourceUsageTrackerSettings = get_plugin_settings(client.app) + + pricing_unit_get = parse_obj_as( + PricingUnitGet, PricingUnitGet.Config.schema_extra["examples"][0] + ) + + service_pricing_plan_get = parse_obj_as( + ServicePricingPlanGet, + ServicePricingPlanGet.Config.schema_extra["examples"][0], + ) + + aioresponses_mocker.get( + re.compile(f"^{settings.api_base_url}/pricing-plans/+.+$"), + payload=jsonable_encoder(pricing_unit_get), + ) + aioresponses_mocker.get( + re.compile(f"^{settings.api_base_url}/services/+.+$"), + payload=jsonable_encoder(service_pricing_plan_get), + ) + + return aioresponses_mocker + + +@pytest.mark.parametrize("user_role", [(UserRole.USER)]) +async def test_list_service_usage( + client: TestClient, + logged_user: UserInfoDict, + mock_rut_api_responses, +): + # Get specific pricing plan unit + url = client.app.router["get_pricing_plan_unit"].url_for( + pricing_plan_id="1", pricing_unit_id="1" + ) + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, web.HTTPOk) + assert mock_rut_api_responses + assert len(data.keys()) == 4 + assert data["unitName"] == "SMALL" + + # Get default pricing plan for service + url = client.app.router["get_service_pricing_plan"].url_for( + service_key="simcore%2Fservices%2Fcomp%2Fitis%2Fsleeper", + service_version="1.0.16", + ) + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, web.HTTPOk) + assert data["pricingPlanKey"] == "pricing-plan-sleeper" + assert len(data["pricingUnits"]) == 1