Skip to content

Commit e64d162

Browse files
authored
Fix: multiple bugs on CLI (#406)
* feature: pricing now allow to specify a payment-type * fix: get_annotated_constraint was not getting the max value * fix: pricing only show the pricing of the selected payment_type * fix: aleph instance list to display Credits instead of $ALEPH for credits instance * fix: ports in the aggregate can be None, if None then we continue * fix: ports in the aggregate can be None, if None then we continue
1 parent f1d4a5e commit e64d162

File tree

9 files changed

+125
-60
lines changed

9 files changed

+125
-60
lines changed

src/aleph_client/commands/instance/__init__.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import shutil
77
from decimal import Decimal
88
from pathlib import Path
9-
from typing import Annotated, Any, Optional, Union
9+
from typing import Annotated, Any, Optional
1010

1111
import aiohttp
1212
import typer
@@ -30,8 +30,6 @@
3030
)
3131
from aleph.sdk.query.responses import PriceResponse
3232
from aleph.sdk.types import (
33-
CrnExecutionV1,
34-
CrnExecutionV2,
3533
InstanceAllocationsInfo,
3634
InstanceWithScheduler,
3735
PortFlags,
@@ -213,7 +211,7 @@ async def create(
213211
)
214212
)
215213
async with AuthenticatedAlephHttpClient(account=account) as client:
216-
vouchers = await client.voucher.fetch_vouchers_by_chain(chain=Chain(account.CHAIN))
214+
vouchers = await client.voucher.fetch_vouchers_by_chain(address=address, chain=Chain(account.CHAIN))
217215
if len(vouchers) == 0:
218216
console.print("No NFT vouchers find on this account")
219217
raise typer.Exit(code=1)
@@ -354,7 +352,9 @@ async def create(
354352
)
355353

356354
if not tier:
357-
pricing.display_table_for(entity=pricing_entity, network_gpu=found_gpu_models, tier=None)
355+
pricing.display_table_for(
356+
entity=pricing_entity, network_gpu=found_gpu_models, tier=None, payment_type=payment_type
357+
)
358358
tiers = list(pricing.data[pricing_entity].tiers)
359359

360360
# GPU entities: filter to tiers that actually use the selected GPUs
@@ -418,7 +418,8 @@ async def create(
418418
compute_unit_price = pricing.data[pricing_entity].price.get("compute_unit")
419419
if payment_type in [PaymentType.hold, PaymentType.superfluid]:
420420
# Early check with minimal cost (Gas + Aleph ERC20)
421-
balance_response = await client.get_balances(address)
421+
async with AlephHttpClient(api_server=settings.API_HOST) as client:
422+
balance_response = await client.get_balances(address)
422423
available_amount = balance_response.balance - balance_response.locked_amount
423424
available_funds = Decimal(0 if is_stream else available_amount)
424425
try:
@@ -467,7 +468,6 @@ async def create(
467468
echo(f"Failed to fetch credit info, error: {e}")
468469
raise typer.Exit(code=1) from e
469470

470-
stream_reward_address = None
471471
crn, crn_info, gpu_id = None, None, None
472472
if is_stream or confidential or gpu or is_credit:
473473
if crn_url:
@@ -607,7 +607,7 @@ async def create(
607607

608608
payment = Payment(
609609
chain=payment_chain,
610-
receiver=stream_reward_address if stream_reward_address else None,
610+
receiver=crn_info.stream_reward_address if crn_info and crn_info.stream_reward_address else None,
611611
type=payment_type,
612612
)
613613

@@ -633,7 +633,7 @@ async def create(
633633
try:
634634
content = make_instance_content(**content_dict)
635635
price: PriceResponse = await client.get_estimated_price(content)
636-
required_tokens = Decimal(price.required_tokens)
636+
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
637637
except Exception as e:
638638
echo(f"Failed to estimate instance cost, error: {e}")
639639
raise typer.Exit(code=1) from e
@@ -836,7 +836,6 @@ async def delete(
836836
existing_message: InstanceMessage = await client.get_message(
837837
item_hash=ItemHash(item_hash), message_type=InstanceMessage
838838
)
839-
840839
except MessageNotFoundError:
841840
echo("Instance does not exist")
842841
raise typer.Exit(code=1) from None
@@ -869,11 +868,6 @@ async def delete(
869868
if isinstance(instance_info, InstanceWithScheduler):
870869
echo(f"Instance {item_hash} was auto-scheduled, VM will be erased automatically.")
871870
elif instance_info is not None and hasattr(instance_info, "crn_url") and instance_info.crn_url:
872-
execution: Optional[Union[CrnExecutionV1, CrnExecutionV2]] = await client.crn.get_vm(
873-
instance_info.crn_url, item_hash=ItemHash(item_hash)
874-
)
875-
if not execution:
876-
echo("VM is not running or CRN not accessible, Skipping ...")
877871
try:
878872
async with VmClient(account, instance_info.crn_url) as manager:
879873
status, _ = await manager.erase_instance(vm_id=item_hash)

src/aleph_client/commands/instance/display.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ async def _prepare_instance_column(self):
283283
# Price information
284284
async with AlephHttpClient() as client:
285285
price: PriceResponse = await client.get_program_price(self.message.item_hash)
286-
required_tokens = Decimal(price.required_tokens)
286+
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
287287

288288
if self.is_hold:
289289
aleph_price = Text(f"{displayable_amount(required_tokens, decimals=3)} (fixed)", style="magenta3")

src/aleph_client/commands/instance/port_forwarder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ async def list_ports(
7474
ports_map = config.root
7575

7676
for ih, ports in ports_map.items():
77+
if not ports:
78+
continue
7779
name = await client.instance.get_name_of_executable(item_hash=ItemHash(ih))
7880

7981
# If an item hash is specified, only show that one

src/aleph_client/commands/pricing.py

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from aleph.sdk.conf import settings
2121
from aleph.sdk.utils import displayable_amount
22+
from aleph_message.models.execution.base import PaymentType
2223
from rich import box
2324
from rich.console import Console, Group
2425
from rich.panel import Panel
@@ -105,9 +106,11 @@ def fmt(value, unit):
105106
lines = [f"{prefix}$ALEPH (Holding): [bright_cyan]{holding}"]
106107

107108
# Show credits ONLY for storage, and only if a credit price exists
108-
if storage and storage_price.credit:
109-
credit = fmt(storage_price.credit, "credit")
110-
lines.append(f"Credits: [bright_cyan]{credit}")
109+
# Credits not handled for now on storage so we keep that comment for now
110+
111+
# if storage and storage_price.credit:
112+
# credit = fmt(storage_price.credit, "credit")
113+
# lines.append(f"Credits: [bright_cyan]{credit}")
111114

112115
infos.append(Text.from_markup("\n".join(lines)))
113116

@@ -117,6 +120,7 @@ def build_column(
117120
self,
118121
entity: PricingEntity,
119122
entity_info: PricingPerEntity,
123+
payment_type: Optional[PaymentType] = None,
120124
):
121125
# Common Column for PROGRAM / INSTANCE / CONF / GPU
122126
self.table.add_column("Tier", style="cyan")
@@ -131,13 +135,20 @@ def build_column(
131135
self.table.add_column("VRAM (GiB)", style="orange1")
132136

133137
cu_price = entity_info.price.get("compute_unit")
134-
if isinstance(cu_price, Price) and cu_price.holding:
138+
139+
if (isinstance(cu_price, Price) and cu_price.holding) and (
140+
payment_type is None or payment_type == PaymentType.hold
141+
):
135142
self.table.add_column("$ALEPH (Holding)", style="red", justify="center")
136143

137-
if isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP:
144+
if (isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP) and (
145+
payment_type is None or payment_type == PaymentType.superfluid
146+
):
138147
self.table.add_column("$ALEPH (Pay-As-You-Go)", style="green", justify="center")
139148

140-
if isinstance(cu_price, Price) and cu_price.credit:
149+
if (isinstance(cu_price, Price) and cu_price.credit) and (
150+
payment_type is None or payment_type == PaymentType.credit
151+
):
141152
self.table.add_column("$ Credits", style="green", justify="center")
142153

143154
if entity in PRICING_GROUPS[GroupEntity.PROGRAM]:
@@ -149,6 +160,7 @@ def fill_tier(
149160
entity: PricingEntity,
150161
entity_info: PricingPerEntity,
151162
network_gpu: Optional[NetworkGPUS] = None,
163+
payment_type: Optional[PaymentType] = None,
152164
):
153165
tier_id = self._format_tier_id(tier.id)
154166
self.table.add_section()
@@ -190,7 +202,9 @@ def fill_tier(
190202

191203
cu_price = entity_info.price.get("compute_unit")
192204
# Fill Holding row
193-
if isinstance(cu_price, Price) and cu_price.holding:
205+
if (isinstance(cu_price, Price) and cu_price.holding) and (
206+
payment_type is None or payment_type == PaymentType.hold
207+
):
194208
if entity == PricingEntity.INSTANCE_CONFIDENTIAL or (
195209
entity == PricingEntity.INSTANCE and tier.compute_units > 4
196210
):
@@ -200,15 +214,19 @@ def fill_tier(
200214
f"{displayable_amount(Decimal(str(cu_price.holding)) * tier.compute_units, decimals=3)} tokens"
201215
)
202216
# Fill PAYG row
203-
if isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP:
217+
if (isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP) and (
218+
payment_type is None or payment_type == PaymentType.superfluid
219+
):
204220
payg_price = cu_price.payg
205221
payg_hourly = Decimal(str(payg_price)) * tier.compute_units
206222
row.append(
207223
f"{displayable_amount(payg_hourly, decimals=3)} token/hour"
208224
f"\n{displayable_amount(payg_hourly * 24, decimals=3)} token/day"
209225
)
210226
# Fill Credit row
211-
if isinstance(cu_price, Price) and cu_price.credit:
227+
if (isinstance(cu_price, Price) and cu_price.credit) and (
228+
payment_type is None or payment_type == PaymentType.credit
229+
):
212230
credit_price = cu_price.credit
213231
credit_hourly = Decimal(str(credit_price)) * tier.compute_units
214232
row.append(
@@ -237,6 +255,7 @@ def fill_column(
237255
entity_info: PricingPerEntity,
238256
network_gpu: Optional[NetworkGPUS],
239257
only_tier: Optional[int] = None, # <-- now int
258+
payment_type: Optional[PaymentType] = None,
240259
):
241260
any_added = False
242261

@@ -247,12 +266,18 @@ def fill_column(
247266
for tier in entity_info.tiers:
248267
if not self._tier_matches(tier, only_tier):
249268
continue
250-
self.fill_tier(tier=tier, entity=entity, entity_info=entity_info, network_gpu=network_gpu)
269+
self.fill_tier(
270+
tier=tier, entity=entity, entity_info=entity_info, network_gpu=network_gpu, payment_type=payment_type
271+
)
251272
any_added = True
252273
return any_added
253274

254275
def display_table_for(
255-
self, entity: PricingEntity, network_gpu: Optional[NetworkGPUS] = None, tier: Optional[int] = None
276+
self,
277+
entity: PricingEntity,
278+
network_gpu: Optional[NetworkGPUS] = None,
279+
tier: Optional[int] = None,
280+
payment_type: Optional[PaymentType] = None,
256281
):
257282
info = self.data[entity]
258283
label = self._format_name(entity=entity)
@@ -279,39 +304,68 @@ def display_table_for(
279304
)
280305
self.table = table
281306

282-
self.build_column(entity=entity, entity_info=info)
307+
self.build_column(entity=entity, entity_info=info, payment_type=payment_type)
283308

284-
any_rows_added = self.fill_column(entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=tier)
309+
any_rows_added = self.fill_column(
310+
entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=tier, payment_type=payment_type
311+
)
285312

286313
# If no rows were added, the filter was too restrictive
287314
# So add all tiers without filter
288315
if not any_rows_added:
289316
self.fill_column(entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=None)
290317

291-
storage_price = info.price.get("storage")
292-
extra_price_holding = ""
293-
if isinstance(storage_price, Price) and storage_price.holding:
294-
extra_price_holding = (
295-
f"[red]{displayable_amount(Decimal(str(storage_price.holding)) * 1024, decimals=5)}"
296-
" token/GiB[/red] (Holding) -or- "
297-
)
298-
299-
payg_storage_price = "0"
300-
if isinstance(storage_price, Price) and storage_price.payg:
301-
payg_storage_price = displayable_amount(Decimal(str(storage_price.payg)) * 1024 * 24, decimals=5)
302-
303-
extra_price_credits = "0"
304-
if isinstance(storage_price, Price) and storage_price.credit:
305-
extra_price_credits = displayable_amount(Decimal(str(storage_price.credit)) * 1024 * 24, decimals=5)
318+
def build_extra_volume_cost(
319+
storage_price: Optional[Price], payment_type: Optional[PaymentType] = None
320+
) -> Text:
321+
"""
322+
Returns a Text markup with the correct extra volume cost
323+
depending on payment_type.
324+
"""
325+
326+
extra_price_holding = ""
327+
payg_storage_price = "0"
328+
extra_price_credits = "0"
329+
330+
if isinstance(storage_price, Price):
331+
if storage_price.holding:
332+
extra_price_holding = (
333+
f"[red]{displayable_amount(Decimal(str(storage_price.holding)) * 1024, decimals=5)}"
334+
" token/GiB[/red] (Holding)"
335+
)
336+
337+
if storage_price.payg:
338+
payg_storage_price = displayable_amount(
339+
Decimal(str(storage_price.payg)) * 1024 * 24, decimals=5
340+
)
341+
342+
if storage_price.credit:
343+
extra_price_credits = displayable_amount(
344+
Decimal(str(storage_price.credit)) * 1024 * 24, decimals=5
345+
)
346+
347+
# Handle by payment_type
348+
if payment_type is None:
349+
return Text.from_markup(
350+
f"Extra Volume Cost: {extra_price_holding} "
351+
f"[green]{payg_storage_price} token/GiB/day[/green] (Pay-As-You-Go) "
352+
f"-or- [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
353+
)
354+
elif payment_type == PaymentType.superfluid:
355+
return Text.from_markup(
356+
f"Extra Volume Cost: [green]{payg_storage_price} token/GiB/day[/green] (Pay-As-You-Go)\n"
357+
)
358+
elif payment_type == PaymentType.hold:
359+
return Text.from_markup(f"Extra Volume Cost: {extra_price_holding}\n")
360+
elif payment_type == PaymentType.credit:
361+
return Text.from_markup(
362+
f"Extra Volume Cost: [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
363+
)
364+
else:
365+
return Text("Extra Volume Cost: Unknown payment type\n")
306366

307-
infos = [
308-
Text.from_markup(
309-
f"Extra Volume Cost: {extra_price_holding}"
310-
f"[green]{payg_storage_price}"
311-
" token/GiB/day[/green] (Pay-As-You-Go)"
312-
f" -or- [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
313-
)
314-
]
367+
storage_price = info.price.get("storage")
368+
infos = [build_extra_volume_cost(storage_price=storage_price, payment_type=payment_type)]
315369
displayable_group = Group(
316370
self.table,
317371
Text.assemble(*infos),
@@ -340,6 +394,7 @@ async def fetch_pricing_aggregate() -> Pricing:
340394
async def prices_for_service(
341395
service: Annotated[GroupEntity, typer.Argument(help="Service to display pricing for")],
342396
tier: Annotated[Optional[int], typer.Option(help="Service specific Tier")] = None,
397+
payment_type: Annotated[Optional[PaymentType], typer.Option(help="Payment Type")] = None,
343398
json: Annotated[bool, typer.Option(help="JSON output instead of Rich Table")] = False,
344399
no_null: Annotated[bool, typer.Option(help="Exclude null values in JSON output")] = False,
345400
with_current_availability: Annotated[
@@ -376,4 +431,4 @@ async def prices_for_service(
376431
)
377432
else:
378433
for entity in group:
379-
pricing.display_table_for(entity, network_gpu=network_gpu, tier=tier)
434+
pricing.display_table_for(entity, network_gpu=network_gpu, tier=tier, payment_type=payment_type)

src/aleph_client/commands/program.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ async def upload(
242242
try:
243243
content = make_program_content(**content_dict)
244244
price: PriceResponse = await client.get_estimated_price(content)
245-
required_tokens = Decimal(price.required_tokens)
245+
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
246246
except Exception as e:
247247
typer.echo(f"Failed to estimate program cost, error: {e}")
248248
raise typer.Exit(code=1) from e
@@ -575,7 +575,7 @@ async def list_programs(
575575
if message.sender != message.content.address:
576576
payer = Text.assemble("\nPayer: ", Text(str(message.content.address), style="orange1"))
577577
price: PriceResponse = await client.get_program_price(message.item_hash)
578-
required_tokens = Decimal(price.required_tokens)
578+
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
579579
if price.payment_type == PaymentType.hold.value:
580580
aleph_price = Text(
581581
f"{displayable_amount(required_tokens, decimals=3)} (fixed)".ljust(13), style="violet"

src/aleph_client/commands/utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,16 @@ def get_annotated_constraint(annotated_type: type, constraint_name: str) -> Any
100100
if not args:
101101
return None
102102

103-
for arg in args:
104-
if isinstance(arg, FieldInfo):
105-
value = getattr(arg, constraint_name, None)
106-
return value
103+
# We only care about FieldInfo, not the base type (like int)
104+
field_info = next((a for a in args if isinstance(a, FieldInfo)), None)
105+
if not field_info:
106+
return None
107+
108+
# FieldInfo stores constraint objects in its metadata list
109+
for meta in getattr(field_info, "metadata", []):
110+
if hasattr(meta, constraint_name):
111+
return getattr(meta, constraint_name)
112+
107113
return None
108114

109115

0 commit comments

Comments
 (0)