Skip to content

Commit 881ce5e

Browse files
♻️ Is922/select default wallet and pricing plan in the backend part 2 (#4869)
Co-authored-by: Pedro Crespo <[email protected]>
1 parent ed6da73 commit 881ce5e

File tree

18 files changed

+249
-58
lines changed

18 files changed

+249
-58
lines changed

packages/models-library/src/models_library/api_schemas_directorv2/dynamic_services.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any, ClassVar, TypeAlias
22

3+
from models_library.resource_tracker import HardwareInfo, PricingInfo
34
from pydantic import BaseModel, ByteSize, Field
45

56
from ..services import ServicePortKey
@@ -46,6 +47,14 @@ class DynamicServiceCreate(ServiceDetails):
4647
default=None,
4748
description="contains information about the wallet used to bill the running service",
4849
)
50+
pricing_info: PricingInfo | None = Field(
51+
default=None,
52+
description="contains pricing information (ex. pricing plan and unit ids)",
53+
)
54+
hardware_info: HardwareInfo | None = Field(
55+
default=None,
56+
description="contains harware information (ex. aws_ec2_instances)",
57+
)
4958

5059
class Config:
5160
schema_extra: ClassVar[dict[str, Any]] = {
@@ -62,6 +71,8 @@ class Config:
6271
"examples"
6372
][0],
6473
"wallet_info": WalletInfo.Config.schema_extra["examples"][0],
74+
"pricing_info": PricingInfo.Config.schema_extra["examples"][0],
75+
"hardware_info": HardwareInfo.Config.schema_extra["examples"][0],
6576
}
6677
}
6778

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from enum import auto
2-
from typing import TypeAlias
2+
from typing import Any, ClassVar, NamedTuple, TypeAlias
33

4-
from pydantic import PositiveInt
4+
from pydantic import BaseModel, PositiveInt
55

66
from .utils.enums import StrAutoEnum
77

@@ -37,3 +37,37 @@ class CreditClassification(StrAutoEnum):
3737

3838
class PricingPlanClassification(StrAutoEnum):
3939
TIER = auto()
40+
41+
42+
class PricingInfo(BaseModel):
43+
pricing_plan_id: PricingPlanId
44+
pricing_unit_id: PricingUnitId
45+
pricing_unit_cost_id: PricingUnitCostId
46+
47+
class Config:
48+
schema_extra: ClassVar[dict[str, Any]] = {
49+
"examples": [
50+
{"pricing_plan_id": 1, "pricing_unit_id": 1, "pricing_unit_cost_id": 1}
51+
]
52+
}
53+
54+
55+
class HardwareInfo(BaseModel):
56+
aws_ec2_instances: list[str]
57+
58+
class Config:
59+
schema_extra: ClassVar[dict[str, Any]] = {
60+
"examples": [{"aws_ec2_instances": ["c6a.4xlarge"]}]
61+
}
62+
63+
64+
class PricingAndHardwareInfoTuple(NamedTuple):
65+
pricing_plan_id: PricingPlanId
66+
pricing_unit_id: PricingUnitId
67+
current_cost_per_unit_id: PricingUnitCostId
68+
aws_ec2_instances: list[str]
69+
70+
71+
class PricingPlanAndUnitIdsTuple(NamedTuple):
72+
pricing_plan_id: PricingPlanId
73+
pricing_unit_id: PricingUnitId
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""migration of aws_ec2_instances data in pricing units
2+
3+
Revision ID: 5c62b190e124
4+
Revises: 7777d181dc1f
5+
Create Date: 2023-10-17 05:15:29.780925+00:00
6+
7+
"""
8+
from alembic import op
9+
from simcore_postgres_database.models.resource_tracker_pricing_units import (
10+
resource_tracker_pricing_units,
11+
)
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "5c62b190e124"
15+
down_revision = "7777d181dc1f"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# One time migration to populate specific info with some reasonable value, it will be changed manually based on concrete needs
22+
op.execute(
23+
resource_tracker_pricing_units.update().values(
24+
specific_info={"aws_ec2_instances": ["t3.medium"]}
25+
)
26+
)
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
pass
32+
# ### end Alembic commands ###

services/director-v2/src/simcore_service_director_v2/models/dynamic_services_scheduler.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from models_library.callbacks_mapping import CallbacksMapping
1717
from models_library.generated_models.docker_rest_api import ContainerState, Status2
1818
from models_library.projects_nodes_io import NodeID
19+
from models_library.resource_tracker import HardwareInfo, PricingInfo
1920
from models_library.service_settings_labels import (
2021
DynamicSidecarServiceLabels,
2122
PathMappingsLabel,
@@ -427,6 +428,14 @@ def endpoint(self) -> AnyHttpUrl:
427428
default=None,
428429
description="contains information about the wallet used to bill the running service",
429430
)
431+
pricing_info: PricingInfo | None = Field(
432+
default=None,
433+
description="contains pricing information so we know what is the cost of running of the service",
434+
)
435+
hardware_info: HardwareInfo | None = Field(
436+
default=None,
437+
description="contains harware information so we know on which hardware to run the service",
438+
)
430439

431440
@property
432441
def get_proxy_endpoint(self) -> AnyHttpUrl:
@@ -485,6 +494,8 @@ def from_http_request(
485494
"request_simcore_user_agent": request_simcore_user_agent,
486495
"dynamic_sidecar": {"service_removal_state": {"can_save": can_save}},
487496
"wallet_info": service.wallet_info,
497+
"pricing_info": service.pricing_info,
498+
"hardware_info": service.hardware_info,
488499
}
489500
if run_id:
490501
obj_dict["run_id"] = run_id

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from .....core.settings import DynamicSidecarSettings
1717
from .....models.dynamic_services_scheduler import SchedulerData
18-
from .....modules.resource_usage_client import ResourceUsageApi
1918
from .....utils.db import get_repository
2019
from ....db.repositories.groups_extra_properties import GroupsExtraPropertiesRepository
2120
from ....db.repositories.projects import ProjectsRepository
@@ -132,14 +131,9 @@ async def progress_create_containers(
132131
if scheduler_data.wallet_info:
133132
wallet_id = scheduler_data.wallet_info.wallet_id
134133
wallet_name = scheduler_data.wallet_info.wallet_name
135-
resource_usage_api = ResourceUsageApi.get_from_state(app)
136-
(
137-
pricing_plan_id,
138-
pricing_unit_id,
139-
pricing_unit_cost_id,
140-
) = await resource_usage_api.get_default_service_pricing_plan_and_pricing_unit(
141-
scheduler_data.product_name, scheduler_data.key, scheduler_data.version
142-
)
134+
pricing_plan_id = scheduler_data.pricing_info.pricing_plan_id
135+
pricing_unit_id = scheduler_data.pricing_info.pricing_unit_id
136+
pricing_unit_cost_id = scheduler_data.pricing_info.pricing_unit_cost_id
143137

144138
metrics_params = CreateServiceMetricsAdditionalParams(
145139
wallet_id=wallet_id,

services/director-v2/tests/unit/with_dbs/test_modules_dynamic_sidecar_docker_service_specs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.aiodocker_api import AioDockerServiceSpec
1313
from models_library.callbacks_mapping import CallbacksMapping
1414
from models_library.docker import to_simcore_runtime_docker_label_key
15+
from models_library.resource_tracker import HardwareInfo, PricingInfo
1516
from models_library.service_settings_labels import (
1617
SimcoreServiceLabels,
1718
SimcoreServiceSettingsLabel,
@@ -153,6 +154,8 @@ def expected_dynamic_sidecar_spec(
153154
"request_simcore_user_agent": request_simcore_user_agent,
154155
"restart_policy": "on-inputs-downloaded",
155156
"wallet_info": WalletInfo.Config.schema_extra["examples"][0],
157+
"pricing_info": PricingInfo.Config.schema_extra["examples"][0],
158+
"hardware_info": HardwareInfo.Config.schema_extra["examples"][0],
156159
"service_name": "dy-sidecar_75c7f3f4-18f9-4678-8610-54a2ade78eaa",
157160
"service_port": 65534,
158161
"service_resources": {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class PricingUnitCostsDB(BaseModel):
1919
valid_from: datetime
2020
valid_to: datetime | None
2121
created: datetime
22-
comment: str
22+
comment: str | None
2323
modified: datetime
2424

2525
class Config:

services/web/server/src/simcore_service_webserver/catalog/_handlers.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,13 @@ async def get_service_pricing_plan(request: Request):
332332
ctx = CatalogRequestContext.create(request)
333333
path_params = parse_request_path_parameters_as(ServicePathParams, request)
334334

335-
service_pricing_plan: ServicePricingPlanGet = (
336-
await get_default_service_pricing_plan(
337-
app=request.app,
338-
product_name=ctx.product_name,
339-
service_key=path_params.service_key,
340-
service_version=path_params.service_version,
341-
)
335+
service_pricing_plan = await get_default_service_pricing_plan(
336+
app=request.app,
337+
product_name=ctx.product_name,
338+
service_key=path_params.service_key,
339+
service_version=path_params.service_version,
342340
)
343341

344-
return envelope_json_response(service_pricing_plan)
342+
return envelope_json_response(
343+
parse_obj_as(ServicePricingPlanGet, service_pricing_plan)
344+
)

services/web/server/src/simcore_service_webserver/director_v2/_core_dynamic_services.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.projects import ProjectID
1313
from models_library.projects_nodes_io import NodeIDStr
1414
from models_library.rabbitmq_messages import ProgressRabbitMessageProject, ProgressType
15+
from models_library.resource_tracker import HardwareInfo, PricingInfo
1516
from models_library.services import ServicePortKey
1617
from models_library.services_resources import (
1718
ServiceResourcesDict,
@@ -96,6 +97,8 @@ async def run_dynamic_service(
9697
simcore_user_agent: str,
9798
service_resources: ServiceResourcesDict,
9899
wallet_info: WalletInfo | None,
100+
pricing_info: PricingInfo | None,
101+
hardware_info: HardwareInfo | None,
99102
) -> DataType:
100103
"""
101104
Requests to run (i.e. create and start) a dynamic service:
@@ -115,6 +118,8 @@ async def run_dynamic_service(
115118
service_resources
116119
),
117120
"wallet_info": wallet_info,
121+
"pricing_info": pricing_info,
122+
"hardware_info": hardware_info,
118123
}
119124

120125
headers = {

services/web/server/src/simcore_service_webserver/director_v2/_handlers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
GroupExtraPropertiesRepo,
2222
)
2323
from simcore_service_webserver.db.plugin import get_database_engine
24+
from simcore_service_webserver.users.exceptions import UserDefaultWalletNotFoundError
2425

2526
from .._constants import RQ_PRODUCT_KEY
2627
from .._meta import API_VTAG as VTAG
@@ -114,9 +115,7 @@ async def start_computation(request: web.Request) -> web.Response:
114115
preference_class=user_preferences_api.PreferredWalletIdFrontendUserPreference,
115116
)
116117
if user_default_wallet_preference is None:
117-
raise ValueError(
118-
"User does not have default wallet - this should not happen"
119-
)
118+
raise UserDefaultWalletNotFoundError(uid=req_ctx.user_id)
120119
project_wallet_id = parse_obj_as(
121120
WalletID, user_default_wallet_preference.value
122121
)
@@ -198,6 +197,8 @@ async def start_computation(request: web.Request) -> web.Response:
198197
reason=exc.reason,
199198
http_error_cls=get_http_error(exc.status) or web.HTTPServiceUnavailable,
200199
)
200+
except UserDefaultWalletNotFoundError as exc:
201+
return create_error_response(exc, http_error_cls=web.HTTPNotFound)
201202

202203

203204
@routes.post(f"/{VTAG}/computations/{{project_id}}:stop", name="stop_computation")

services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from ._nodes_api import NodeScreenshot, get_node_screenshots
6060
from .db import ProjectDBAPI
6161
from .exceptions import (
62+
DefaultPricingUnitNotFoundError,
6263
NodeNotFoundError,
6364
ProjectNodeResourcesInsufficientRightsError,
6465
ProjectNodeResourcesInvalidError,
@@ -79,6 +80,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
7980
ProjectNotFoundError,
8081
NodeNotFoundError,
8182
UserDefaultWalletNotFoundError,
83+
DefaultPricingUnitNotFoundError,
8284
) as exc:
8385
raise web.HTTPNotFound(reason=f"{exc}") from exc
8486

@@ -173,9 +175,9 @@ async def get_node(request: web.Request) -> web.Response:
173175

174176
if "data" not in service_data:
175177
# dynamic-service NODE STATE
176-
assert (
178+
assert ( # nosec
177179
parse_obj_as(NodeGet | NodeGetIdle, service_data) is not None
178-
) # nosec
180+
)
179181
return envelope_json_response(service_data)
180182

181183
# LEGACY-service NODE STATE

services/web/server/src/simcore_service_webserver/projects/_project_nodes_pricing_unit_handlers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77

88
from aiohttp import web
9+
from models_library.api_schemas_webserver.resource_usage import PricingUnitGet
910
from models_library.projects import ProjectID
1011
from models_library.projects_nodes_io import NodeID
1112
from models_library.resource_tracker import PricingPlanId, PricingUnitId
@@ -83,7 +84,14 @@ async def get_project_node_pricing_unit(request: web.Request):
8384
pricing_unit_get = await rut_api.get_pricing_plan_unit(
8485
request.app, req_ctx.product_name, pricing_plan_id, pricing_unit_id
8586
)
86-
return envelope_json_response(pricing_unit_get)
87+
webserver_pricing_unit_get = PricingUnitGet(
88+
pricing_unit_id=pricing_unit_get.pricing_unit_id,
89+
unit_name=pricing_unit_get.unit_name,
90+
unit_extra_info=pricing_unit_get.unit_extra_info,
91+
current_cost_per_unit=pricing_unit_get.current_cost_per_unit,
92+
default=pricing_unit_get.default,
93+
)
94+
return envelope_json_response(webserver_pricing_unit_get)
8795

8896

8997
class _ProjectNodePricingUnitPathParams(BaseModel):

services/web/server/src/simcore_service_webserver/projects/db.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
from models_library.projects_comments import CommentID, ProjectsCommentsDB
1919
from models_library.projects_nodes import Node
2020
from models_library.projects_nodes_io import NodeID, NodeIDStr
21-
from models_library.resource_tracker import PricingPlanId, PricingUnitId
21+
from models_library.resource_tracker import (
22+
PricingPlanAndUnitIdsTuple,
23+
PricingPlanId,
24+
PricingUnitId,
25+
)
2226
from models_library.users import UserID
2327
from models_library.utils.fastapi_encoders import jsonable_encoder
2428
from models_library.wallets import WalletDB, WalletID
@@ -802,7 +806,7 @@ async def get_project_node_pricing_unit_id(
802806
self,
803807
project_uuid: ProjectID,
804808
node_uuid: NodeID,
805-
) -> tuple | None:
809+
) -> PricingPlanAndUnitIdsTuple | None:
806810
async with self.engine.acquire() as conn:
807811
result = await conn.execute(
808812
sa.select(
@@ -823,9 +827,7 @@ async def get_project_node_pricing_unit_id(
823827
)
824828
row = await result.fetchone()
825829
if row:
826-
return parse_obj_as(
827-
tuple[PricingPlanId, PricingUnitId], (row[0], row[1])
828-
)
830+
return PricingPlanAndUnitIdsTuple(row[0], row[1])
829831
return None
830832

831833
async def connect_pricing_unit_to_project_node(

services/web/server/src/simcore_service_webserver/projects/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,14 @@ class ProjectNodeResourcesInvalidError(BaseProjectError):
106106

107107
class ProjectNodeResourcesInsufficientRightsError(BaseProjectError):
108108
...
109+
110+
111+
class DefaultPricingUnitNotFoundError(BaseProjectError):
112+
"""Node was not found in project"""
113+
114+
def __init__(self, project_uuid: str, node_uuid: str):
115+
super().__init__(
116+
f"Default pricing unit not found for node {node_uuid} in project {project_uuid}"
117+
)
118+
self.node_uuid = node_uuid
119+
self.project_uuid = project_uuid

0 commit comments

Comments
 (0)