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: 3fe27ff48f73
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 = "3fe27ff48f73"
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
@@ -1,9 +1,3 @@
""" Database API

- Adds a layer to the postgres API with a focus on the projects comments

"""

import logging
from datetime import datetime
from typing import Final, cast
Expand Down
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
@@ -1,9 +1,3 @@
""" Database API

- Adds a layer to the postgres API with a focus on the projects comments

"""

import logging
from typing import Any, Literal, cast

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,104 @@
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