Skip to content
23 changes: 23 additions & 0 deletions packages/models-library/src/models_library/licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .utils.enums import StrAutoEnum

LicensedItemID: TypeAlias = UUID
LicensedResourceID: TypeAlias = UUID


class LicensedResourceType(StrAutoEnum):
Expand Down Expand Up @@ -90,6 +91,28 @@ class LicensedItemUpdateDB(BaseModel):
trash: bool | None = None


class LicensedResourceDB(BaseModel):
licensed_resource_id: LicensedResourceID
display_name: str

licensed_resource_name: str
licensed_resource_type: LicensedResourceType
licensed_resource_data: dict[str, Any] | None

# states
created: datetime
modified: datetime
trashed: datetime | None

model_config = ConfigDict(from_attributes=True)


class LicensedResourceUpdateDB(BaseModel):
display_name: str | None = None
licensed_resource_name: str | None = None
trash: bool | None = None


class LicensedItem(BaseModel):
licensed_item_id: LicensedItemID
display_name: str
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""add licensed resources

Revision ID: 68777fdf9539
Revises: e71ea59858f4
Create Date: 2025-02-09 10:24:50.533653+00:00

"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "68777fdf9539"
down_revision = "e71ea59858f4"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"licensed_resources",
sa.Column(
"licensed_resource_id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("display_name", sa.String(), nullable=False),
sa.Column("licensed_resource_name", sa.String(), nullable=False),
sa.Column(
"licensed_resource_type",
sa.Enum("VIP_MODEL", name="licensedresourcetype"),
nullable=False,
),
sa.Column(
"licensed_resource_data",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
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.Column(
"trashed",
sa.DateTime(timezone=True),
nullable=True,
comment="The date and time when the licensed_resources was marked as trashed. Null if the licensed_resources has not been trashed [default].",
),
sa.PrimaryKeyConstraint("licensed_resource_id"),
sa.UniqueConstraint(
"licensed_resource_name",
"licensed_resource_type",
name="uq_licensed_resource_name_type2",
),
)
# ### end Alembic commands ###

# Migration of licensed resources from licensed_items table to new licensed_resources table
op.execute(
sa.DDL(
"""
INSERT INTO licensed_resources (display_name, licensed_resource_name, licensed_resource_type, licensed_resource_data, created, modified)
SELECT
display_name,
licensed_resource_name,
licensed_resource_type,
licensed_resource_data,
CURRENT_TIMESTAMP as created,
CURRENT_TIMESTAMP as modified
FROM licensed_items
"""
)
)


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("licensed_resources")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
""" resource_tracker_service_runs table
"""


import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from ._common import (
column_created_datetime,
column_modified_datetime,
column_trashed_datetime,
)
from .base import metadata
from .licensed_items import LicensedResourceType

licensed_resources = sa.Table(
"licensed_resources",
metadata,
sa.Column(
"licensed_resource_id",
postgresql.UUID(as_uuid=True),
nullable=False,
primary_key=True,
server_default=sa.text("gen_random_uuid()"),
),
sa.Column(
"display_name",
sa.String,
nullable=False,
doc="Display name for front-end",
),
sa.Column(
"licensed_resource_name",
sa.String,
nullable=False,
doc="Resource name identifier",
),
sa.Column(
"licensed_resource_type",
sa.Enum(LicensedResourceType),
nullable=False,
doc="Resource type, ex. VIP_MODEL",
),
sa.Column(
"licensed_resource_data",
postgresql.JSONB,
nullable=True,
doc="Resource metadata. Used for read-only purposes",
),
column_created_datetime(timezone=True),
column_modified_datetime(timezone=True),
column_trashed_datetime("licensed_resources"),
sa.UniqueConstraint(
"licensed_resource_name",
"licensed_resource_type",
name="uq_licensed_resource_name_type2",
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def sync_resources_with_licensed_items(
with log_context(
_logger, logging.INFO, "Registering %s", licensed_resource_name
), log_catch(_logger, reraise=False):
result = await _licensed_items_service.register_resource_as_licensed_item(
result = await _licensed_items_service.register_resource_as_licensed_resource(
app,
licensed_item_display_name=f"{vip_data.features.get('name', 'UNNAMED!!')} "
f"{vip_data.features.get('version', 'UNVERSIONED!!')}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from ..users.api import get_user
from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet
from ..wallets.errors import WalletNotEnoughCreditsError
from . import _licensed_items_repository
from . import _licensed_items_repository, _licensed_resources_repository
from ._common.models import LicensedItemsBodyParams
from .errors import LicensedItemNotFoundError, LicensedItemPricingPlanMatchError

Expand All @@ -51,7 +51,7 @@ class RegistrationResult(NamedTuple):
message: str | None


async def register_resource_as_licensed_item(
async def register_resource_as_licensed_resource(
app: web.Application,
*,
licensed_resource_name: str,
Expand Down Expand Up @@ -81,6 +81,22 @@ async def register_resource_as_licensed_item(
licensed_resource_name=licensed_resource_name,
licensed_resource_type=licensed_resource_type,
)
licensed_resource = (
await _licensed_resources_repository.get_by_resource_identifier(
app,
licensed_resource_name=licensed_resource_name,
licensed_resource_type=licensed_resource_type,
)
)
# NOTE: MD: This is temporaty, we are splitting the licensed_item and licensed_resource
assert (
licensed_resource.licensed_resource_name
== licensed_item.licensed_resource_name
) # nosec
assert (
licensed_resource.licensed_resource_type
== licensed_item.licensed_resource_type
) # nosec

if licensed_item.licensed_resource_data != new_licensed_resource_data:
ddiff = DeepDiff(
Expand Down Expand Up @@ -110,6 +126,22 @@ async def register_resource_as_licensed_item(
product_name=None,
pricing_plan_id=None,
)
licensed_resource = await _licensed_resources_repository.create_if_not_exists(
app,
display_name=licensed_item_display_name,
licensed_resource_name=licensed_resource_name,
licensed_resource_type=licensed_resource_type,
licensed_resource_data=new_licensed_resource_data,
)
# NOTE: MD: This is temporaty, we are splitting the licensed_item and licensed_resource
assert (
licensed_resource.licensed_resource_name
== licensed_item.licensed_resource_name
) # nosec
assert (
licensed_resource.licensed_resource_type
== licensed_item.licensed_resource_type
) # nosec

return RegistrationResult(
licensed_item,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
""" Database API
- Adds a layer to the postgres API with a focus on the projects comments
"""

import logging
from typing import Any

from aiohttp import web
from models_library.licenses import LicensedResourceDB, LicensedResourceType
from simcore_postgres_database.models.licensed_resources import licensed_resources
from simcore_postgres_database.utils_repos import (
get_columns_from_db_model,
pass_or_acquire_connection,
transaction_context,
)
from sqlalchemy import func
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.asyncio import AsyncConnection
from sqlalchemy.sql import select

from ..db.plugin import get_asyncpg_engine
from .errors import LicensedResourceNotFoundError

_logger = logging.getLogger(__name__)


_SELECTION_ARGS = get_columns_from_db_model(licensed_resources, LicensedResourceDB)


def _create_insert_query(
display_name: str,
licensed_resource_name: str,
licensed_resource_type: LicensedResourceType,
licensed_resource_data: dict[str, Any] | None,
):
return (
postgresql.insert(licensed_resources)
.values(
licensed_resource_name=licensed_resource_name,
licensed_resource_type=licensed_resource_type,
licensed_resource_data=licensed_resource_data,
display_name=display_name,
created=func.now(),
modified=func.now(),
)
.returning(*_SELECTION_ARGS)
)


async def create_if_not_exists(
app: web.Application,
connection: AsyncConnection | None = None,
*,
display_name: str,
licensed_resource_name: str,
licensed_resource_type: LicensedResourceType,
licensed_resource_data: dict[str, Any] | None = None,
) -> LicensedResourceDB:

insert_or_none_query = _create_insert_query(
display_name,
licensed_resource_name,
licensed_resource_type,
licensed_resource_data,
).on_conflict_do_nothing()

async with transaction_context(get_asyncpg_engine(app), connection) as conn:
result = await conn.execute(insert_or_none_query)
row = result.one_or_none()

if row is None:
select_query = select(*_SELECTION_ARGS).where(
(licensed_resources.c.licensed_resource_name == licensed_resource_name)
& (
licensed_resources.c.licensed_resource_type
== licensed_resource_type
)
)

result = await conn.execute(select_query)
row = result.one()

assert row is not None # nosec
return LicensedResourceDB.model_validate(row)


async def get_by_resource_identifier(
app: web.Application,
connection: AsyncConnection | None = None,
*,
licensed_resource_name: str,
licensed_resource_type: LicensedResourceType,
) -> LicensedResourceDB:
select_query = select(*_SELECTION_ARGS).where(
(licensed_resources.c.licensed_resource_name == licensed_resource_name)
& (licensed_resources.c.licensed_resource_type == licensed_resource_type)
)

async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn:
result = await conn.execute(select_query)
row = result.one_or_none()
if row is None:
raise LicensedResourceNotFoundError(
licensed_item_id="Unkown", # <-- NOTE: will be changed for licensed_resource_id
licensed_resource_name=licensed_resource_name,
licensed_resource_type=licensed_resource_type,
)
return LicensedResourceDB.model_validate(row)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ class LicensesValueError(WebServerBaseError, ValueError):


class LicensedItemNotFoundError(LicensesValueError):
msg_template = "License good {licensed_item_id} not found"
msg_template = "License item {licensed_item_id} not found"


class LicensedResourceNotFoundError(LicensesValueError):
msg_template = "License resource {licensed_resource_id} not found"


class LicensedItemPricingPlanMatchError(LicensesValueError):
Expand Down
Loading
Loading