Skip to content

Commit 1e0d29e

Browse files
committed
feat: Implement historical pricing for message cost recalculation
1 parent 3cf1938 commit 1e0d29e

File tree

7 files changed

+1058
-53
lines changed

7 files changed

+1058
-53
lines changed

deployment/migrations/versions/0033_1c06d0ade60c_calculate_costs_statically.py

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from aleph.db.accessors.cost import make_costs_upsert_query
1818
from aleph.db.accessors.messages import get_message_by_item_hash
1919
from aleph.services.cost import _is_confidential_vm, get_detailed_costs, CostComputableContent
20-
from aleph.types.cost import ProductComputeUnit, ProductPrice, ProductPriceOptions, ProductPriceType, ProductPricing
20+
from aleph.services.pricing_utils import build_default_pricing_model
21+
from aleph.types.cost import ProductPriceType, ProductPricing
2122
from aleph.types.db_session import DbSession
2223

2324
logger = logging.getLogger("alembic")
@@ -30,48 +31,6 @@
3031
depends_on = None
3132

3233

33-
hardcoded_initial_price: Dict[ProductPriceType, ProductPricing] = {
34-
ProductPriceType.PROGRAM: ProductPricing(
35-
ProductPriceType.PROGRAM,
36-
ProductPrice(
37-
ProductPriceOptions("0.05", "0.000000977"),
38-
ProductPriceOptions("200", "0.011")
39-
),
40-
ProductComputeUnit(1, 2048, 2048)
41-
),
42-
ProductPriceType.PROGRAM_PERSISTENT: ProductPricing(
43-
ProductPriceType.PROGRAM_PERSISTENT,
44-
ProductPrice(
45-
ProductPriceOptions("0.05", "0.000000977"),
46-
ProductPriceOptions("1000", "0.055")
47-
),
48-
ProductComputeUnit(1, 20480, 2048)
49-
),
50-
ProductPriceType.INSTANCE: ProductPricing(
51-
ProductPriceType.INSTANCE,
52-
ProductPrice(
53-
ProductPriceOptions("0.05", "0.000000977"),
54-
ProductPriceOptions("1000", "0.055")
55-
),
56-
ProductComputeUnit(1, 20480, 2048)
57-
),
58-
ProductPriceType.INSTANCE_CONFIDENTIAL: ProductPricing(
59-
ProductPriceType.INSTANCE_CONFIDENTIAL,
60-
ProductPrice(
61-
ProductPriceOptions("0.05", "0.000000977"),
62-
ProductPriceOptions("2000", "0.11")
63-
),
64-
ProductComputeUnit(1, 20480, 2048)
65-
),
66-
ProductPriceType.STORAGE: ProductPricing(
67-
ProductPriceType.STORAGE,
68-
ProductPrice(
69-
ProductPriceOptions("0.333333333"),
70-
)
71-
),
72-
}
73-
74-
7534
def _get_product_instance_type(
7635
content: InstanceContent
7736
) -> ProductPriceType:
@@ -112,12 +71,15 @@ def do_calculate_costs() -> None:
11271

11372
logger.debug("INIT: CALCULATE COSTS FOR: %r", msg_item_hashes)
11473

74+
# Build the initial pricing model from DEFAULT_PRICE_AGGREGATE
75+
initial_pricing_model = build_default_pricing_model()
76+
11577
for item_hash in msg_item_hashes:
11678
message = get_message_by_item_hash(session, item_hash)
11779
if message:
11880
content = message.parsed_content
11981
type = _get_product_price_type(content)
120-
pricing = hardcoded_initial_price[type]
82+
pricing = initial_pricing_model[type]
12183
costs = get_detailed_costs(session, content, message.item_hash, pricing)
12284

12385
if len(costs) > 0:

src/aleph/services/pricing_utils.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Utility functions for pricing model creation and management.
3+
"""
4+
5+
import datetime as dt
6+
from typing import Dict, List, Union
7+
8+
from aleph.db.accessors.aggregates import get_aggregate_elements, merge_aggregate_elements
9+
from aleph.db.models import AggregateElementDb
10+
from aleph.toolkit.constants import (
11+
DEFAULT_PRICE_AGGREGATE,
12+
PRICE_AGGREGATE_KEY,
13+
PRICE_AGGREGATE_OWNER,
14+
)
15+
from aleph.types.cost import ProductPriceType, ProductPricing
16+
from aleph.types.db_session import DbSession
17+
18+
19+
def build_pricing_model_from_aggregate(aggregate_content: Dict[Union[ProductPriceType, str], dict]) -> Dict[ProductPriceType, ProductPricing]:
20+
"""
21+
Build a complete pricing model from an aggregate content dictionary.
22+
23+
This function converts the DEFAULT_PRICE_AGGREGATE format or any pricing aggregate
24+
content into a dictionary of ProductPricing objects that can be used by the cost
25+
calculation functions.
26+
27+
Args:
28+
aggregate_content: Dictionary containing pricing information with ProductPriceType as keys
29+
30+
Returns:
31+
Dictionary mapping ProductPriceType to ProductPricing objects
32+
"""
33+
pricing_model: Dict[ProductPriceType, ProductPricing] = {}
34+
35+
for price_type, pricing_data in aggregate_content.items():
36+
try:
37+
price_type = ProductPriceType(price_type)
38+
pricing_model[price_type] = ProductPricing.from_aggregate(
39+
price_type, aggregate_content
40+
)
41+
except (KeyError, ValueError) as e:
42+
# Log the error but continue processing other price types
43+
import logging
44+
logger = logging.getLogger(__name__)
45+
logger.warning(f"Failed to parse pricing for {price_type}: {e}")
46+
47+
return pricing_model
48+
49+
50+
def build_default_pricing_model() -> Dict[ProductPriceType, ProductPricing]:
51+
"""
52+
Build the default pricing model from DEFAULT_PRICE_AGGREGATE constant.
53+
54+
Returns:
55+
Dictionary mapping ProductPriceType to ProductPricing objects
56+
"""
57+
return build_pricing_model_from_aggregate(DEFAULT_PRICE_AGGREGATE)
58+
59+
60+
def get_pricing_aggregate_history(session: DbSession) -> List[AggregateElementDb]:
61+
"""
62+
Get all pricing aggregate updates in chronological order.
63+
64+
Args:
65+
session: Database session
66+
67+
Returns:
68+
List of AggregateElementDb objects ordered by creation_datetime
69+
"""
70+
aggregate_elements = get_aggregate_elements(
71+
session=session,
72+
owner=PRICE_AGGREGATE_OWNER,
73+
key=PRICE_AGGREGATE_KEY
74+
)
75+
return list(aggregate_elements)
76+
77+
78+
def get_pricing_timeline(session: DbSession) -> List[tuple[dt.datetime, Dict[ProductPriceType, ProductPricing]]]:
79+
"""
80+
Get the complete pricing timeline with timestamps and pricing models.
81+
82+
This function returns a chronologically ordered list of pricing changes,
83+
useful for processing messages in chronological order and applying the
84+
correct pricing at each point in time.
85+
86+
This properly merges aggregate elements up to each point in time to create
87+
the cumulative pricing state, similar to how _update_aggregate works.
88+
89+
Args:
90+
session: Database session
91+
92+
Returns:
93+
List of tuples containing (timestamp, pricing_model)
94+
"""
95+
pricing_elements = get_pricing_aggregate_history(session)
96+
97+
timeline = []
98+
99+
# Add default pricing as the initial state
100+
timeline.append((dt.datetime.min.replace(tzinfo=dt.timezone.utc), build_default_pricing_model()))
101+
102+
# Build cumulative pricing models by merging elements up to each timestamp
103+
elements_so_far = []
104+
for element in pricing_elements:
105+
elements_so_far.append(element)
106+
107+
# Merge all elements up to this point to get the cumulative state
108+
merged_content = merge_aggregate_elements(elements_so_far)
109+
110+
# Build pricing model from the merged content
111+
pricing_model = build_pricing_model_from_aggregate(merged_content)
112+
timeline.append((element.creation_datetime, pricing_model))
113+
114+
return timeline

src/aleph/toolkit/constants.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
MINUTE = 60
66
HOUR = 60 * MINUTE
77

8+
from aleph.types.cost import ProductPriceType
9+
810
PRICE_AGGREGATE_OWNER = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A"
911
PRICE_AGGREGATE_KEY = "pricing"
1012
PRICE_PRECISION = 18
1113
DEFAULT_PRICE_AGGREGATE = {
12-
"program": {
14+
ProductPriceType.PROGRAM: {
1315
"price": {
1416
"storage": {"payg": "0.000000977", "holding": "0.05"},
1517
"compute_unit": {"payg": "0.011", "holding": "200"},
@@ -28,8 +30,8 @@
2830
"memory_mib": 2048,
2931
},
3032
},
31-
"storage": {"price": {"storage": {"holding": "0.333333333"}}},
32-
"instance": {
33+
ProductPriceType.STORAGE: {"price": {"storage": {"holding": "0.333333333"}}},
34+
ProductPriceType.INSTANCE: {
3335
"price": {
3436
"storage": {"payg": "0.000000977", "holding": "0.05"},
3537
"compute_unit": {"payg": "0.055", "holding": "1000"},
@@ -48,8 +50,8 @@
4850
"memory_mib": 2048,
4951
},
5052
},
51-
"web3_hosting": {"price": {"fixed": 50, "storage": {"holding": "0.333333333"}}},
52-
"program_persistent": {
53+
ProductPriceType.WEB3_HOSTING: {"price": {"fixed": 50, "storage": {"holding": "0.333333333"}}},
54+
ProductPriceType.PROGRAM_PERSISTENT: {
5355
"price": {
5456
"storage": {"payg": "0.000000977", "holding": "0.05"},
5557
"compute_unit": {"payg": "0.055", "holding": "1000"},
@@ -68,7 +70,7 @@
6870
"memory_mib": 2048,
6971
},
7072
},
71-
"instance_gpu_premium": {
73+
ProductPriceType.INSTANCE_GPU_PREMIUM: {
7274
"price": {
7375
"storage": {"payg": "0.000000977"},
7476
"compute_unit": {"payg": "0.56"},
@@ -93,7 +95,7 @@
9395
"memory_mib": 6144,
9496
},
9597
},
96-
"instance_confidential": {
98+
ProductPriceType.INSTANCE_CONFIDENTIAL: {
9799
"price": {
98100
"storage": {"payg": "0.000000977", "holding": "0.05"},
99101
"compute_unit": {"payg": "0.11", "holding": "2000"},
@@ -112,7 +114,7 @@
112114
"memory_mib": 2048,
113115
},
114116
},
115-
"instance_gpu_standard": {
117+
ProductPriceType.INSTANCE_GPU_STANDARD: {
116118
"price": {
117119
"storage": {"payg": "0.000000977"},
118120
"compute_unit": {"payg": "0.28"},

0 commit comments

Comments
 (0)