Skip to content

Commit bc83eb0

Browse files
license goods DB layer
1 parent 00fce00 commit bc83eb0

File tree

16 files changed

+716
-5
lines changed

16 files changed

+716
-5
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from datetime import datetime
2+
from typing import NamedTuple
3+
4+
from models_library.license_goods import LicenseGoodID, LicenseResourceType
5+
from models_library.resource_tracker import PricingPlanId
6+
from pydantic import PositiveInt
7+
8+
from ._base import OutputSchema
9+
10+
11+
class LicenseGoodGet(OutputSchema):
12+
license_good_id: LicenseGoodID
13+
name: str
14+
license_resource_type: LicenseResourceType
15+
pricing_plan_id: PricingPlanId
16+
created_at: datetime
17+
modified_at: datetime
18+
19+
20+
class LicenseGoodGetPage(NamedTuple):
21+
items: list[LicenseGoodGet]
22+
total: PositiveInt
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from datetime import datetime
2+
from enum import auto
3+
from typing import TypeAlias
4+
5+
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
6+
7+
from .products import ProductName
8+
from .resource_tracker import PricingPlanId
9+
from .utils.enums import StrAutoEnum
10+
11+
LicenseGoodID: TypeAlias = PositiveInt
12+
13+
14+
class LicenseResourceType(StrAutoEnum):
15+
VIP_MODEL = auto()
16+
17+
18+
#
19+
# DB
20+
#
21+
22+
23+
class LicenseGoodDB(BaseModel):
24+
license_good_id: LicenseGoodID
25+
name: str
26+
license_resource_type: LicenseResourceType
27+
pricing_plan_id: PricingPlanId
28+
product_name: ProductName
29+
created: datetime = Field(
30+
...,
31+
description="Timestamp on creation",
32+
)
33+
modified: datetime = Field(
34+
...,
35+
description="Timestamp of last modification",
36+
)
37+
# ----
38+
model_config = ConfigDict(from_attributes=True)
39+
40+
41+
class LicenseGoodUpdateDB(BaseModel):
42+
name: str | None = None
43+
pricing_plan_id: PricingPlanId
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""add license db tables
2+
3+
Revision ID: ba974648bbdf
4+
Revises: e05bdc5b3c7b
5+
Create Date: 2024-12-04 08:37:55.511823+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "ba974648bbdf"
13+
down_revision = "e05bdc5b3c7b"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.create_table(
21+
"resource_tracker_license_purchases",
22+
sa.Column("license_purchase_id", sa.String(), nullable=False),
23+
sa.Column("product_name", sa.String(), nullable=False),
24+
sa.Column("license_good_id", sa.BigInteger(), nullable=False),
25+
sa.Column("wallet_id", sa.BigInteger(), nullable=False),
26+
sa.Column(
27+
"start_at",
28+
sa.DateTime(timezone=True),
29+
server_default=sa.text("now()"),
30+
nullable=False,
31+
),
32+
sa.Column(
33+
"expire_at",
34+
sa.DateTime(timezone=True),
35+
server_default=sa.text("now()"),
36+
nullable=False,
37+
),
38+
sa.Column("purchased_by_user", sa.BigInteger(), nullable=False),
39+
sa.Column(
40+
"purchased_at",
41+
sa.DateTime(timezone=True),
42+
server_default=sa.text("now()"),
43+
nullable=False,
44+
),
45+
sa.Column(
46+
"modified",
47+
sa.DateTime(timezone=True),
48+
server_default=sa.text("now()"),
49+
nullable=False,
50+
),
51+
sa.PrimaryKeyConstraint("license_purchase_id"),
52+
)
53+
op.create_table(
54+
"resource_tracker_license_checkouts",
55+
sa.Column("license_checkout_id", sa.String(), nullable=False),
56+
sa.Column("license_package_id", sa.String(), nullable=True),
57+
sa.Column("wallet_id", sa.BigInteger(), nullable=False),
58+
sa.Column("user_id", sa.BigInteger(), nullable=False),
59+
sa.Column("user_email", sa.String(), nullable=True),
60+
sa.Column("product_name", sa.String(), nullable=False),
61+
sa.Column("service_run_id", sa.String(), nullable=True),
62+
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
63+
sa.Column("stopped_at", sa.DateTime(timezone=True), nullable=True),
64+
sa.Column("num_of_seats", sa.SmallInteger(), nullable=False),
65+
sa.Column(
66+
"modified",
67+
sa.DateTime(timezone=True),
68+
server_default=sa.text("now()"),
69+
nullable=False,
70+
),
71+
sa.ForeignKeyConstraint(
72+
["product_name", "service_run_id"],
73+
[
74+
"resource_tracker_service_runs.product_name",
75+
"resource_tracker_service_runs.service_run_id",
76+
],
77+
name="resource_tracker_license_checkouts_service_run_id_fkey",
78+
onupdate="CASCADE",
79+
ondelete="RESTRICT",
80+
),
81+
sa.PrimaryKeyConstraint("license_checkout_id"),
82+
)
83+
op.create_index(
84+
op.f("ix_resource_tracker_license_checkouts_wallet_id"),
85+
"resource_tracker_license_checkouts",
86+
["wallet_id"],
87+
unique=False,
88+
)
89+
op.create_table(
90+
"license_goods",
91+
sa.Column("license_good_id", sa.String(), nullable=False),
92+
sa.Column("name", sa.String(), nullable=False),
93+
sa.Column(
94+
"license_resource_type",
95+
sa.Enum("VIP_MODEL", name="licenseresourcetype"),
96+
nullable=False,
97+
),
98+
sa.Column("pricing_plan_id", sa.BigInteger(), nullable=False),
99+
sa.Column("product_name", sa.String(), nullable=False),
100+
sa.Column(
101+
"created",
102+
sa.DateTime(timezone=True),
103+
server_default=sa.text("now()"),
104+
nullable=False,
105+
),
106+
sa.Column(
107+
"modified",
108+
sa.DateTime(timezone=True),
109+
server_default=sa.text("now()"),
110+
nullable=False,
111+
),
112+
sa.ForeignKeyConstraint(
113+
["pricing_plan_id"],
114+
["resource_tracker_pricing_plans.pricing_plan_id"],
115+
name="fk_resource_tracker_license_packages_pricing_plan_id",
116+
onupdate="CASCADE",
117+
ondelete="RESTRICT",
118+
),
119+
sa.ForeignKeyConstraint(
120+
["product_name"],
121+
["products.name"],
122+
name="fk_resource_tracker_license_packages_product_name",
123+
onupdate="CASCADE",
124+
ondelete="CASCADE",
125+
),
126+
sa.PrimaryKeyConstraint("license_good_id"),
127+
)
128+
# ### end Alembic commands ###
129+
130+
131+
def downgrade():
132+
# ### commands auto generated by Alembic - please adjust! ###
133+
op.drop_table("license_goods")
134+
op.drop_index(
135+
op.f("ix_resource_tracker_license_checkouts_wallet_id"),
136+
table_name="resource_tracker_license_checkouts",
137+
)
138+
op.drop_table("resource_tracker_license_checkouts")
139+
op.drop_table("resource_tracker_license_purchases")
140+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/license_packages.py renamed to packages/postgres-database/src/simcore_postgres_database/models/license_goods.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111

1212

1313
def _custom_id_generator():
14-
return f"lpa_{shortuuid.uuid()}"
14+
return f"lgo_{shortuuid.uuid()}"
1515

1616

1717
class LicenseResourceType(str, enum.Enum):
1818
VIP_MODEL = "VIP_MODEL"
1919

2020

21-
license_packages = sa.Table(
22-
"license_packages",
21+
license_goods = sa.Table(
22+
"license_goods",
2323
metadata,
2424
sa.Column(
25-
"license_package_id",
25+
"license_good_id",
2626
sa.String,
2727
nullable=False,
2828
primary_key=True,

packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_license_purchases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _custom_id_generator():
3030
doc="Product name",
3131
),
3232
sa.Column(
33-
"license_package_id",
33+
"license_good_id",
3434
sa.BigInteger,
3535
nullable=False,
3636
),

packages/postgres-database/src/simcore_postgres_database/utils_repos.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
async def pass_or_acquire_connection(
1212
engine: AsyncEngine, connection: AsyncConnection | None = None
1313
) -> AsyncIterator[AsyncConnection]:
14+
"""
15+
When to use: For READ operations only!
16+
"""
1417
# NOTE: When connection is passed, the engine is actually not needed
1518
# NOTE: Creator is responsible of closing connection
1619
is_connection_created = connection is None
@@ -30,6 +33,9 @@ async def pass_or_acquire_connection(
3033
async def transaction_context(
3134
engine: AsyncEngine, connection: AsyncConnection | None = None
3235
):
36+
"""
37+
When to use: For WRITE operations only!
38+
"""
3339
async with pass_or_acquire_connection(engine, connection) as conn:
3440
if conn.in_transaction():
3541
async with conn.begin_nested(): # inner transaction (savepoint)

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

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
3+
from servicelib.aiohttp import status
4+
5+
from ..exception_handling import (
6+
ExceptionToHttpErrorMap,
7+
HttpErrorInfo,
8+
exception_handling_decorator,
9+
to_exceptions_handlers_map,
10+
)
11+
from ..projects.exceptions import ProjectRunningConflictError, ProjectStoppingError
12+
from .errors import (
13+
WorkspaceAccessForbiddenError,
14+
WorkspaceGroupNotFoundError,
15+
WorkspaceNotFoundError,
16+
)
17+
18+
_logger = logging.getLogger(__name__)
19+
20+
21+
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
22+
WorkspaceGroupNotFoundError: HttpErrorInfo(
23+
status.HTTP_404_NOT_FOUND,
24+
"Workspace {workspace_id} group {group_id} not found.",
25+
),
26+
WorkspaceAccessForbiddenError: HttpErrorInfo(
27+
status.HTTP_403_FORBIDDEN,
28+
"Does not have access to this workspace",
29+
),
30+
WorkspaceNotFoundError: HttpErrorInfo(
31+
status.HTTP_404_NOT_FOUND,
32+
"Workspace not found. {reason}",
33+
),
34+
# Trashing
35+
ProjectRunningConflictError: HttpErrorInfo(
36+
status.HTTP_409_CONFLICT,
37+
"One or more studies in this workspace are in use and cannot be trashed. Please stop all services first and try again",
38+
),
39+
ProjectStoppingError: HttpErrorInfo(
40+
status.HTTP_503_SERVICE_UNAVAILABLE,
41+
"Something went wrong while stopping running services in studies within this workspace before trashing. Aborting trash.",
42+
),
43+
}
44+
45+
46+
handle_plugin_requests_exceptions = exception_handling_decorator(
47+
to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP)
48+
)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# pylint: disable=unused-argument
2+
3+
import logging
4+
5+
import _license_goods_db
6+
from aiohttp import web
7+
from models_library.api_schemas_webserver.license_goods import (
8+
LicenseGoodGet,
9+
LicenseGoodGetPage,
10+
)
11+
from models_library.license_goods import LicenseGoodID
12+
from models_library.products import ProductName
13+
from models_library.rest_ordering import OrderBy
14+
from models_library.users import UserID
15+
from pydantic import NonNegativeInt
16+
17+
from ._models import LicenseGoodsBodyParams
18+
19+
_logger = logging.getLogger(__name__)
20+
21+
22+
async def get_license_good(
23+
app: web.Application,
24+
*,
25+
license_good_id: LicenseGoodID,
26+
product_name: ProductName,
27+
) -> LicenseGoodGet:
28+
29+
license_good_db = await _license_goods_db.get(
30+
app, license_good_id=license_good_id, product_name=product_name
31+
)
32+
return LicenseGoodGet.model_construct(**license_good_db.model_dump())
33+
34+
35+
async def list_license_goods(
36+
app: web.Application,
37+
*,
38+
product_name: ProductName,
39+
offset: NonNegativeInt,
40+
limit: int,
41+
order_by: OrderBy,
42+
) -> LicenseGoodGetPage:
43+
total_count, license_good_db_list = await _license_goods_db.list_(
44+
app, product_name=product_name, offset=offset, limit=limit, order_by=order_by
45+
)
46+
return LicenseGoodGetPage(
47+
items=[
48+
LicenseGoodGet.model_construct(**license_good_db.model_dump())
49+
for license_good_db in license_good_db_list
50+
],
51+
total=total_count,
52+
)
53+
54+
55+
async def purchase_license_good(
56+
app: web.Application,
57+
*,
58+
product_name: ProductName,
59+
user_id: UserID,
60+
license_good_id: LicenseGoodID,
61+
body_params: LicenseGoodsBodyParams,
62+
) -> None:
63+
raise NotImplementedError

0 commit comments

Comments
 (0)