Skip to content

Commit e060492

Browse files
implement rut part of checkout/release license
1 parent abccfab commit e060492

File tree

9 files changed

+589
-1
lines changed

9 files changed

+589
-1
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from datetime import datetime
2+
from typing import NamedTuple, TypeAlias
3+
from uuid import UUID
4+
5+
from models_library.licensed_items import LicensedItemID
6+
from models_library.products import ProductName
7+
from models_library.resource_tracker import ServiceRunId
8+
from models_library.resource_tracker_licensed_items_usages import LicensedItemUsageID
9+
from models_library.users import UserID
10+
from models_library.wallets import WalletID
11+
from pydantic import BaseModel, ConfigDict, PositiveInt
12+
13+
LicenseCheckoutID: TypeAlias = UUID
14+
15+
16+
class LicensedItemUsageGet(BaseModel):
17+
licensed_item_usage_id: LicensedItemUsageID
18+
licensed_item_id: LicensedItemID
19+
wallet_id: WalletID
20+
user_id: UserID
21+
product_name: ProductName
22+
service_run_id: ServiceRunId
23+
start_at: datetime
24+
stopped_at: datetime | None
25+
num_of_seats: int
26+
27+
model_config = ConfigDict(
28+
json_schema_extra={
29+
"examples": [
30+
{
31+
"licensed_item_usage_id": "beb16d18-d57d-44aa-a638-9727fa4a72ef",
32+
"licensed_item_id": "303942ef-6d31-4ba8-afbe-dbb1fce2a953",
33+
"wallet_id": 1,
34+
"user_id": 1,
35+
"product_name": "osparc",
36+
"service_run_id": "run_1",
37+
"start_at": "2023-01-11 13:11:47.293595",
38+
"stopped_at": "2023-01-11 13:11:47.293595",
39+
"num_of_seats": 1,
40+
}
41+
]
42+
}
43+
)
44+
45+
46+
class LicensedItemsUsagesPage(NamedTuple):
47+
items: list[LicensedItemUsageGet]
48+
total: PositiveInt
49+
50+
51+
class LicenseItemCheckoutGet(BaseModel):
52+
checkout_id: LicenseCheckoutID # This is a licensed_item_usage_id generated in the `resource_tracker_licensed_items_usages` table
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import TypeAlias
2+
from uuid import UUID
3+
4+
from models_library.api_schemas_resource_usage_tracker.licensed_items_usages import (
5+
LicenseCheckoutID,
6+
)
7+
from models_library.products import ProductName
8+
from pydantic import BaseModel, ConfigDict
9+
10+
LicensedItemUsageID: TypeAlias = UUID
11+
12+
13+
class LicenseCheckoutCreate(BaseModel):
14+
checkout_id: LicenseCheckoutID
15+
product_name: ProductName
16+
17+
model_config = ConfigDict(from_attributes=True)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
),
2020
sa.Column(
2121
"licensed_item_id",
22-
sa.String,
22+
UUID(as_uuid=True),
2323
nullable=True,
2424
),
2525
sa.Column(

services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/exceptions/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from models_library.resource_tracker_licensed_items_purchases import (
33
LicensedItemPurchaseID,
44
)
5+
from models_library.resource_tracker_licensed_items_usages import LicensedItemUsageID
56

67

78
class ResourceUsageTrackerBaseError(OsparcErrorMixin, Exception):
@@ -75,3 +76,7 @@ class PricingPlanNotFoundForServiceError(RutNotFoundError):
7576

7677
class LicensedItemPurchaseNotFoundError(RutNotFoundError):
7778
licensed_item_purchase_id: LicensedItemPurchaseID
79+
80+
81+
class LicensedItemUsageNotFoundError(RutNotFoundError):
82+
licensed_item_usage_id: LicensedItemUsageID
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from datetime import datetime
2+
3+
from models_library.licensed_items import LicensedItemID
4+
from models_library.products import ProductName
5+
from models_library.resource_tracker import ServiceRunId
6+
from models_library.resource_tracker_licensed_items_usages import LicensedItemUsageID
7+
from models_library.users import UserID
8+
from models_library.wallets import WalletID
9+
from pydantic import BaseModel, ConfigDict
10+
11+
12+
class LicensedItemUsageDB(BaseModel):
13+
licensed_item_usage_id: LicensedItemUsageID
14+
licensed_item_id: LicensedItemID
15+
wallet_id: WalletID
16+
user_id: UserID
17+
user_email: str
18+
product_name: ProductName
19+
service_run_id: ServiceRunId
20+
start_at: datetime
21+
stopped_at: datetime | None
22+
num_of_seats: int
23+
24+
model_config = ConfigDict(from_attributes=True)
25+
26+
27+
class CreateLicensedItemUsageDB(BaseModel):
28+
licensed_item_id: LicensedItemID
29+
wallet_id: WalletID
30+
user_id: UserID
31+
user_email: str
32+
product_name: ProductName
33+
service_run_id: ServiceRunId
34+
start_at: datetime
35+
num_of_seats: int
36+
37+
model_config = ConfigDict(from_attributes=True)
38+
39+
40+
class UpdateLicensedItemUsageDB(BaseModel):
41+
stopped_at: datetime
42+
43+
model_config = ConfigDict(from_attributes=True)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from datetime import UTC, datetime
2+
from typing import Annotated
3+
4+
from fastapi import Depends
5+
from models_library.api_schemas_resource_usage_tracker.licensed_items_usages import (
6+
LicensedItemsUsagesPage,
7+
LicensedItemUsageGet,
8+
LicenseItemCheckoutGet,
9+
)
10+
from models_library.licensed_items import LicensedItemID
11+
from models_library.products import ProductName
12+
from models_library.resource_tracker import ServiceRunId, ServiceRunStatus
13+
from models_library.resource_tracker_licensed_items_usages import (
14+
LicenseCheckoutCreate,
15+
LicensedItemUsageID,
16+
)
17+
from models_library.rest_ordering import OrderBy
18+
from models_library.users import UserID
19+
from models_library.wallets import WalletID
20+
from sqlalchemy.ext.asyncio import AsyncEngine
21+
22+
from ..api.rest.dependencies import get_resource_tracker_db_engine
23+
from ..models.licensed_items_usages import (
24+
CreateLicensedItemUsageDB,
25+
LicensedItemUsageDB,
26+
)
27+
from .modules.db import (
28+
licensed_items_purchases_db,
29+
licensed_items_usages_db,
30+
service_runs_db,
31+
)
32+
33+
34+
async def list_licensed_items_purchases(
35+
db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)],
36+
*,
37+
product_name: ProductName,
38+
filter_wallet_id: WalletID,
39+
offset: int = 0,
40+
limit: int = 20,
41+
order_by: OrderBy,
42+
) -> LicensedItemsUsagesPage:
43+
total, licensed_items_usages_list_db = await licensed_items_usages_db.list_(
44+
db_engine,
45+
product_name=product_name,
46+
filter_wallet_id=filter_wallet_id,
47+
offset=offset,
48+
limit=limit,
49+
order_by=order_by,
50+
)
51+
return LicensedItemsUsagesPage(
52+
total=total,
53+
items=[
54+
LicensedItemUsageGet(
55+
licensed_item_usage_id=licensed_item_usage_db.licensed_item_usage_id,
56+
licensed_item_id=licensed_item_usage_db.licensed_item_id,
57+
wallet_id=licensed_item_usage_db.wallet_id,
58+
user_id=licensed_item_usage_db.user_id,
59+
product_name=licensed_item_usage_db.product_name,
60+
service_run_id=licensed_item_usage_db.service_run_id,
61+
start_at=licensed_item_usage_db.start_at,
62+
stopped_at=licensed_item_usage_db.stopped_at,
63+
num_of_seats=licensed_item_usage_db.num_of_seats,
64+
)
65+
for licensed_item_usage_db in licensed_items_usages_list_db
66+
],
67+
)
68+
69+
70+
async def get_licensed_item_usage(
71+
db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)],
72+
*,
73+
product_name: ProductName,
74+
licensed_item_usage_id: LicensedItemUsageID,
75+
) -> LicensedItemUsageGet:
76+
licensed_item_usage_db: LicensedItemUsageDB = await licensed_items_usages_db.get(
77+
db_engine,
78+
product_name=product_name,
79+
licensed_item_usage_id=licensed_item_usage_id,
80+
)
81+
82+
return LicensedItemUsageGet(
83+
licensed_item_usage_id=licensed_item_usage_db.licensed_item_usage_id,
84+
licensed_item_id=licensed_item_usage_db.licensed_item_id,
85+
wallet_id=licensed_item_usage_db.wallet_id,
86+
user_id=licensed_item_usage_db.user_id,
87+
product_name=licensed_item_usage_db.product_name,
88+
service_run_id=licensed_item_usage_db.service_run_id,
89+
start_at=licensed_item_usage_db.start_at,
90+
stopped_at=licensed_item_usage_db.stopped_at,
91+
num_of_seats=licensed_item_usage_db.num_of_seats,
92+
)
93+
94+
95+
async def checkout_licensed_item(
96+
db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)],
97+
*,
98+
licensed_item_id: LicensedItemID,
99+
wallet_id: WalletID,
100+
product_name: ProductName,
101+
num_of_seats: int,
102+
service_run_id: ServiceRunId,
103+
user_id: UserID,
104+
user_email: str,
105+
) -> LicenseItemCheckoutGet:
106+
107+
_active_purchased_seats: int = await licensed_items_purchases_db.get_active_purchased_seats_for_item_and_wallet(
108+
db_engine,
109+
licensed_item_id=licensed_item_id,
110+
wallet_id=wallet_id,
111+
product_name=product_name,
112+
)
113+
114+
_currently_used_seats = (
115+
await licensed_items_usages_db.get_currently_used_seats_for_item_and_wallet(
116+
db_engine,
117+
licensed_item_id=licensed_item_id,
118+
wallet_id=wallet_id,
119+
product_name=product_name,
120+
)
121+
)
122+
123+
available_seats = _active_purchased_seats - _currently_used_seats
124+
if available_seats <= 0:
125+
raise ValueError("Not enough available seats")
126+
127+
if available_seats - num_of_seats < 0:
128+
raise ValueError("Can not checkout num of seats, not enough available")
129+
130+
# Check if the service run ID is currently running
131+
service_run = await service_runs_db.get_service_run_by_id(
132+
db_engine, service_run_id=service_run_id
133+
)
134+
if (
135+
service_run is None
136+
or service_run.service_run_status != ServiceRunStatus.RUNNING
137+
):
138+
raise ValueError("This should not happen")
139+
140+
_create_item_usage = CreateLicensedItemUsageDB(
141+
licensed_item_id=licensed_item_id,
142+
wallet_id=wallet_id,
143+
user_id=user_id,
144+
user_email=user_email,
145+
product_name=product_name,
146+
service_run_id=service_run_id,
147+
start_at=datetime.now(tz=UTC),
148+
num_of_seats=num_of_seats,
149+
)
150+
license_item_usage_db = await licensed_items_usages_db.create(
151+
db_engine, data=_create_item_usage
152+
)
153+
154+
# Return checkout ID
155+
return LicenseItemCheckoutGet(
156+
checkout_id=license_item_usage_db.licensed_item_usage_id
157+
)
158+
159+
160+
async def release_licensed_item(
161+
db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)],
162+
*,
163+
data: LicenseCheckoutCreate,
164+
) -> LicensedItemUsageGet:
165+
166+
licensed_item_usage_db: LicensedItemUsageDB = await licensed_items_usages_db.update(
167+
db_engine,
168+
licensed_item_usage_id=data.checkout_id,
169+
product_name=data.product_name,
170+
stopped_at=datetime.now(tz=UTC),
171+
)
172+
173+
return LicensedItemUsageGet(
174+
licensed_item_usage_id=licensed_item_usage_db.licensed_item_usage_id,
175+
licensed_item_id=licensed_item_usage_db.licensed_item_id,
176+
wallet_id=licensed_item_usage_db.wallet_id,
177+
user_id=licensed_item_usage_db.user_id,
178+
product_name=licensed_item_usage_db.product_name,
179+
service_run_id=licensed_item_usage_db.service_run_id,
180+
start_at=licensed_item_usage_db.start_at,
181+
stopped_at=licensed_item_usage_db.stopped_at,
182+
num_of_seats=licensed_item_usage_db.num_of_seats,
183+
)

services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/licensed_items_purchases_db.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from datetime import UTC, datetime
12
from typing import cast
23

34
import sqlalchemy as sa
5+
from models_library.licensed_items import LicensedItemID
46
from models_library.products import ProductName
57
from models_library.resource_tracker_licensed_items_purchases import (
68
LicensedItemPurchaseID,
@@ -150,3 +152,37 @@ async def get(
150152
licensed_item_purchase_id=licensed_item_purchase_id
151153
)
152154
return LicensedItemsPurchasesDB.model_validate(row)
155+
156+
157+
async def get_active_purchased_seats_for_item_and_wallet(
158+
engine: AsyncEngine,
159+
connection: AsyncConnection | None = None,
160+
*,
161+
licensed_item_id: LicensedItemID,
162+
wallet_id: WalletID,
163+
product_name: ProductName,
164+
) -> int:
165+
"""
166+
Exclude expired seats
167+
"""
168+
_current_time = datetime.now(tz=UTC)
169+
170+
sum_stmt = sa.select(
171+
sa.func.sum(resource_tracker_licensed_items_purchases.c.num_of_seats)
172+
).where(
173+
(resource_tracker_licensed_items_purchases.c.wallet_id == wallet_id)
174+
& (
175+
resource_tracker_licensed_items_purchases.c.licensed_item_id
176+
== licensed_item_id
177+
)
178+
& (resource_tracker_licensed_items_purchases.c.product_name == product_name)
179+
& (resource_tracker_licensed_items_purchases.c.start_at <= _current_time)
180+
& (resource_tracker_licensed_items_purchases.c.expire_at >= _current_time)
181+
)
182+
183+
async with pass_or_acquire_connection(engine, connection) as conn:
184+
result = await conn.execute(sum_stmt)
185+
row = result.first()
186+
if row is None or row[0] is None:
187+
return 0
188+
return row[0]

0 commit comments

Comments
 (0)