Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add account, aggregate, node, file essential Functionality #177

Merged
merged 13 commits into from
Nov 16, 2023
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,6 +71,7 @@ testing =
# httpx is required in tests by fastapi.testclient
httpx
requests
types-requests
aleph-pytezos==0.1.0
types-setuptools
mqtt =
Expand Down
15 changes: 9 additions & 6 deletions src/aleph_client/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
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()


@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

Expand All @@ -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()
35 changes: 35 additions & 0 deletions src/aleph_client/commands/account.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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."
)
86 changes: 84 additions & 2 deletions src/aleph_client/commands/aggregate.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +16,8 @@

app = AsyncTyper()

from aleph_client.commands.utils import colorful_message_json


@app.command()
async def forget(
Expand Down Expand Up @@ -50,3 +53,82 @@ 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 or Update 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.")


142 changes: 142 additions & 0 deletions src/aleph_client/commands/files.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import json as json_lib
import logging
from datetime import datetime
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
from aleph.sdk.conf import settings as sdk_settings
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 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
Expand Down Expand Up @@ -132,3 +139,138 @@ 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.",
)


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"),
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.",
),
json: bool = typer.Option(
default=False, help="Print as json instead of rich table"
),
):
MHHukiewitz marked this conversation as resolved.
Show resolved Hide resolved
"""List all files for a given address"""
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:
files_data = response.json()
formatted_files_data = json_lib.dumps(files_data, indent=4)
if not 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}"
)
else:
typer.echo(
"Error: Please provide either a private key, private key file, or an address."
)
Loading
Loading