Skip to content

Commit c6a1f28

Browse files
committed
Add terms and conditions (updated)
1 parent af117b9 commit c6a1f28

File tree

11 files changed

+141
-42
lines changed

11 files changed

+141
-42
lines changed

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ dependencies = [
3434
"aiodns==3.2",
3535
"aiohttp==3.10.6",
3636
"aleph-message>=0.6",
37-
"aleph-sdk-python>=1.2.1,<2",
38-
"base58==2.1.1", # Needed now as default with _load_account changement
39-
"py-sr25519-bindings==0.2", # Needed for DOT signatures
37+
"aleph-sdk-python @ git+https://github.com/aleph-im/aleph-sdk-python@feat-add-ipfs-gateway",
38+
"base58==2.1.1", # Needed now as default with _load_account changement
39+
"py-sr25519-bindings==0.2", # Needed for DOT signatures
4040
"pygments==2.18",
41-
"pynacl==1.5", # Needed now as default with _load_account changement
41+
"pynacl==1.5", # Needed now as default with _load_account changement
4242
"python-magic==0.4.27",
4343
"rich==13.9.3",
4444
"setuptools>=65.5",
45-
"substrate-interface==1.7.11", # Needed for DOT signatures
45+
"substrate-interface==1.7.11", # Needed for DOT signatures
4646
"textual==0.73",
4747
"typer==0.12.5",
4848
]

scripts/gendoc.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
Copied from typer.cli.py to customise doc generation
44
"""
55

6-
import click
76
import importlib.util
87
import re
98
import sys
9+
from pathlib import Path
10+
from typing import Any, List, Optional
11+
12+
import click
1013
import typer
1114
import typer.core
1215
from click import Command, Group
13-
from pathlib import Path
14-
from typing import Any, List, Optional
1516

1617
default_app_names = ("app", "cli", "main")
1718
default_func_names = ("main", "cli", "app")

src/aleph_client/commands/files.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient
1212
from aleph.sdk.account import _load_account
1313
from aleph.sdk.conf import settings
14-
from aleph.sdk.types import AccountFromPrivateKey, StorageEnum
15-
from aleph_message.models import ItemHash, StoreMessage
14+
from aleph.sdk.exceptions import ForgottenMessageError, MessageNotFoundError
15+
from aleph.sdk.types import AccountFromPrivateKey, StorageEnum, StoredContent
16+
from aleph.sdk.utils import safe_getattr
17+
from aleph_message.models import ItemHash, ItemType, MessageType, StoreMessage
1618
from aleph_message.status import MessageStatus
1719
from pydantic import BaseModel, Field
1820
from rich import box
@@ -101,28 +103,42 @@ async def download(
101103
output_path: Path = typer.Option(Path("."), help="Output directory path"),
102104
file_name: str = typer.Option(None, help="Output file name (without extension)"),
103105
file_extension: str = typer.Option(None, help="Output file extension"),
106+
only_info: bool = False,
107+
verbose: bool = True,
104108
debug: bool = False,
105-
):
106-
"""Download a file on aleph.im."""
109+
) -> Optional[StoredContent]:
110+
"""Download a file on aleph.im or display related infos."""
107111

108112
setup_logging(debug)
109113

110-
output_path.mkdir(parents=True, exist_ok=True)
114+
if not only_info:
115+
output_path.mkdir(parents=True, exist_ok=True)
111116

112-
file_name = file_name if file_name else hash
113-
file_extension = file_extension if file_extension else ""
117+
file_name = file_name if file_name else hash
118+
file_extension = file_extension if file_extension else ""
114119

115-
output_file_path = output_path / f"{file_name}{file_extension}"
120+
output_file_path = output_path / f"{file_name}{file_extension}"
116121

117-
async with AlephHttpClient(api_server=settings.API_HOST) as client:
118-
logger.info(f"Downloading {hash} ...")
119-
with open(output_file_path, "wb") as fd:
120-
if not use_ipfs:
121-
await client.download_file_to_buffer(hash, fd)
122-
else:
123-
await client.download_file_ipfs_to_buffer(hash, fd)
122+
async with AlephHttpClient(api_server=settings.API_HOST) as client:
123+
logger.info(f"Downloading {hash} ...")
124+
with open(output_file_path, "wb") as fd:
125+
if not use_ipfs:
126+
await client.download_file_to_buffer(hash, fd)
127+
else:
128+
await client.download_file_ipfs_to_buffer(hash, fd)
124129

125-
logger.debug("File downloaded successfully.")
130+
logger.debug("File downloaded successfully.")
131+
else:
132+
async with AlephHttpClient(api_server=settings.API_HOST) as client:
133+
content = await client.get_stored_content(hash)
134+
if verbose:
135+
typer.echo(
136+
f"Filename: {content.filename}\nHash: {content.hash}\nURL: {content.url}"
137+
if safe_getattr(content, "url")
138+
else safe_getattr(content, "error")
139+
)
140+
return content
141+
return None
126142

127143

128144
@app.command()

src/aleph_client/commands/help_strings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
SSH_PUBKEY_FILE = "Path to a public ssh key to be added to the instance"
3636
CRN_HASH = "Hash of the CRN to deploy to (only applicable for confidential and/or Pay-As-You-Go instances)"
3737
CRN_URL = "URL of the CRN to deploy to (only applicable for confidential and/or Pay-As-You-Go instances)"
38+
CRN_AUTO_TAC = "Automatically accept the Terms & Conditions of the CRN if you read them beforehand"
3839
CONFIDENTIAL_OPTION = "Launch a confidential instance (requires creating an encrypted volume)"
3940
CONFIDENTIAL_FIRMWARE = "Hash to UEFI Firmware to launch confidential instance"
4041
CONFIDENTIAL_FIRMWARE_HASH = "Hash of the UEFI Firmware content, to validate measure (ignored if path is provided)"

src/aleph_client/commands/instance/__init__.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from aleph.sdk.query.filters import MessageFilter
2727
from aleph.sdk.query.responses import PriceResponse
2828
from aleph.sdk.types import StorageEnum
29-
from aleph.sdk.utils import calculate_firmware_hash
29+
from aleph.sdk.utils import calculate_firmware_hash, safe_getattr
3030
from aleph_message.models import InstanceMessage, StoreMessage
3131
from aleph_message.models.base import Chain, MessageType
3232
from aleph_message.models.execution.base import Payment, PaymentType
@@ -58,7 +58,6 @@
5858
from aleph_client.commands.utils import (
5959
filter_only_valid_messages,
6060
get_or_prompt_volumes,
61-
safe_getattr,
6261
setup_logging,
6362
str_to_datetime,
6463
validate_ssh_pubkey_file,
@@ -117,6 +116,7 @@ async def create(
117116
None,
118117
help=help_strings.IMMUTABLE_VOLUME,
119118
),
119+
crn_auto_tac: bool = typer.Option(False, help=help_strings.CRN_AUTO_TAC),
120120
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
121121
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
122122
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
@@ -342,6 +342,7 @@ async def create(
342342
crn_info.get("computing", {}).get("ENABLE_CONFIDENTIAL_COMPUTING", False)
343343
),
344344
gpu_support=bool(crn_info.get("computing", {}).get("ENABLE_GPU_SUPPORT", False)),
345+
terms_and_conditions=crn_info.get("terms_and_conditions"),
345346
)
346347
echo("\n* Selected CRN *")
347348
crn.display_crn_specs()
@@ -413,6 +414,15 @@ async def create(
413414
device_id=selected_gpu.device_id,
414415
)
415416
]
417+
if crn.terms_and_conditions:
418+
accepted = await crn.display_terms_and_conditions(auto_accept=crn_auto_tac)
419+
if accepted is None:
420+
echo("Failed to fetch terms and conditions.\nContact support or use a different CRN.")
421+
raise typer.Exit(1)
422+
elif not accepted:
423+
echo("Terms & Conditions rejected: instance creation aborted.")
424+
raise typer.Exit(1)
425+
echo("Terms & Conditions accepted.")
416426

417427
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
418428
payment = Payment(
@@ -437,7 +447,12 @@ async def create(
437447
payment=payment,
438448
requirements=(
439449
HostRequirements(
440-
node=NodeRequirements(node_hash=crn.hash),
450+
node=NodeRequirements(
451+
node_hash=crn.hash,
452+
terms_and_conditions=(
453+
ItemHash(crn.terms_and_conditions) if crn.terms_and_conditions else None
454+
),
455+
),
441456
gpu=gpu_requirement,
442457
)
443458
if crn
@@ -734,6 +749,19 @@ async def _show_instances(messages: List[InstanceMessage], node_list: NodeInfo):
734749
Text(str(info["ipv6_logs"])),
735750
style="bright_yellow" if len(str(info["ipv6_logs"]).split(":")) == 8 else "dark_orange",
736751
),
752+
(
753+
Text.assemble(
754+
Text(
755+
f"\n[{'✅' if info['terms_and_conditions']['accepted'] else '❌'}] Accepted Terms & Conditions:\n"
756+
),
757+
Text(
758+
f"{info['terms_and_conditions']['url']}",
759+
style="orange1",
760+
),
761+
)
762+
if info["terms_and_conditions"]["hash"]
763+
else ""
764+
),
737765
)
738766
table.add_row(instance, specifications, status_column)
739767
table.add_section()
@@ -1197,6 +1225,7 @@ async def confidential_create(
11971225
None,
11981226
help=help_strings.IMMUTABLE_VOLUME,
11991227
),
1228+
crn_auto_tac: bool = typer.Option(False, help=help_strings.CRN_AUTO_TAC),
12001229
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
12011230
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
12021231
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
@@ -1228,6 +1257,7 @@ async def confidential_create(
12281257
ssh_pubkey_file=ssh_pubkey_file,
12291258
crn_hash=crn_hash,
12301259
crn_url=crn_url,
1260+
crn_auto_tac=crn_auto_tac,
12311261
confidential=True,
12321262
confidential_firmware=confidential_firmware,
12331263
gpu=gpu,

src/aleph_client/commands/instance/display.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def compose(self):
7272
self.table.add_column("Free RAM 🌡", key="ram")
7373
self.table.add_column("Free Disk 💿", key="hdd")
7474
self.table.add_column("URL", key="url")
75+
self.table.add_column("Terms & Conditions 📝", key="tac")
7576
yield Label("Choose a Compute Resource Node (CRN) to run your instance")
7677
with Horizontal():
7778
self.loader_label_start = Label(self.label_start)
@@ -103,6 +104,7 @@ async def fetch_node_list(self):
103104
qemu_support=None,
104105
confidential_computing=None,
105106
gpu_support=None,
107+
terms_and_conditions=node.get("terms_and_conditions"),
106108
)
107109

108110
# Initialize the progress bar
@@ -161,6 +163,9 @@ async def fetch_node_info(self, node: CRNInfo):
161163
return
162164
self.filtered_crns += 1
163165

166+
# Fetch terms and conditions
167+
tac = await node.terms_and_conditions_content
168+
164169
self.table.add_row(
165170
_format_score(node.score),
166171
node.name,
@@ -173,6 +178,7 @@ async def fetch_node_info(self, node: CRNInfo):
173178
node.display_ram,
174179
node.display_hdd,
175180
node.url,
181+
tac.url if tac else "✖",
176182
key=node.hash,
177183
)
178184

src/aleph_client/commands/instance/network.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
import logging
44
from ipaddress import IPv6Interface
55
from json import JSONDecodeError
6-
from typing import Optional
6+
from typing import Any, Optional
77

88
import aiohttp
99
from aleph.sdk import AlephHttpClient
1010
from aleph.sdk.conf import settings
11+
from aleph.sdk.utils import safe_getattr
1112
from aleph_message.models import InstanceMessage
1213
from aleph_message.models.execution.base import PaymentType
1314
from aleph_message.models.item_hash import ItemHash
1415
from pydantic import ValidationError
1516

1617
from aleph_client.commands import help_strings
18+
from aleph_client.commands.files import download
1719
from aleph_client.commands.node import NodeInfo, _fetch_nodes
18-
from aleph_client.commands.utils import safe_getattr
1920
from aleph_client.models import MachineUsage
20-
from aleph_client.utils import AsyncTyper, fetch_json, sanitize_url
21+
from aleph_client.utils import fetch_json, sanitize_url
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -68,7 +69,7 @@ async def fetch_crn_info(node_url: str) -> dict | None:
6869
return None
6970

7071

71-
async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[str, dict[str, object]]:
72+
async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[str, dict[str, Any]]:
7273
"""
7374
Fetches VM information given an instance message and the node list.
7475
@@ -93,6 +94,7 @@ async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[
9394
allocation_type="",
9495
ipv6_logs="",
9596
crn_url="",
97+
terms_and_conditions=dict(hash=None, url=None, accepted=False),
9698
)
9799
try:
98100
# Fetch from the scheduler API directly if no payment or no receiver (hold-tier non-confidential)
@@ -118,6 +120,14 @@ async def fetch_vm_info(message: InstanceMessage, node_list: NodeInfo) -> tuple[
118120
for node in node_list.nodes:
119121
if node["hash"] == safe_getattr(message, "content.requirements.node.node_hash"):
120122
info["crn_url"] = node["address"].rstrip("/")
123+
124+
# Terms and conditions
125+
tac_item_hash = safe_getattr(message, "content.requirements.node.terms_and_conditions")
126+
if tac_item_hash:
127+
tac = await download(tac_item_hash, only_info=True, verbose=False)
128+
tac_url = safe_getattr(tac, "url") or f"missing → {tac_item_hash}"
129+
info["terms_and_conditions"] = dict(hash=tac_item_hash, url=tac_url, accepted=True)
130+
121131
path = f"{node['address'].rstrip('/')}/about/executions/list"
122132
executions = await fetch_json(session, path)
123133
if message.item_hash in executions:

src/aleph_client/commands/utils.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,6 @@ def is_environment_interactive() -> bool:
239239
)
240240

241241

242-
def safe_getattr(obj, attr, default=None):
243-
for part in attr.split("."):
244-
obj = getattr(obj, part, default)
245-
if obj is default:
246-
break
247-
return obj
248-
249-
250242
async def wait_for_processed_instance(session: ClientSession, item_hash: ItemHash):
251243
"""Wait for a message to be processed by CCN"""
252244
while True:

src/aleph_client/models.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from datetime import datetime
2-
from enum import Enum
32
from typing import List, Optional
43

4+
from aleph.sdk.types import StoredContent
55
from aleph_message.models import ItemHash
66
from aleph_message.models.execution.environment import CpuProperties
77
from pydantic import BaseModel
8+
from rich.prompt import Prompt
89
from typer import echo
910

11+
from aleph_client.commands.files import download
1012
from aleph_client.commands.node import _escape_and_normalize, _remove_ansi_escape
1113

1214

@@ -130,6 +132,7 @@ class CRNInfo(BaseModel):
130132
qemu_support: Optional[bool]
131133
confidential_computing: Optional[bool]
132134
gpu_support: Optional[bool]
135+
terms_and_conditions: Optional[str]
133136

134137
@property
135138
def display_cpu(self) -> str:
@@ -149,6 +152,27 @@ def display_hdd(self) -> str:
149152
return f"{self.machine_usage.disk.available_kB / 1_000_000:>4.0f} / {self.machine_usage.disk.total_kB / 1_000_000:>4.0f} GB"
150153
return ""
151154

155+
@property
156+
async def terms_and_conditions_content(self) -> Optional[StoredContent]:
157+
if self.terms_and_conditions:
158+
return await download(self.terms_and_conditions, only_info=True, verbose=False)
159+
return None
160+
161+
async def display_terms_and_conditions(self, auto_accept: bool = False) -> Optional[bool]:
162+
if self.terms_and_conditions:
163+
tac = await self.terms_and_conditions_content
164+
echo("* Terms & Conditions *")
165+
if tac:
166+
echo("The selected CRN requires you to accept the following conditions and terms of use:\n")
167+
if tac.filename:
168+
echo(f"Filename: {tac.filename}")
169+
echo(f"↳ {tac.url}\n")
170+
if auto_accept:
171+
echo("To proceed, enter “Yes I read and accept”: Yes I read and accept")
172+
return True
173+
return Prompt.ask("To proceed, enter “Yes I read and accept”").lower() == "yes i read and accept"
174+
return None
175+
152176
def display_crn_specs(self):
153177
echo(f"Hash: {self.hash}")
154178
echo(f"Name: {self.name}")
@@ -163,3 +187,5 @@ def display_crn_specs(self):
163187
echo(f"Support Qemu: {self.qemu_support}")
164188
echo(f"Support Confidential: {self.confidential_computing}")
165189
echo(f"Support GPU: {self.gpu_support}")
190+
if self.terms_and_conditions:
191+
echo(f"Terms & Conditions: {self.terms_and_conditions}")

0 commit comments

Comments
 (0)