From e1c105728dc12de9615f5e694f95e3d08f19e4f9 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:49:38 +0100 Subject: [PATCH 01/13] Feature: Node (compute & core) Solutions: - aleph node core -> show all core node - aleph node compute -> show all compute node --- setup.cfg | 1 + src/aleph_client/__main__.py | 15 +-- src/aleph_client/commands/node.py | 151 ++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 src/aleph_client/commands/node.py diff --git a/setup.cfg b/setup.cfg index 6be539b7..3eefe378 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ install_requires = eth_account>=0.4.0 python-magic pygments + rich # The usage of test_requires is discouraged, see `Dependency Management` docs # tests_require = pytest; pytest-cov # Require a specific Python version, e.g. Python 2.7 or >= 3.4 diff --git a/src/aleph_client/__main__.py b/src/aleph_client/__main__.py index 5300d6e0..7a29bdeb 100644 --- a/src/aleph_client/__main__.py +++ b/src/aleph_client/__main__.py @@ -5,7 +5,7 @@ import typer from aleph_client.utils import AsyncTyper -from .commands import about, account, aggregate, files, message, program +from .commands import about, account, aggregate, files, message, program, node app = AsyncTyper() @@ -13,8 +13,12 @@ @app.callback() def common( ctx: typer.Context, - version: bool = typer.Option(None, "--version", callback=about.get_version, help="Show Aleph CLI Version"), - v: bool = typer.Option(None, "-v", callback=about.get_version, help="Show Aleph CLI Version"), + version: bool = typer.Option( + None, "--version", callback=about.get_version, help="Show Aleph CLI Version" + ), + v: bool = typer.Option( + None, "-v", callback=about.get_version, help="Show Aleph CLI Version" + ), ): pass @@ -34,10 +38,9 @@ def common( app.add_typer( program.app, name="program", help="Upload and update programs on aleph.im VM" ) -app.add_typer( - about.app, name="about", help="Display the informations of Aleph CLI" -) +app.add_typer(about.app, name="about", help="Display the informations of Aleph CLI") +app.add_typer(node.app, name="node", help="Get node info on aleph.im network") if __name__ == "__main__": app() diff --git a/src/aleph_client/commands/node.py b/src/aleph_client/commands/node.py new file mode 100644 index 00000000..edd55c69 --- /dev/null +++ b/src/aleph_client/commands/node.py @@ -0,0 +1,151 @@ +import datetime +import logging +import re +import unicodedata + +import requests +import typer +from rich import text +from rich.console import Console +from rich.markup import escape +from rich.table import Table + +from aleph_client.commands.utils import setup_logging + +logger = logging.getLogger(__name__) +app = typer.Typer() + +node_link = "https://api2.aleph.im/api/v0/aggregates/0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10.json?keys=corechannel" + + +class NodeInfo: + def __init__(self, **kwargs): + self.data = kwargs.get("data", {}) + self.nodes = self.data.get("corechannel", {}).get("resource_nodes", []) + self.nodes.sort(key=lambda x: x.get("score", 0), reverse=True) + self.core_node = self.data.get("corechannel", {}).get("nodes", []) + self.core_node.sort(key=lambda x: x.get("score", 0), reverse=True) + + +# Fetch node aggregates and format it inside class +def _fetch_nodes() -> NodeInfo: + response = requests.get(node_link) + return NodeInfo(**response.json()) + + +def _escape_and_normalize(string: str) -> str: + sanitized_text = escape(string) + normalized_text = unicodedata.normalize("NFC", sanitized_text) + return normalized_text + + +def _remove_ansi_escape(string: str) -> str: + ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") + return ansi_escape.sub("", string) + + +def _format_score(score): + if score < 0.5: + return text.Text(f"{score:.2%}", style="red", justify="right") + elif score < 0.75: + return text.Text(f"{score:.2%}", style="orange", justify="right") + else: + return text.Text(f"{score:.2%}", style="green", justify="right") + + +def _format_status(status): + if status.lower() == "linked" or status.lower() == "active": + return text.Text(status, style="green", justify="left") + return text.Text(status, style="red", justify="left") + + +def _show_compute(node_info): + table = Table(title="Compute Node Information") + table.add_column("Score", style="green", no_wrap=True, justify="right") + table.add_column("Name", style="#029AFF", justify="left") + table.add_column("Creation Time", style="#029AFF", justify="center") + table.add_column("Decentralization", style="green", justify="right") + table.add_column("Status", style="green", justify="right") + + for node in node_info.nodes: + # Prevent escaping with name + node_name = node["name"] + node_name = _escape_and_normalize(node_name) + node_name = _remove_ansi_escape(node_name) + + # Format Value + creation_time = datetime.datetime.fromtimestamp(node["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + score = _format_score(node["score"]) + decentralization = _format_score(node["decentralization"]) + status = _format_status(node["status"]) + + table.add_row( + score, + node_name, + creation_time, + decentralization, + status, + ) + + console = Console() + console.print(table) + + +def _show_core(node_info): + table = Table(title="Core Channel Node Information") + table.add_column("Score", style="green", no_wrap=True, justify="right") + table.add_column("Name", style="#029AFF", justify="left") + table.add_column("Staked", style="#029AFF", justify="left") + table.add_column("Linked", style="#029AFF", justify="left") + table.add_column("Creation Time", style="#029AFF", justify="center") + table.add_column("Status", style="green", justify="right") + + for node in node_info.core_node: + # Prevent escaping with name + node_name = node["name"] + node_name = _escape_and_normalize(node_name) + node_name = _remove_ansi_escape(node_name) + + # Format Value + creation_time = datetime.datetime.fromtimestamp(node["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + score = _format_score(node["score"]) + status = _format_status(node["status"]) + + table.add_row( + score, + node_name, + f"{int(node['total_staked']):,}", + str(len(node["resource_nodes"])), + creation_time, + status, + ) + + console = Console() + console.print(table) + + +@app.command() +def compute( + debug: bool = False, +): + """Get all compute node on aleph network""" + + setup_logging(debug) + + compute_info: NodeInfo = _fetch_nodes() + _show_compute(compute_info) + + +@app.command() +def core( + debug: bool = False, +): + """Get all core node on aleph""" + setup_logging(debug) + + core_info: NodeInfo = _fetch_nodes() + _show_core(core_info) From 78ab64c1694a20326fb199dfd05f5ae7f6fe5c8b Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:55:47 +0100 Subject: [PATCH 02/13] Feature: Balance Solutions: - aleph account balance -> get current user balance - aleph account balance --address {add} -> get specific address balance --- src/aleph_client/commands/account.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 5be5c1f1..459a2dbd 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -1,10 +1,13 @@ import asyncio import base64 +import json import logging import sys from pathlib import Path from typing import Optional +import requests + import typer from aleph.sdk.account import _load_account from aleph.sdk.chains.common import generate_key @@ -143,3 +146,35 @@ def sign_bytes( coroutine = account.sign_raw(message.encode()) signature = asyncio.run(coroutine) typer.echo(signature.hex()) + +@app.command() +def balance( + address: Optional[str] = typer.Option(None, help="Address"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), +): + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + if account and not address: + address = account.get_address() + + if address: + uri = f"{sdk_settings.API_HOST}/api/v0/addresses/{address}/balance" + + with requests.get(uri) as response: + if response.status_code == 200: + balance_data = response.json() + formatted_balance_data = json.dumps(balance_data, indent=4) + typer.echo(formatted_balance_data) + else: + typer.echo( + f"Failed to retrieve balance for address {address}. Status code: {response.status_code}" + ) + else: + typer.echo( + "Error: Please provide either a private key, private key file, or an address." + ) \ No newline at end of file From 7952c571b380c91ee849a50efe06abd2a2a67315 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:09:31 +0100 Subject: [PATCH 03/13] Feature: File List & Forget Solutions: - aleph file list - aleph file forget --- src/aleph_client/commands/files.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 663407d8..96470055 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -1,7 +1,9 @@ +import json import logging from pathlib import Path from typing import Optional +import requests import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.account import _load_account @@ -9,6 +11,7 @@ from aleph.sdk.types import AccountFromPrivateKey, StorageEnum from aleph_message.models import StoreMessage from aleph_message.status import MessageStatus +from pydantic import BaseModel, Field from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging @@ -132,3 +135,86 @@ async def download( await client.download_file_ipfs_to_buffer(hash, fd) logger.debug("File downloaded successfully.") + + +@app.command() +async def forget( + item_hash: str = typer.Argument(..., help="Hash to forget"), + reason: str = typer.Argument(..., help="reason to forget"), + channel: Optional[str] = typer.Option(None, help="channel"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), + debug: bool = False, +): + """forget a file and his message on aleph.im.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + async with AuthenticatedAlephHttpClient( + account=account, api_server=sdk_settings.API_HOST + ) as client: + value = await client.forget(hashes=[item_hash], reason=reason, channel=channel) + typer.echo(f"{value[0].json(indent=4)}") + + +class GetAccountFilesQueryParams(BaseModel): + pagination: int = Field( + default=100, + ge=0, + description="Maximum number of files to return. Specifying 0 removes this limit.", + ) + page: int = Field(default=1, ge=1, description="Offset in pages. Starts at 1.") + sort_order: int = Field( + default=-1, + description="Order in which files should be listed: -1 means most recent messages first, 1 means older messages first.", + ) + + +# Your list command +@app.command() +def list( + address: Optional[str] = typer.Option(None, help="Address"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), + pagination: int = typer.Option(100, help="Maximum number of files to return."), + page: int = typer.Option(1, help="Offset in pages."), + sort_order: int = typer.Option( + -1, + help="Order in which files should be listed: -1 means most recent messages first, 1 means older messages first.", + ), +): + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + if account and not address: + address = account.get_address() + + if address: + # Build the query parameters + query_params = GetAccountFilesQueryParams( + pagination=pagination, page=page, sort_order=sort_order + ) + + uri = f"{sdk_settings.API_HOST}/api/v0/addresses/{address}/files" + with requests.get(uri, params=query_params.dict()) as response: + if response.status_code == 200: + balance_data = response.json() + formatted_balance_data = json.dumps(balance_data, indent=4) + typer.echo(formatted_balance_data) + else: + typer.echo( + f"Failed to retrieve files for address {address}. Status code: {response.status_code}" + ) + else: + typer.echo( + "Error: Please provide either a private key, private key file, or an address." + ) From 61b9d38556e98aae348ef1a484d37a67b80e9daa Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:26:43 +0100 Subject: [PATCH 04/13] Feature: Aggregate (post get amend) Solutions: - aleph aggregate post - aleph aggregate amend - aleph aggregate get --- src/aleph_client/commands/aggregate.py | 135 ++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index 7636dfcd..9f084a0d 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -1,12 +1,13 @@ +import json from pathlib import Path -from typing import Optional +from typing import Any, Mapping, Optional import typer from aleph.sdk.account import _load_account from aleph.sdk.client import AuthenticatedAlephHttpClient from aleph.sdk.conf import settings as sdk_settings -from aleph.sdk.types import AccountFromPrivateKey from aleph.sdk.query.filters import MessageFilter +from aleph.sdk.types import AccountFromPrivateKey from aleph_message.models import MessageType from aleph_client.commands import help_strings @@ -15,6 +16,8 @@ app = AsyncTyper() +from aleph_client.commands.utils import colorful_message_json + @app.command() async def forget( @@ -50,3 +53,131 @@ async def forget( hash_list = [message["item_hash"] for message in message_response.messages] await client.forget(hashes=hash_list, reason=reason, channel=channel) + + +@app.command() +async def post( + key: str = typer.Argument(..., help="Aggregate key to be created."), + content: str = typer.Argument( + ..., help="Aggregate content (ex : {'c': 3, 'd': 4})" + ), + address: Optional[str] = typer.Option(default=None, help="address"), + channel: Optional[str] = typer.Option(default=None, help=help_strings.CHANNEL), + inline: Optional[bool] = typer.Option(False, help="inline"), + sync: Optional[bool] = typer.Option(False, help="Sync response"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), + debug: bool = False, +): + """Create an Aggregate""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + try: + content_dict = json.loads(content) + except json.JSONDecodeError: + typer.echo("Invalid JSON for content. Please provide valid JSON.") + raise typer.Exit(1) + + async with AuthenticatedAlephHttpClient( + account=account, api_server=sdk_settings.API_HOST + ) as client: + message, _ = await client.create_aggregate( + key=key, + content=content_dict, + channel=channel, + sync=sync, + inline=inline, + address=address, + ) + log_message = json.dumps(message.dict(), indent=4) + typer.echo(log_message) + + +@app.command() +async def get( + key: str = typer.Argument(..., help="Aggregate key to be fetched."), + address: Optional[str] = typer.Option(default=None, help="Address"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), + debug: bool = False, +): + """Fetch an aggregate by key and content.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + # if no address we load current account as a private key + address = account.get_address() if address is None else address + + async with AuthenticatedAlephHttpClient( + account=account, api_server=sdk_settings.API_HOST + ) as client: + aggregates = await client.fetch_aggregate(address=address, key=key) + + if aggregates: + typer.echo(json.dumps(aggregates, indent=4)) + else: + typer.echo("No aggregates found for the given key and content.") + + +@app.command() +async def amend( + key: str = typer.Argument(..., help="Aggregate key to be ammend."), + content: str = typer.Argument( + ..., help="Aggregate content (ex : {'a': 1, 'b': 2})" + ), + address: Optional[str] = typer.Option(default=None, help="address"), + channel: Optional[str] = typer.Option(default=None, help=help_strings.CHANNEL), + inline: Optional[bool] = typer.Option(False, help="inline"), + sync: Optional[bool] = typer.Option(False, help="Sync response"), + private_key: Optional[str] = typer.Option( + sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), + debug: bool = False, +): + """Update an Aggregate""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + try: + content_dict = json.loads(content) + except json.JSONDecodeError: + typer.echo("Invalid JSON for content. Please provide valid JSON.") + raise typer.Exit(1) + + async with AuthenticatedAlephHttpClient( + account=account, api_server=sdk_settings.API_HOST + ) as client: + # Fetch aggregates to check if the key is already present + aggregates = await client.fetch_aggregate(address=address, key=key) + + if aggregates: + message, _ = await client.create_aggregate( + key=key, + content=content_dict, + channel=channel, + sync=sync, + inline=inline, + address=address, + ) + + typer.echo(colorful_message_json(message)) + else: + typer.echo("No aggregates found for the given key and content.") From 6beaaf12949c2634115496f142e0f62bebf89257 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:45:07 +0100 Subject: [PATCH 05/13] Fix: mypy Solution: - Add types-requests lib --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 3eefe378..c7fe582e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ testing = # httpx is required in tests by fastapi.testclient httpx requests + types-requests aleph-pytezos==0.1.0 types-setuptools mqtt = From 8a991761a31675f5fa38db0520c6a15703785e2c Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:49:30 +0100 Subject: [PATCH 06/13] Update src/aleph_client/commands/files.py Co-authored-by: Mike Hukiewitz <70762838+MHHukiewitz@users.noreply.github.com> --- src/aleph_client/commands/files.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 96470055..2f6c3564 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -176,7 +176,6 @@ class GetAccountFilesQueryParams(BaseModel): ) -# Your list command @app.command() def list( address: Optional[str] = typer.Option(None, help="Address"), From 08cff3ff995cbae9fa1438039238fe352847eae2 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:49:50 +0100 Subject: [PATCH 07/13] Update src/aleph_client/commands/node.py Co-authored-by: Mike Hukiewitz <70762838+MHHukiewitz@users.noreply.github.com> --- src/aleph_client/commands/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph_client/commands/node.py b/src/aleph_client/commands/node.py index edd55c69..c282c6c7 100644 --- a/src/aleph_client/commands/node.py +++ b/src/aleph_client/commands/node.py @@ -27,8 +27,8 @@ def __init__(self, **kwargs): self.core_node.sort(key=lambda x: x.get("score", 0), reverse=True) -# Fetch node aggregates and format it inside class def _fetch_nodes() -> NodeInfo: +""" Fetch node aggregates and format it as NodeInfo """ response = requests.get(node_link) return NodeInfo(**response.json()) From 5bd0541382efc3b08ffbea6599bd2117baf1bbcd Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:50:10 +0100 Subject: [PATCH 08/13] Update src/aleph_client/commands/files.py Co-authored-by: Mike Hukiewitz <70762838+MHHukiewitz@users.noreply.github.com> --- src/aleph_client/commands/files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 2f6c3564..b5aa7060 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -206,9 +206,9 @@ def list( uri = f"{sdk_settings.API_HOST}/api/v0/addresses/{address}/files" with requests.get(uri, params=query_params.dict()) as response: if response.status_code == 200: - balance_data = response.json() - formatted_balance_data = json.dumps(balance_data, indent=4) - typer.echo(formatted_balance_data) + files_data = response.json() + formatted_files_data = json.dumps(files_data, indent=4) + typer.echo(formatted_files_data) else: typer.echo( f"Failed to retrieve files for address {address}. Status code: {response.status_code}" From 22b6290e0493400fe4a8a80dce47ba63e8b92c18 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:51:57 +0100 Subject: [PATCH 09/13] Refactor: Remove Aggregate Amend command Solutions: - Amend command wasn't needed (can simply use post to update an aggregate) --- src/aleph_client/commands/aggregate.py | 51 +------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index 9f084a0d..c7d41fe8 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -73,7 +73,7 @@ async def post( ), debug: bool = False, ): - """Create an Aggregate""" + """Create or Update aggregate""" setup_logging(debug) @@ -132,52 +132,3 @@ async def get( typer.echo("No aggregates found for the given key and content.") -@app.command() -async def amend( - key: str = typer.Argument(..., help="Aggregate key to be ammend."), - content: str = typer.Argument( - ..., help="Aggregate content (ex : {'a': 1, 'b': 2})" - ), - address: Optional[str] = typer.Option(default=None, help="address"), - channel: Optional[str] = typer.Option(default=None, help=help_strings.CHANNEL), - inline: Optional[bool] = typer.Option(False, help="inline"), - sync: Optional[bool] = typer.Option(False, help="Sync response"), - private_key: Optional[str] = typer.Option( - sdk_settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY - ), - private_key_file: Optional[Path] = typer.Option( - sdk_settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE - ), - debug: bool = False, -): - """Update an Aggregate""" - - setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - try: - content_dict = json.loads(content) - except json.JSONDecodeError: - typer.echo("Invalid JSON for content. Please provide valid JSON.") - raise typer.Exit(1) - - async with AuthenticatedAlephHttpClient( - account=account, api_server=sdk_settings.API_HOST - ) as client: - # Fetch aggregates to check if the key is already present - aggregates = await client.fetch_aggregate(address=address, key=key) - - if aggregates: - message, _ = await client.create_aggregate( - key=key, - content=content_dict, - channel=channel, - sync=sync, - inline=inline, - address=address, - ) - - typer.echo(colorful_message_json(message)) - else: - typer.echo("No aggregates found for the given key and content.") From 580d3d84cd983ae8fb479904780b0b39a5cf6805 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:53:24 +0100 Subject: [PATCH 10/13] Fix: mypy --- src/aleph_client/commands/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph_client/commands/node.py b/src/aleph_client/commands/node.py index c282c6c7..b7177f42 100644 --- a/src/aleph_client/commands/node.py +++ b/src/aleph_client/commands/node.py @@ -28,7 +28,7 @@ def __init__(self, **kwargs): def _fetch_nodes() -> NodeInfo: -""" Fetch node aggregates and format it as NodeInfo """ + """ Fetch node aggregates and format it as NodeInfo """ response = requests.get(node_link) return NodeInfo(**response.json()) From c011d0a8360751f9cfa56dc6dd15295ce3d3af9e Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:59:08 +0100 Subject: [PATCH 11/13] Refactor: aleph file list Solutions: - Add a Rich Table for a nicer output. - Allow the user to use --export-json to get JSON instead of a rich table. --- src/aleph_client/commands/files.py | 59 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index b5aa7060..4edae7ec 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -1,5 +1,6 @@ import json import logging +from datetime import datetime from pathlib import Path from typing import Optional @@ -12,6 +13,9 @@ from aleph_message.models import StoreMessage from aleph_message.status import MessageStatus from pydantic import BaseModel, Field +from rich import box +from rich.console import Console +from rich.table import Table from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging @@ -176,6 +180,52 @@ class GetAccountFilesQueryParams(BaseModel): ) +def _show_files(files_data: dict) -> None: + table = Table(title="Files Information", box=box.SIMPLE_HEAVY) + table.add_column("File Hash", style="cyan", no_wrap=True, min_width=None) + table.add_column("Size (MB)", style="magenta", min_width=None) + table.add_column("Type", style="green", min_width=None) + table.add_column("Created", style="blue", min_width=None) + table.add_column("Item Hash", style="yellow", min_width=None, no_wrap=True) + + console = Console() + + # Add files to the table + for file_info in files_data["files"]: + created = datetime.strptime(file_info["created"], "%Y-%m-%dT%H:%M:%S.%f%z") + formatted_created = created.strftime("%Y-%m-%d %H:%M:%S") + size_in_mb = float(file_info["size"]) / (1024 * 1024) + table.add_row( + file_info["file_hash"], + f"{size_in_mb:.4f} MB", + file_info["type"], + formatted_created, + file_info["item_hash"], + ) + + pagination_page = files_data["pagination_page"] + pagination_total = files_data["pagination_total"] + pagination_per_page = files_data["pagination_per_page"] + address = files_data["address"] + total_size = float(files_data["total_size"]) / (1024 * 1024) + + console.print( + f"\n[bold]Address:[/bold] {address}", + ) + console.print(f"[bold]Total Size:[/bold] ~ {total_size:.4f} MB") + + console.print("\n[bold]Pagination:[/bold]") + console.print( + f"[bold]Page:[/bold] {pagination_page}", + ) + console.print( + f"[bold]Total Pages:[/bold] {pagination_total}", + ) + console.print(f"[bold]Items Per Page:[/bold] {pagination_per_page}") + + console.print(table) + + @app.command() def list( address: Optional[str] = typer.Option(None, help="Address"), @@ -191,7 +241,11 @@ def list( -1, help="Order in which files should be listed: -1 means most recent messages first, 1 means older messages first.", ), + export_json: bool = typer.Option( + default=False, help="Print as json instead of rich table" + ), ): + """List all files for a given address""" account: AccountFromPrivateKey = _load_account(private_key, private_key_file) if account and not address: @@ -208,7 +262,10 @@ def list( if response.status_code == 200: files_data = response.json() formatted_files_data = json.dumps(files_data, indent=4) - typer.echo(formatted_files_data) + if not export_json: + _show_files(files_data) + else: + typer.echo(formatted_files_data) else: typer.echo( f"Failed to retrieve files for address {address}. Status code: {response.status_code}" From 6400dc10a8e7305549138d3bf4ed7a08394c9b1e Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:13:49 +0100 Subject: [PATCH 12/13] Refactor: aleph files list Solutions: - Replace --export_json by --json for more simple use --- src/aleph_client/commands/files.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 4edae7ec..22526106 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -1,4 +1,4 @@ -import json +import json as json_lib import logging from datetime import datetime from pathlib import Path @@ -241,7 +241,7 @@ def list( -1, help="Order in which files should be listed: -1 means most recent messages first, 1 means older messages first.", ), - export_json: bool = typer.Option( + json: bool = typer.Option( default=False, help="Print as json instead of rich table" ), ): @@ -261,8 +261,8 @@ def list( with requests.get(uri, params=query_params.dict()) as response: if response.status_code == 200: files_data = response.json() - formatted_files_data = json.dumps(files_data, indent=4) - if not export_json: + formatted_files_data = json_lib.dumps(files_data, indent=4) + if not json: _show_files(files_data) else: typer.echo(formatted_files_data) From 499bce61bfbba823bbb1870fd573be0951d2a76f Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:20:35 +0100 Subject: [PATCH 13/13] Refactor: Node commands Solutions: -Allow user to export as json instead of rich table using --json --- src/aleph_client/commands/node.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/aleph_client/commands/node.py b/src/aleph_client/commands/node.py index b7177f42..fed214e1 100644 --- a/src/aleph_client/commands/node.py +++ b/src/aleph_client/commands/node.py @@ -1,4 +1,5 @@ import datetime +import json as json_lib import logging import re import unicodedata @@ -28,7 +29,7 @@ def __init__(self, **kwargs): def _fetch_nodes() -> NodeInfo: - """ Fetch node aggregates and format it as NodeInfo """ + """Fetch node aggregates and format it as NodeInfo""" response = requests.get(node_link) return NodeInfo(**response.json()) @@ -130,6 +131,9 @@ def _show_core(node_info): @app.command() def compute( + json: bool = typer.Option( + default=False, help="Print as json instead of rich table" + ), debug: bool = False, ): """Get all compute node on aleph network""" @@ -137,15 +141,24 @@ def compute( setup_logging(debug) compute_info: NodeInfo = _fetch_nodes() - _show_compute(compute_info) + if not json: + _show_compute(compute_info) + else: + typer.echo(json_lib.dumps(compute_info.nodes, indent=4)) @app.command() def core( + json: bool = typer.Option( + default=False, help="Print as json instead of rich table" + ), debug: bool = False, ): """Get all core node on aleph""" setup_logging(debug) core_info: NodeInfo = _fetch_nodes() - _show_core(core_info) + if not json: + _show_core(core_info) + else: + typer.echo(json_lib.dumps(core_info.core_node, indent=4))