Skip to content

Commit 6e4e385

Browse files
authored
Fix: Various improvements (UX, edge cases, coverage) (#312)
## Changes ### Instances - [x] Add 10sec delay in `aleph instance confidential` cmd to ensure VM is starting - [x] Fix payment args and fallback when wrong chain selected for hold/payg - [x] Remove deprecated `aleph instance erase` and `aleph instance expire` - [x] Improve `aleph instance create` and network.py inner logic - [x] Improve commands UX ### Programs - [x] Update UX (same as instances) - [x] Add `create` alias for `aleph program upload` - [x] Add missing `aleph program delete` , `aleph program list` and `aleph program persist` - [x] Add `runtime checker` program and useful command: `aleph program runtime-checker` ### Pytest (To fix bad coverage) on Aleph testnet - [x] Add all mock tests for `instance` general cmds - [x] Add all mock tests for `instance` coco cmds - [x] Add all mock tests for `programs` cmds ### Others - [x] Add missing try/catch to handle exceptions - [x] Improve `aleph account balance` - [x] Improve `aleph node compute`
1 parent c5d11ad commit 6e4e385

File tree

21 files changed

+2425
-675
lines changed

21 files changed

+2425
-675
lines changed

README.md

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Documentation can be found on https://docs.aleph.im/tools/aleph-client/
1515
Some cryptographic functionalities use curve secp256k1 and require
1616
installing [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
1717

18-
> apt-get install -y python3-pip libsecp256k1-dev
18+
> apt-get install -y python3-pip libsecp256k1-dev squashfs-tools
1919
2020
### macOs
2121

@@ -24,8 +24,7 @@ installing [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
2424
2525
### Windows
2626

27-
The software is not tested on Windows, but should work using
28-
the Windows Subsystem for Linux (WSL).
27+
We recommend using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) (Windows Subsystem for Linux).
2928

3029
## Installation
3130

@@ -85,28 +84,15 @@ To install from source and still be able to modify the source code:
8584
8685
## Updating the User Documentation
8786

88-
The user documentation for Aleph is maintained in the `aleph-docs` repository. When releasing a new version, it's
89-
important to update the documentation as part of the release process.
90-
91-
### Steps for Updating Documentation
92-
93-
Documentation is generated using the `typer` command.
87+
The user documentation for Aleph is maintained in the [aleph-docs](https://github.com/aleph-im/aleph-docs) repository. The CLI page is generated using the `typer` command. When releasing a new version, it's important to update the documentation as part of the release process.
9488

9589
If you have the `aleph-docs` repository cloned as a sibling folder to your current directory, you can use the following
9690
command to generate updated documentation:
9791

9892
```shell
99-
./scripts/gendoc.py src/aleph_client/__main__.py docs --name aleph --title 'Aleph CLI Documentation' --output ../aleph-docs/docs/tools/aleph-client/usage.md
93+
./scripts/gendoc.py src/aleph_client/__main__.py docs \
94+
--name aleph --title 'Aleph CLI Documentation' \
95+
--output ../aleph-docs/docs/tools/aleph-client/usage.md
10096
```
10197

102-
After generating the documentation, you may need to update the path for the private key, as this depends on the user
103-
configuration. This can be fixed manually using the `sed` command. For example:
104-
105-
```shell
106-
sed -i 's#/home/olivier/.aleph-im/private-keys/sol2.key#~/.aleph-im/private-keys/ethereum.key#' ../aleph-docs/docs/tools/aleph-client/usage.md
107-
```
108-
109-
This command replaces any hardcoded private key paths with the correct configuration path (
110-
`~/.aleph-im/private-keys/ethereum.key`).
111-
112-
Once the documentation is updated, open a Pull Request (PR) on the `aleph-docs` repository with your changes.
98+
Then, open a Pull Request (PR) on the [aleph-docs](https://github.com/aleph-im/aleph-docs/pulls) repository with your changes.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ pythonpath = [
208208
testpaths = [
209209
"tests",
210210
]
211+
asyncio_default_fixture_loop_scope = "function"
211212

212213
[tool.coverage.run]
213214
branch = true

scripts/gendoc.py

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

6-
import click
76
import importlib.util
7+
import os
88
import re
99
import sys
10+
from pathlib import Path
11+
from typing import Any, List, Optional
12+
13+
import click
1014
import typer
1115
import typer.core
1216
from click import Command, Group
13-
from pathlib import Path
14-
from typing import Any, List, Optional
1517

1618
default_app_names = ("app", "cli", "main")
1719
default_func_names = ("main", "cli", "app")
@@ -246,6 +248,19 @@ def get_docs_for_click(
246248
return docs
247249

248250

251+
def replace_local_values(text: str) -> str:
252+
# Replace username
253+
current_user = Path.home().owner()
254+
text = text.replace(current_user, "$USER")
255+
256+
# Replace private key file path
257+
pattern = r"[^/]+\.key"
258+
replacement = r"ethereum.key"
259+
text = re.sub(pattern, replacement, text)
260+
261+
return text
262+
263+
249264
@utils_app.command()
250265
def docs(
251266
ctx: typer.Context,
@@ -269,13 +284,14 @@ def docs(
269284
typer.echo("No Typer app found", err=True)
270285
raise typer.Abort()
271286
click_obj = typer.main.get_command(typer_obj)
272-
docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title)
273-
clean_docs = f"{docs.strip()}\n"
287+
generated_docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title)
288+
clean_docs = f"{generated_docs.strip()}\n"
289+
fixed_docs = replace_local_values(clean_docs)
274290
if output:
275-
output.write_text(clean_docs)
291+
output.write_text(fixed_docs)
276292
typer.echo(f"Docs saved to: {output}")
277293
else:
278-
typer.echo(clean_docs)
294+
typer.echo(fixed_docs)
279295

280296

281297
utils_app()

src/aleph_client/__main__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,19 @@
1717

1818
app = AsyncTyper(no_args_is_help=True)
1919

20-
app.add_typer(account.app, name="account", help="Manage account")
21-
app.add_typer(aggregate.app, name="aggregate", help="Manage aggregate messages on aleph.im")
22-
app.add_typer(files.app, name="file", help="File uploading and pinning on IPFS and aleph.im")
20+
app.add_typer(account.app, name="account", help="Manage accounts")
2321
app.add_typer(
2422
message.app,
2523
name="message",
26-
help="Post, amend, watch and forget messages on aleph.im",
24+
help="Manage messages (post, amend, watch and forget) on aleph.im & twentysix.cloud",
2725
)
28-
app.add_typer(program.app, name="program", help="Upload and update programs on aleph.im VM")
26+
app.add_typer(aggregate.app, name="aggregate", help="Manage aggregate messages on aleph.im & twentysix.cloud")
27+
app.add_typer(files.app, name="file", help="Manage files (upload and pin on IPFS) on aleph.im & twentysix.cloud")
28+
app.add_typer(program.app, name="program", help="Manage programs (micro-VMs) on aleph.im & twentysix.cloud")
29+
app.add_typer(instance.app, name="instance", help="Manage instances (VMs) on aleph.im & twentysix.cloud")
30+
app.add_typer(domain.app, name="domain", help="Manage custom domain (DNS) on aleph.im & twentysix.cloud")
31+
app.add_typer(node.app, name="node", help="Get node info on aleph.im & twentysix.cloud")
2932
app.add_typer(about.app, name="about", help="Display the informations of Aleph CLI")
3033

31-
app.add_typer(node.app, name="node", help="Get node info on aleph.im network")
32-
app.add_typer(domain.app, name="domain", help="Manage custom Domain (dns) on aleph.im")
33-
app.add_typer(instance.app, name="instance", help="Manage instances (VMs) on aleph.im network")
34-
3534
if __name__ == "__main__":
3635
app()

src/aleph_client/commands/account.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
from aleph.sdk.utils import bytes_from_hex
2525
from aleph_message.models import Chain
2626
from rich.console import Console
27+
from rich.panel import Panel
2728
from rich.prompt import Prompt
2829
from rich.table import Table
30+
from rich.text import Text
2931
from typer.colors import GREEN, RED
3032

3133
from aleph_client.commands import help_strings
@@ -233,7 +235,7 @@ def sign_bytes(
233235
if not message:
234236
message = input_multiline()
235237

236-
coroutine = account.sign_raw(message.encode())
238+
coroutine = account.sign_raw(str(message).encode())
237239
signature = asyncio.run(coroutine)
238240
typer.echo("\nSignature: " + signature.hex())
239241

@@ -259,15 +261,44 @@ async def balance(
259261
if response.status == 200:
260262
balance_data = await response.json()
261263
balance_data["available_amount"] = balance_data["balance"] - balance_data["locked_amount"]
262-
typer.echo(
263-
"\n"
264-
+ f"Address: {balance_data['address']}\n"
265-
+ f"Balance: {balance_data['balance']:.2f}".rstrip("0").rstrip(".")
266-
+ "\n"
267-
+ f" - Locked: {balance_data['locked_amount']:.2f}".rstrip("0").rstrip(".")
268-
+ "\n"
269-
+ f" - Available: {balance_data['available_amount']:.2f}".rstrip("0").rstrip(".")
270-
+ "\n"
264+
265+
infos = [
266+
Text.from_markup(f"Address: [bright_cyan]{balance_data['address']}[/bright_cyan]"),
267+
Text.from_markup(
268+
f"\nBalance: [bright_cyan]{balance_data['balance']:.2f}".rstrip("0").rstrip(".")
269+
+ "[/bright_cyan]"
270+
),
271+
]
272+
details = balance_data.get("details")
273+
if details:
274+
infos += [Text("\n ↳ Details")]
275+
for chain, chain_balance in details.items():
276+
infos += [
277+
Text.from_markup(
278+
f"\n {chain}: [orange3]{chain_balance:.2f}".rstrip("0").rstrip(".") + "[/orange3]"
279+
)
280+
]
281+
available_color = "bright_cyan" if balance_data["available_amount"] >= 0 else "red"
282+
infos += [
283+
Text.from_markup(
284+
f"\n - Locked: [bright_cyan]{balance_data['locked_amount']:.2f}".rstrip("0").rstrip(".")
285+
+ "[/bright_cyan]"
286+
),
287+
Text.from_markup(
288+
f"\n - Available: [{available_color}]{balance_data['available_amount']:.2f}".rstrip("0").rstrip(
289+
"."
290+
)
291+
+ f"[/{available_color}]"
292+
),
293+
]
294+
console.print(
295+
Panel(
296+
Text.assemble(*infos),
297+
title="Account Infos",
298+
border_style="bright_cyan",
299+
expand=False,
300+
title_align="left",
301+
)
271302
)
272303
else:
273304
typer.echo(f"Failed to retrieve balance for address {address}. Status code: {response.status}")

src/aleph_client/commands/files.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
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.types import AccountFromPrivateKey, StorageEnum, StoredContent
15+
from aleph.sdk.utils import safe_getattr
16+
from aleph_message.models import ItemHash, ItemType, MessageType, StoreMessage
1617
from aleph_message.status import MessageStatus
1718
from pydantic import BaseModel, Field
1819
from rich import box
@@ -101,28 +102,42 @@ async def download(
101102
output_path: Path = typer.Option(Path("."), help="Output directory path"),
102103
file_name: str = typer.Option(None, help="Output file name (without extension)"),
103104
file_extension: str = typer.Option(None, help="Output file extension"),
105+
only_info: bool = False,
106+
verbose: bool = True,
104107
debug: bool = False,
105-
):
106-
"""Download a file on aleph.im."""
108+
) -> Optional[StoredContent]:
109+
"""Download a file from aleph.im or display related infos."""
107110

108111
setup_logging(debug)
109112

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

112-
file_name = file_name if file_name else hash
113-
file_extension = file_extension if file_extension else ""
116+
file_name = file_name if file_name else hash
117+
file_extension = file_extension if file_extension else ""
114118

115-
output_file_path = output_path / f"{file_name}{file_extension}"
119+
output_file_path = output_path / f"{file_name}{file_extension}"
116120

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)
121+
async with AlephHttpClient(api_server=settings.API_HOST) as client:
122+
logger.info(f"Downloading {hash} ...")
123+
with open(output_file_path, "wb") as fd:
124+
if not use_ipfs:
125+
await client.download_file_to_buffer(hash, fd)
126+
else:
127+
await client.download_file_ipfs_to_buffer(hash, fd)
124128

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

127142

128143
@app.command()

src/aleph_client/commands/help_strings.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@
22
CHANNEL = "Aleph.im network channel where the message is or will be broadcasted"
33
PRIVATE_KEY = "Your private key. Cannot be used with --private-key-file"
44
PRIVATE_KEY_FILE = "Path to your private key file"
5-
REF = "Checkout https://aleph-im.gitbook.io/aleph-js/api-resources-reference/posts"
5+
REF = "Item hash of the message to update"
66
SIGNABLE_MESSAGE = "Message to sign"
77
CUSTOM_DOMAIN_TARGET_TYPES = "IPFS|PROGRAM|INSTANCE"
88
CUSTOM_DOMAIN_OWNER_ADDRESS = "Owner address, default current account"
99
CUSTOM_DOMAIN_NAME = "Domain name. ex: aleph.im"
1010
CUSTOM_DOMAIN_ITEM_HASH = "Item hash"
1111
SKIP_VOLUME = "Skip prompt to attach more volumes"
12-
PERSISTENT_VOLUME = """Persistent volumes are allocated on the host machine and are not deleted when the VM is stopped.\n
13-
Requires at least "name", "persistence", "mount" and "size_mib". For more info, see the docs: https://docs.aleph.im/computing/volumes/persistent/\n
14-
Example: --persistent_volume name=data,persistence=host,size_mib=100,mount=/opt/data"""
15-
EPHEMERAL_VOLUME = """Ephemeral volumes are allocated on the host machine when the VM is started and deleted when the VM is stopped.\n
16-
Requires at least "name", "mount" and "size_mib".\n
17-
Example: --ephemeral-volume name=temp,size_mib=100,mount=/tmp/data"""
18-
IMMUTABLE_VOLUME = """Immutable volumes are pinned on the network and can be used by multiple VMs at the same time. They are read-only and useful for setting up libraries or other dependencies.\n
19-
Requires at least "name", "ref" (message hash) and "mount" path. "use_latest" is True by default, to use the latest version of the volume, if it has been amended. See the docs for more info: https://docs.aleph.im/computing/volumes/immutable/\n
20-
Example: --immutable-volume name=libs,ref=25a393222692c2f73489dc6710ae87605a96742ceef7b91de4d7ec34bb688d94,mount=/lib/python3.8/site-packages"""
12+
PERSISTENT_VOLUME = "Persistent volumes are allocated on the host machine and are not deleted when the VM is stopped.\nRequires at least `name`, `persistence`, `mount` and `size_mib`. For more info, see the docs: https://docs.aleph.im/computing/volumes/persistent/\nExample: --persistent_volume name=data,persistence=host,size_mib=100,mount=/opt/data"
13+
EPHEMERAL_VOLUME = "Ephemeral volumes are allocated on the host machine when the VM is started and deleted when the VM is stopped.\nRequires at least `name`, `mount` and `size_mib`.\nExample: --ephemeral-volume name=temp,size_mib=100,mount=/tmp/data"
14+
IMMUTABLE_VOLUME = "Immutable volumes are pinned on the network and can be used by multiple VMs at the same time. They are read-only and useful for setting up libraries or other dependencies.\nRequires at least `name`, `ref` (message hash) and `mount` path. `use_latest` is True by default, to use the latest version of the volume, if it has been amended. See the docs for more info: https://docs.aleph.im/computing/volumes/immutable/\nExample: --immutable-volume name=libs,ref=25a3...8d94,mount=/lib/python3.11/site-packages"
15+
SKIP_ENV_VAR = "Skip prompt to set environment variables"
16+
ENVIRONMENT_VARIABLES = "Environment variables to pass. They will be public and visible in the message, so don't include secrets. Must be a comma separated list. Example: `KEY=value` or `KEY=value,KEY=value`"
2117
ASK_FOR_CONFIRMATION = "Prompt user for confirmation"
2218
IPFS_CATCH_ALL_PATH = "Choose a relative path to catch all unmatched route or a 404 error"
2319
PAYMENT_TYPE = "Payment method, either holding tokens, NFTs, or Pay-As-You-Go via token streaming"
@@ -46,7 +42,7 @@
4642
VM_ID = "Item hash of your VM. If provided, skip the instance creation, else create a new one"
4743
VM_NOT_READY = "VM not initialized/started"
4844
VM_SCHEDULED = "VM scheduled but not available yet"
49-
VM_NOT_AVAILABLE_YET = "VM not available yet"
45+
CRN_UNKNOWN = "Unknown"
5046
CRN_PENDING = "Pending..."
5147
ALLOCATION_AUTO = "Auto - Scheduler"
5248
ALLOCATION_MANUAL = "Manual - Selection"
@@ -56,3 +52,12 @@
5652
ADDRESS_CHAIN = "Chain for the address"
5753
CREATE_REPLACE = "Overwrites private key file if it already exists"
5854
CREATE_ACTIVE = "Loads the new private key after creation"
55+
PROMPT_CRN_URL = "URL of the CRN (Compute node) on which the instance is running"
56+
PROMPT_PROGRAM_CRN_URL = "URL of the CRN (Compute node) on which the program is running"
57+
PROGRAM_PATH = "Path to your source code. Can be a directory, a .squashfs file or a .zip archive"
58+
PROGRAM_ENTRYPOINT = "Your program entrypoint. Example: `main:app` for Python programs, else `run.sh` for a script containing your launch command"
59+
PROGRAM_RUNTIME = "Hash of the runtime to use for your program. You can also create your own runtime and pin it. Currently defaults to `{runtime_id}` (Use `aleph program runtime-checker` to inspect it)"
60+
PROGRAM_BETA = "If true, you will be prompted to add message subscriptions to your program"
61+
PROGRAM_UPDATABLE = "Allow program updates. By default, only the source code can be modified without requiring redeployement (same item hash). When enabled (set to True), this option allows to update any other field. However, such modifications will require a program redeployment (new item hash)"
62+
PROGRAM_KEEP_CODE = "Keep the source code intact instead of deleting it"
63+
PROGRAM_KEEP_PREV = "Keep the previous program intact instead of deleting it"

0 commit comments

Comments
 (0)