Skip to content

Commit ef4a7e8

Browse files
✨ api for osparc credits usage aggregation (#6145)
1 parent da32561 commit ef4a7e8

File tree

13 files changed

+746
-11
lines changed

13 files changed

+746
-11
lines changed

api/specs/web-server/_resource_usage.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
UpdatePricingUnitBodyParams,
2727
)
2828
from models_library.generics import Envelope
29-
from models_library.resource_tracker import PricingPlanId, PricingUnitId
29+
from models_library.resource_tracker import (
30+
PricingPlanId,
31+
PricingUnitId,
32+
ServicesAggregatedUsagesTimePeriod,
33+
ServicesAggregatedUsagesType,
34+
)
3035
from models_library.rest_pagination import DEFAULT_NUMBER_OF_ITEMS_PER_PAGE
3136
from models_library.wallets import WalletID
3237
from pydantic import Json, NonNegativeInt
@@ -40,6 +45,7 @@
4045
)
4146
from simcore_service_webserver.resource_usage._service_runs_handlers import (
4247
ORDER_BY_DESCRIPTION,
48+
_ListServicesAggregatedUsagesQueryParams,
4349
_ListServicesResourceUsagesQueryParams,
4450
_ListServicesResourceUsagesQueryParamsWithPagination,
4551
)
@@ -85,6 +91,27 @@ async def list_resource_usage_services(
8591
)
8692

8793

94+
@router.get(
95+
"/services/-/aggregated-usages",
96+
response_model=Envelope[list[ServiceRunGet]],
97+
summary="Used credits based on aggregate by type, currently supported `services`. (user and product are taken from context, optionally wallet_id parameter might be provided).",
98+
tags=["usage"],
99+
)
100+
async def list_osparc_credits_aggregated_usages(
101+
aggregated_by: ServicesAggregatedUsagesType,
102+
time_period: ServicesAggregatedUsagesTimePeriod,
103+
wallet_id: Annotated[WalletID, Query],
104+
limit: int = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
105+
offset: NonNegativeInt = 0,
106+
):
107+
...
108+
109+
110+
assert_handler_signature_against_model(
111+
list_osparc_credits_aggregated_usages, _ListServicesAggregatedUsagesQueryParams
112+
)
113+
114+
88115
@router.get(
89116
"/services/-/usage-report",
90117
status_code=status.HTTP_302_FOUND,

packages/models-library/src/models_library/api_schemas_resource_usage_tracker/service_runs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ class ServiceRunGet(BaseModel):
3838
class ServiceRunPage(NamedTuple):
3939
items: list[ServiceRunGet]
4040
total: PositiveInt
41+
42+
43+
class OsparcCreditsAggregatedByServiceGet(BaseModel):
44+
osparc_credits: Decimal
45+
service_key: ServiceKey
46+
47+
48+
class OsparcCreditsAggregatedUsagesPage(NamedTuple):
49+
items: list[OsparcCreditsAggregatedByServiceGet]
50+
total: PositiveInt

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from datetime import datetime, timezone
33
from decimal import Decimal
4-
from enum import auto
4+
from enum import IntEnum, auto
55
from typing import Any, ClassVar, NamedTuple, TypeAlias
66

77
from pydantic import (
@@ -283,3 +283,13 @@ class Config:
283283
},
284284
]
285285
}
286+
287+
288+
class ServicesAggregatedUsagesType(StrAutoEnum):
289+
services = "services"
290+
291+
292+
class ServicesAggregatedUsagesTimePeriod(IntEnum):
293+
ONE_DAY = 1
294+
ONE_WEEK = 7
295+
ONE_MONTH = 30

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/service_runs.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
RESOURCE_USAGE_TRACKER_RPC_NAMESPACE,
66
)
77
from models_library.api_schemas_resource_usage_tracker.service_runs import (
8+
OsparcCreditsAggregatedUsagesPage,
89
ServiceRunPage,
910
)
1011
from models_library.products import ProductName
1112
from models_library.rabbitmq_basic_types import RPCMethodName
12-
from models_library.resource_tracker import ServiceResourceUsagesFilters
13+
from models_library.resource_tracker import (
14+
ServiceResourceUsagesFilters,
15+
ServicesAggregatedUsagesTimePeriod,
16+
ServicesAggregatedUsagesType,
17+
)
1318
from models_library.rest_ordering import OrderBy
1419
from models_library.users import UserID
1520
from models_library.wallets import WalletID
@@ -35,7 +40,7 @@ async def get_service_run_page(
3540
wallet_id: WalletID | None = None,
3641
access_all_wallet_usage: bool = False,
3742
order_by: OrderBy | None = None,
38-
filters: ServiceResourceUsagesFilters | None = None
43+
filters: ServiceResourceUsagesFilters | None = None,
3944
) -> ServiceRunPage:
4045
result = await rabbitmq_rpc_client.request(
4146
RESOURCE_USAGE_TRACKER_RPC_NAMESPACE,
@@ -54,6 +59,36 @@ async def get_service_run_page(
5459
return result
5560

5661

62+
@log_decorator(_logger, level=logging.DEBUG)
63+
async def get_osparc_credits_aggregated_usages_page(
64+
rabbitmq_rpc_client: RabbitMQRPCClient,
65+
*,
66+
user_id: UserID,
67+
product_name: ProductName,
68+
aggregated_by: ServicesAggregatedUsagesType,
69+
time_period: ServicesAggregatedUsagesTimePeriod,
70+
limit: int = 20,
71+
offset: int = 0,
72+
wallet_id: WalletID,
73+
access_all_wallet_usage: bool = False,
74+
) -> OsparcCreditsAggregatedUsagesPage:
75+
result = await rabbitmq_rpc_client.request(
76+
RESOURCE_USAGE_TRACKER_RPC_NAMESPACE,
77+
parse_obj_as(RPCMethodName, "get_osparc_credits_aggregated_usages_page"),
78+
user_id=user_id,
79+
product_name=product_name,
80+
limit=limit,
81+
offset=offset,
82+
wallet_id=wallet_id,
83+
access_all_wallet_usage=access_all_wallet_usage,
84+
aggregated_by=aggregated_by,
85+
time_period=time_period,
86+
timeout_s=60,
87+
)
88+
assert isinstance(result, OsparcCreditsAggregatedUsagesPage) # nosec
89+
return result
90+
91+
5792
@log_decorator(_logger, level=logging.DEBUG)
5893
async def export_service_runs(
5994
rabbitmq_rpc_client: RabbitMQRPCClient,
@@ -63,7 +98,7 @@ async def export_service_runs(
6398
wallet_id: WalletID | None = None,
6499
access_all_wallet_usage: bool = False,
65100
order_by: OrderBy | None = None,
66-
filters: ServiceResourceUsagesFilters | None = None
101+
filters: ServiceResourceUsagesFilters | None = None,
67102
) -> AnyUrl:
68103
result: AnyUrl = await rabbitmq_rpc_client.request(
69104
RESOURCE_USAGE_TRACKER_RPC_NAMESPACE,

services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_resource_tracker.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
PricingUnitGet,
66
)
77
from models_library.api_schemas_resource_usage_tracker.service_runs import (
8+
OsparcCreditsAggregatedUsagesPage,
89
ServiceRunPage,
910
)
1011
from models_library.products import ProductName
@@ -16,6 +17,8 @@
1617
PricingUnitWithCostCreate,
1718
PricingUnitWithCostUpdate,
1819
ServiceResourceUsagesFilters,
20+
ServicesAggregatedUsagesTimePeriod,
21+
ServicesAggregatedUsagesType,
1922
)
2023
from models_library.rest_ordering import OrderBy
2124
from models_library.services import ServiceKey, ServiceVersion
@@ -95,6 +98,32 @@ async def export_service_runs(
9598
)
9699

97100

101+
@router.expose(reraise_if_error_type=(CustomResourceUsageTrackerError,))
102+
async def get_osparc_credits_aggregated_usages_page(
103+
app: FastAPI,
104+
*,
105+
user_id: UserID,
106+
product_name: ProductName,
107+
aggregated_by: ServicesAggregatedUsagesType,
108+
time_period: ServicesAggregatedUsagesTimePeriod,
109+
limit: int = 20,
110+
offset: int = 0,
111+
wallet_id: WalletID,
112+
access_all_wallet_usage: bool = False,
113+
) -> OsparcCreditsAggregatedUsagesPage:
114+
return await service_runs.get_osparc_credits_aggregated_usages_page(
115+
user_id=user_id,
116+
product_name=product_name,
117+
resource_tracker_repo=ResourceTrackerRepository(db_engine=app.state.engine),
118+
aggregated_by=aggregated_by,
119+
time_period=time_period,
120+
limit=limit,
121+
offset=offset,
122+
wallet_id=wallet_id,
123+
access_all_wallet_usage=access_all_wallet_usage,
124+
)
125+
126+
98127
## Pricing plans
99128

100129

services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/resource_tracker_service_runs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ class Config:
106106
orm_mode = True
107107

108108

109+
class OsparcCreditsAggregatedByServiceKeyDB(BaseModel):
110+
osparc_credits: Decimal
111+
service_key: ServiceKey
112+
113+
class Config:
114+
orm_mode = True
115+
116+
109117
class ServiceRunForCheckDB(BaseModel):
110118
service_run_id: ServiceRunId
111119
last_heartbeat_at: datetime

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from models_library.api_schemas_storage import S3BucketName
1111
from models_library.products import ProductName
1212
from models_library.resource_tracker import (
13+
CreditClassification,
1314
CreditTransactionId,
1415
CreditTransactionStatus,
1516
PricingPlanCreate,
@@ -63,6 +64,7 @@
6364
from ....models.resource_tracker_pricing_unit_costs import PricingUnitCostsDB
6465
from ....models.resource_tracker_pricing_units import PricingUnitsDB
6566
from ....models.resource_tracker_service_runs import (
67+
OsparcCreditsAggregatedByServiceKeyDB,
6668
ServiceRunCreate,
6769
ServiceRunDB,
6870
ServiceRunForCheckDB,
@@ -309,6 +311,89 @@ async def list_service_runs_by_product_and_user_and_wallet(
309311

310312
return [ServiceRunWithCreditsDB.from_orm(row) for row in result.fetchall()]
311313

314+
async def get_osparc_credits_aggregated_by_service(
315+
self,
316+
product_name: ProductName,
317+
*,
318+
user_id: UserID | None,
319+
wallet_id: WalletID,
320+
offset: int,
321+
limit: int,
322+
started_from: datetime | None = None,
323+
started_until: datetime | None = None,
324+
) -> tuple[int, list[OsparcCreditsAggregatedByServiceKeyDB]]:
325+
async with self.db_engine.begin() as conn:
326+
base_query = (
327+
sa.select(
328+
resource_tracker_service_runs.c.service_key,
329+
sa.func.SUM(
330+
resource_tracker_credit_transactions.c.osparc_credits
331+
).label("osparc_credits"),
332+
)
333+
.select_from(
334+
resource_tracker_service_runs.join(
335+
resource_tracker_credit_transactions,
336+
(
337+
resource_tracker_service_runs.c.product_name
338+
== resource_tracker_credit_transactions.c.product_name
339+
)
340+
& (
341+
resource_tracker_service_runs.c.service_run_id
342+
== resource_tracker_credit_transactions.c.service_run_id
343+
),
344+
isouter=True,
345+
)
346+
)
347+
.where(
348+
(resource_tracker_service_runs.c.product_name == product_name)
349+
& (
350+
resource_tracker_credit_transactions.c.transaction_status
351+
== CreditTransactionStatus.BILLED
352+
)
353+
& (
354+
resource_tracker_credit_transactions.c.transaction_classification
355+
== CreditClassification.DEDUCT_SERVICE_RUN
356+
)
357+
& (resource_tracker_credit_transactions.c.wallet_id == wallet_id)
358+
)
359+
.group_by(resource_tracker_service_runs.c.service_key)
360+
)
361+
362+
if user_id:
363+
base_query = base_query.where(
364+
resource_tracker_service_runs.c.user_id == user_id
365+
)
366+
if started_from:
367+
base_query = base_query.where(
368+
sa.func.DATE(resource_tracker_service_runs.c.started_at)
369+
>= started_from.date()
370+
)
371+
if started_until:
372+
base_query = base_query.where(
373+
sa.func.DATE(resource_tracker_service_runs.c.started_at)
374+
<= started_until.date()
375+
)
376+
377+
subquery = base_query.subquery()
378+
count_query = sa.select(sa.func.count()).select_from(subquery)
379+
count_result = await conn.execute(count_query)
380+
381+
# Default ordering and pagination
382+
list_query = (
383+
base_query.order_by(resource_tracker_service_runs.c.service_key.asc())
384+
.offset(offset)
385+
.limit(limit)
386+
)
387+
list_result = await conn.execute(list_query)
388+
389+
return (
390+
cast(int, count_result.scalar()),
391+
[
392+
OsparcCreditsAggregatedByServiceKeyDB.from_orm(row)
393+
for row in list_result.fetchall()
394+
],
395+
)
396+
312397
async def export_service_runs_table_to_s3(
313398
self,
314399
product_name: ProductName,

0 commit comments

Comments
 (0)