Skip to content

Commit 7457535

Browse files
♻️ introduce licensed_resources (🗃️) (#7190)
1 parent 3bb98a6 commit 7457535

File tree

17 files changed

+317
-58
lines changed

17 files changed

+317
-58
lines changed

packages/models-library/src/models_library/licenses.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .utils.enums import StrAutoEnum
1414

1515
LicensedItemID: TypeAlias = UUID
16+
LicensedResourceID: TypeAlias = UUID
1617

1718

1819
class LicensedResourceType(StrAutoEnum):
@@ -90,6 +91,28 @@ class LicensedItemUpdateDB(BaseModel):
9091
trash: bool | None = None
9192

9293

94+
class LicensedResourceDB(BaseModel):
95+
licensed_resource_id: LicensedResourceID
96+
display_name: str
97+
98+
licensed_resource_name: str
99+
licensed_resource_type: LicensedResourceType
100+
licensed_resource_data: dict[str, Any] | None
101+
102+
# states
103+
created: datetime
104+
modified: datetime
105+
trashed: datetime | None
106+
107+
model_config = ConfigDict(from_attributes=True)
108+
109+
110+
class LicensedResourcePatchDB(BaseModel):
111+
display_name: str | None = None
112+
licensed_resource_name: str | None = None
113+
trash: bool | None = None
114+
115+
93116
class LicensedItem(BaseModel):
94117
licensed_item_id: LicensedItemID
95118
display_name: str
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""add licensed resources
2+
3+
Revision ID: 68777fdf9539
4+
Revises: 061607911a22
5+
Create Date: 2025-02-09 10:24:50.533653+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "68777fdf9539"
14+
down_revision = "061607911a22"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
"licensed_resources",
23+
sa.Column(
24+
"licensed_resource_id",
25+
postgresql.UUID(as_uuid=True),
26+
server_default=sa.text("gen_random_uuid()"),
27+
nullable=False,
28+
),
29+
sa.Column("display_name", sa.String(), nullable=False),
30+
sa.Column("licensed_resource_name", sa.String(), nullable=False),
31+
sa.Column(
32+
"licensed_resource_type",
33+
sa.Enum("VIP_MODEL", name="licensedresourcetype"),
34+
nullable=False,
35+
),
36+
sa.Column(
37+
"licensed_resource_data",
38+
postgresql.JSONB(astext_type=sa.Text()),
39+
nullable=True,
40+
),
41+
sa.Column(
42+
"created",
43+
sa.DateTime(timezone=True),
44+
server_default=sa.text("now()"),
45+
nullable=False,
46+
),
47+
sa.Column(
48+
"modified",
49+
sa.DateTime(timezone=True),
50+
server_default=sa.text("now()"),
51+
nullable=False,
52+
),
53+
sa.Column(
54+
"trashed",
55+
sa.DateTime(timezone=True),
56+
nullable=True,
57+
comment="The date and time when the licensed_resources was marked as trashed. Null if the licensed_resources has not been trashed [default].",
58+
),
59+
sa.PrimaryKeyConstraint("licensed_resource_id"),
60+
sa.UniqueConstraint(
61+
"licensed_resource_name",
62+
"licensed_resource_type",
63+
name="uq_licensed_resource_name_type2",
64+
),
65+
)
66+
# ### end Alembic commands ###
67+
68+
# Migration of licensed resources from licensed_items table to new licensed_resources table
69+
op.execute(
70+
sa.DDL(
71+
"""
72+
INSERT INTO licensed_resources (display_name, licensed_resource_name, licensed_resource_type, licensed_resource_data, created, modified)
73+
SELECT
74+
display_name,
75+
licensed_resource_name,
76+
licensed_resource_type,
77+
licensed_resource_data,
78+
CURRENT_TIMESTAMP as created,
79+
CURRENT_TIMESTAMP as modified
80+
FROM licensed_items
81+
"""
82+
)
83+
)
84+
85+
86+
def downgrade():
87+
# ### commands auto generated by Alembic - please adjust! ###
88+
op.drop_table("licensed_resources")
89+
# ### end Alembic commands ###
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
""" resource_tracker_service_runs table
2+
"""
3+
4+
5+
import sqlalchemy as sa
6+
from sqlalchemy.dialects import postgresql
7+
8+
from ._common import (
9+
column_created_datetime,
10+
column_modified_datetime,
11+
column_trashed_datetime,
12+
)
13+
from .base import metadata
14+
from .licensed_items import LicensedResourceType
15+
16+
licensed_resources = sa.Table(
17+
"licensed_resources",
18+
metadata,
19+
sa.Column(
20+
"licensed_resource_id",
21+
postgresql.UUID(as_uuid=True),
22+
nullable=False,
23+
primary_key=True,
24+
server_default=sa.text("gen_random_uuid()"),
25+
),
26+
sa.Column(
27+
"display_name",
28+
sa.String,
29+
nullable=False,
30+
doc="Display name for front-end",
31+
),
32+
sa.Column(
33+
"licensed_resource_name",
34+
sa.String,
35+
nullable=False,
36+
doc="Resource name identifier",
37+
),
38+
sa.Column(
39+
"licensed_resource_type",
40+
sa.Enum(LicensedResourceType),
41+
nullable=False,
42+
doc="Resource type, ex. VIP_MODEL",
43+
),
44+
sa.Column(
45+
"licensed_resource_data",
46+
postgresql.JSONB,
47+
nullable=True,
48+
doc="Resource metadata. Used for read-only purposes",
49+
),
50+
column_created_datetime(timezone=True),
51+
column_modified_datetime(timezone=True),
52+
column_trashed_datetime("licensed_resources"),
53+
sa.UniqueConstraint(
54+
"licensed_resource_name",
55+
"licensed_resource_type",
56+
name="uq_licensed_resource_name_type2",
57+
),
58+
)

services/web/server/src/simcore_service_webserver/folders/_folders_repository.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
""" Database API
2-
3-
- Adds a layer to the postgres API with a focus on the projects comments
4-
5-
"""
6-
71
import logging
82
from datetime import datetime
93
from typing import Final, cast

services/web/server/src/simcore_service_webserver/licenses/_itis_vip_syncer_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async def sync_resources_with_licensed_items(
4646
with log_context(
4747
_logger, logging.INFO, "Registering %s", licensed_resource_name
4848
), log_catch(_logger, reraise=False):
49-
result = await _licensed_items_service.register_resource_as_licensed_item(
49+
result = await _licensed_items_service.register_licensed_resource(
5050
app,
5151
licensed_item_display_name=f"{vip_data.features.get('name', 'UNNAMED!!')} "
5252
f"{vip_data.features.get('version', 'UNVERSIONED!!')}",

services/web/server/src/simcore_service_webserver/licenses/_licensed_items_repository.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
""" Database API
2-
3-
- Adds a layer to the postgres API with a focus on the projects comments
4-
5-
"""
6-
71
import logging
82
from typing import Any, Literal, cast
93

services/web/server/src/simcore_service_webserver/licenses/_licensed_items_service.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from ..users.api import get_user
3333
from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet
3434
from ..wallets.errors import WalletNotEnoughCreditsError
35-
from . import _licensed_items_repository
35+
from . import _licensed_items_repository, _licensed_resources_repository
3636
from ._common.models import LicensedItemsBodyParams
3737
from .errors import LicensedItemNotFoundError, LicensedItemPricingPlanMatchError
3838

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

5353

54-
async def register_resource_as_licensed_item(
54+
async def register_licensed_resource(
5555
app: web.Application,
5656
*,
5757
licensed_resource_name: str,
@@ -81,6 +81,22 @@ async def register_resource_as_licensed_item(
8181
licensed_resource_name=licensed_resource_name,
8282
licensed_resource_type=licensed_resource_type,
8383
)
84+
licensed_resource = (
85+
await _licensed_resources_repository.get_by_resource_identifier(
86+
app,
87+
licensed_resource_name=licensed_resource_name,
88+
licensed_resource_type=licensed_resource_type,
89+
)
90+
)
91+
# NOTE: MD: This is temporaty, we are splitting the licensed_item and licensed_resource
92+
assert (
93+
licensed_resource.licensed_resource_name
94+
== licensed_item.licensed_resource_name
95+
) # nosec
96+
assert (
97+
licensed_resource.licensed_resource_type
98+
== licensed_item.licensed_resource_type
99+
) # nosec
84100

85101
if licensed_item.licensed_resource_data != new_licensed_resource_data:
86102
ddiff = DeepDiff(
@@ -110,6 +126,22 @@ async def register_resource_as_licensed_item(
110126
product_name=None,
111127
pricing_plan_id=None,
112128
)
129+
licensed_resource = await _licensed_resources_repository.create_if_not_exists(
130+
app,
131+
display_name=licensed_item_display_name,
132+
licensed_resource_name=licensed_resource_name,
133+
licensed_resource_type=licensed_resource_type,
134+
licensed_resource_data=new_licensed_resource_data,
135+
)
136+
# NOTE: MD: This is temporaty, we are splitting the licensed_item and licensed_resource
137+
assert (
138+
licensed_resource.licensed_resource_name
139+
== licensed_item.licensed_resource_name
140+
) # nosec
141+
assert (
142+
licensed_resource.licensed_resource_type
143+
== licensed_item.licensed_resource_type
144+
) # nosec
113145

114146
return RegistrationResult(
115147
licensed_item,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import logging
2+
from typing import Any
3+
4+
from aiohttp import web
5+
from models_library.licenses import LicensedResourceDB, LicensedResourceType
6+
from simcore_postgres_database.models.licensed_resources import licensed_resources
7+
from simcore_postgres_database.utils_repos import (
8+
get_columns_from_db_model,
9+
pass_or_acquire_connection,
10+
transaction_context,
11+
)
12+
from sqlalchemy import func
13+
from sqlalchemy.dialects import postgresql
14+
from sqlalchemy.ext.asyncio import AsyncConnection
15+
from sqlalchemy.sql import select
16+
17+
from ..db.plugin import get_asyncpg_engine
18+
from .errors import LicensedResourceNotFoundError
19+
20+
_logger = logging.getLogger(__name__)
21+
22+
23+
_SELECTION_ARGS = get_columns_from_db_model(licensed_resources, LicensedResourceDB)
24+
25+
26+
def _create_insert_query(
27+
display_name: str,
28+
licensed_resource_name: str,
29+
licensed_resource_type: LicensedResourceType,
30+
licensed_resource_data: dict[str, Any] | None,
31+
):
32+
return (
33+
postgresql.insert(licensed_resources)
34+
.values(
35+
licensed_resource_name=licensed_resource_name,
36+
licensed_resource_type=licensed_resource_type,
37+
licensed_resource_data=licensed_resource_data,
38+
display_name=display_name,
39+
created=func.now(),
40+
modified=func.now(),
41+
)
42+
.returning(*_SELECTION_ARGS)
43+
)
44+
45+
46+
async def create_if_not_exists(
47+
app: web.Application,
48+
connection: AsyncConnection | None = None,
49+
*,
50+
display_name: str,
51+
licensed_resource_name: str,
52+
licensed_resource_type: LicensedResourceType,
53+
licensed_resource_data: dict[str, Any] | None = None,
54+
) -> LicensedResourceDB:
55+
56+
insert_or_none_query = _create_insert_query(
57+
display_name,
58+
licensed_resource_name,
59+
licensed_resource_type,
60+
licensed_resource_data,
61+
).on_conflict_do_nothing()
62+
63+
async with transaction_context(get_asyncpg_engine(app), connection) as conn:
64+
result = await conn.execute(insert_or_none_query)
65+
row = result.one_or_none()
66+
67+
if row is None:
68+
select_query = select(*_SELECTION_ARGS).where(
69+
(licensed_resources.c.licensed_resource_name == licensed_resource_name)
70+
& (
71+
licensed_resources.c.licensed_resource_type
72+
== licensed_resource_type
73+
)
74+
)
75+
76+
result = await conn.execute(select_query)
77+
row = result.one()
78+
79+
assert row is not None # nosec
80+
return LicensedResourceDB.model_validate(row)
81+
82+
83+
async def get_by_resource_identifier(
84+
app: web.Application,
85+
connection: AsyncConnection | None = None,
86+
*,
87+
licensed_resource_name: str,
88+
licensed_resource_type: LicensedResourceType,
89+
) -> LicensedResourceDB:
90+
select_query = select(*_SELECTION_ARGS).where(
91+
(licensed_resources.c.licensed_resource_name == licensed_resource_name)
92+
& (licensed_resources.c.licensed_resource_type == licensed_resource_type)
93+
)
94+
95+
async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn:
96+
result = await conn.execute(select_query)
97+
row = result.one_or_none()
98+
if row is None:
99+
raise LicensedResourceNotFoundError(
100+
licensed_item_id="Unkown", # <-- NOTE: will be changed for licensed_resource_id
101+
licensed_resource_name=licensed_resource_name,
102+
licensed_resource_type=licensed_resource_type,
103+
)
104+
return LicensedResourceDB.model_validate(row)

services/web/server/src/simcore_service_webserver/licenses/errors.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ class LicensesValueError(WebServerBaseError, ValueError):
66

77

88
class LicensedItemNotFoundError(LicensesValueError):
9-
msg_template = "License good {licensed_item_id} not found"
9+
msg_template = "License item {licensed_item_id} not found"
10+
11+
12+
class LicensedResourceNotFoundError(LicensesValueError):
13+
msg_template = "License resource {licensed_resource_id} not found"
1014

1115

1216
class LicensedItemPricingPlanMatchError(LicensesValueError):

0 commit comments

Comments
 (0)