From 96347e2e10cef721d41e7403cad10cf6b12cd6c6 Mon Sep 17 00:00:00 2001 From: AmozPay Date: Thu, 6 Oct 2022 10:23:14 +0200 Subject: [PATCH 1/3] refacto: commands were cluttered + no cli help Solution: put all commands in a sub folder and use typer subcommands Use typer.Option and typer.Argument to document each command parameter --- src/aleph_client/__main__.py | 592 +--------------------- src/aleph_client/commands/aggregate.py | 40 ++ src/aleph_client/commands/files.py | 100 ++++ src/aleph_client/commands/help_strings.py | 5 + src/aleph_client/commands/message.py | 192 +++++++ src/aleph_client/commands/program.py | 263 ++++++++++ src/aleph_client/commands/utils.py | 69 +++ 7 files changed, 689 insertions(+), 572 deletions(-) create mode 100644 src/aleph_client/commands/aggregate.py create mode 100644 src/aleph_client/commands/files.py create mode 100644 src/aleph_client/commands/help_strings.py create mode 100644 src/aleph_client/commands/message.py create mode 100644 src/aleph_client/commands/program.py create mode 100644 src/aleph_client/commands/utils.py diff --git a/src/aleph_client/__main__.py b/src/aleph_client/__main__.py index 9506065b..6711339e 100644 --- a/src/aleph_client/__main__.py +++ b/src/aleph_client/__main__.py @@ -1,595 +1,43 @@ -"""Aleph Client command-line interface. """ -import asyncio -import json -import logging -import os.path -import subprocess -import tempfile -from base64 import b32encode, b16decode -from enum import Enum +Aleph Client command-line interface. +""" + +from typing import Optional from pathlib import Path -from typing import Optional, Dict, List -from zipfile import BadZipFile import typer -from aleph_message.models import ( - ProgramMessage, - StoreMessage, - MessageType, - PostMessage, - ForgetMessage, - AlephMessage, - MessagesResponse, - ProgramContent, -) -from typer import echo -from aleph_client.account import _load_account from aleph_client.types import AccountFromPrivateKey -from aleph_client.utils import create_archive -from . import synchronous -from .asynchronous import ( - get_fallback_session, - StorageEnum, +from aleph_client.account import _load_account +from aleph_client.conf import settings +from .commands import ( + files, + message, + program, + help_strings, + aggregate ) -from .conf import settings - -logger = logging.getLogger(__name__) -app = typer.Typer() - -class KindEnum(str, Enum): - json = "json" -def _input_multiline() -> str: - """Prompt the user for a multiline input.""" - echo("Enter/Paste your content. Ctrl-D or Ctrl-Z ( windows ) to save it.") - contents = "" - while True: - try: - line = input() - except EOFError: - break - contents += line + "\n" - return contents - - -def _setup_logging(debug: bool = False): - level = logging.DEBUG if debug else logging.WARNING - logging.basicConfig(level=level) +app = typer.Typer() +app.add_typer(files.app, name="file", help="File uploading and pinning on IPFS and Aleph.im") +app.add_typer(message.app, name="message", help="Post, amend, watch and forget messages on Aleph.im") +app.add_typer(program.app, name="program", help="Upload and update programs on Aleph's VM") +app.add_typer(aggregate.app, name="aggregate", help="Manage aggregate messages on Aleph.im") @app.command() def whoami( - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), ): """ Display your public address. """ account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - echo(account.get_public_key()) - - -@app.command() -def post( - path: Optional[Path] = None, - type: str = "test", - ref: Optional[str] = None, - channel: str = settings.DEFAULT_CHANNEL, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - debug: bool = False, -): - """Post a message on Aleph.im.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - storage_engine: str - content: Dict - - if path: - if not path.is_file(): - echo(f"Error: File not found: '{path}'") - raise typer.Exit(code=1) - - file_size = os.path.getsize(path) - storage_engine = ( - StorageEnum.ipfs if file_size > 4 * 1024 * 1024 else StorageEnum.storage - ) - - with open(path, "r") as fd: - content = json.load(fd) - - else: - content_raw = _input_multiline() - storage_engine = ( - StorageEnum.ipfs - if len(content_raw) > 4 * 1024 * 1024 - else StorageEnum.storage - ) - try: - content = json.loads(content_raw) - except json.decoder.JSONDecodeError: - echo("Not valid JSON") - raise typer.Exit(code=2) - - try: - result: PostMessage = synchronous.create_post( - account=account, - post_content=content, - post_type=type, - ref=ref, - channel=channel, - inline=True, - storage_engine=storage_engine, - ) - echo(result.json(indent=4)) - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -@app.command() -def upload( - path: Path, - channel: str = settings.DEFAULT_CHANNEL, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - ref: Optional[str] = None, - debug: bool = False, -): - """Upload and store a file on Aleph.im.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - try: - if not path.is_file(): - echo(f"Error: File not found: '{path}'") - raise typer.Exit(code=1) - - with open(path, "rb") as fd: - logger.debug("Reading file") - # TODO: Read in lazy mode instead of copying everything in memory - file_content = fd.read() - storage_engine = ( - StorageEnum.ipfs - if len(file_content) > 4 * 1024 * 1024 - else StorageEnum.storage - ) - logger.debug("Uploading file") - result: StoreMessage = synchronous.create_store( - account=account, - file_content=file_content, - storage_engine=storage_engine, - channel=channel, - guess_mime_type=True, - ref=ref, - ) - logger.debug("Upload finished") - echo(f"{result.json(indent=4)}") - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -@app.command() -def pin( - hash: str, - channel: str = settings.DEFAULT_CHANNEL, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - ref: Optional[str] = None, - debug: bool = False, -): - """Persist a file from IPFS on Aleph.im.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - try: - result: StoreMessage = synchronous.create_store( - account=account, - file_hash=hash, - storage_engine=StorageEnum.ipfs, - channel=channel, - ref=ref, - ) - logger.debug("Upload finished") - echo(f"{result.json(indent=4)}") - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -def yes_no_input(text: str, default: Optional[bool] = None): - while True: - if default is True: - response = input(f"{text} [Y/n] ") - elif default is False: - response = input(f"{text} [y/N] ") - else: - response = input(f"{text} ") - - if response.lower() in ("y", "yes"): - return True - elif response.lower() in ("n", "no"): - return False - elif response == "" and default is not None: - return default - else: - if default is None: - echo("Please enter 'y', 'yes', 'n' or 'no'") - else: - echo("Please enter 'y', 'yes', 'n', 'no' or nothing") - continue - - -def _prompt_for_volumes(): - while yes_no_input("Add volume ?", default=False): - comment = input("Description: ") or None - mount = input("Mount: ") - persistent = yes_no_input("Persist on VM host ?", default=False) - if persistent: - name = input("Volume name: ") - size_mib = int(input("Size in MiB: ")) - yield { - "comment": comment, - "mount": mount, - "name": name, - "persistence": "host", - "size_mib": size_mib, - } - else: - ref = input("Ref: ") - use_latest = yes_no_input("Use latest version ?", default=True) - yield { - "comment": comment, - "mount": mount, - "ref": ref, - "use_latest": use_latest, - } - - -@app.command() -def program( - path: Path, - entrypoint: str, - channel: str = settings.DEFAULT_CHANNEL, - memory: int = settings.DEFAULT_VM_MEMORY, - vcpus: int = settings.DEFAULT_VM_VCPUS, - timeout_seconds: float = settings.DEFAULT_VM_TIMEOUT, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - print_messages: bool = False, - print_code_message: bool = False, - print_program_message: bool = False, - runtime: str = None, - beta: bool = False, - debug: bool = False, - persistent: bool = False, -): - """Register a program to run on Aleph.im virtual machines from a zip archive.""" - - _setup_logging(debug) - - path = path.absolute() - - try: - path_object, encoding = create_archive(path) - except BadZipFile: - echo("Invalid zip archive") - raise typer.Exit(3) - except FileNotFoundError: - echo("No such file or directory") - raise typer.Exit(4) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - runtime = ( - runtime - or input(f"Ref of runtime ? [{settings.DEFAULT_RUNTIME_ID}] ") - or settings.DEFAULT_RUNTIME_ID - ) - - volumes = [] - for volume in _prompt_for_volumes(): - volumes.append(volume) - echo("\n") - - subscriptions: Optional[List[Dict]] - if beta and yes_no_input("Subscribe to messages ?", default=False): - content_raw = _input_multiline() - try: - subscriptions = json.loads(content_raw) - except json.decoder.JSONDecodeError: - echo("Not valid JSON") - raise typer.Exit(code=2) - else: - subscriptions = None - - try: - # Upload the source code - with open(path_object, "rb") as fd: - logger.debug("Reading file") - # TODO: Read in lazy mode instead of copying everything in memory - file_content = fd.read() - storage_engine = ( - StorageEnum.ipfs - if len(file_content) > 4 * 1024 * 1024 - else StorageEnum.storage - ) - logger.debug("Uploading file") - user_code: StoreMessage = synchronous.create_store( - account=account, - file_content=file_content, - storage_engine=storage_engine, - channel=channel, - guess_mime_type=True, - ref=None, - ) - logger.debug("Upload finished") - if print_messages or print_code_message: - echo(f"{user_code.json(indent=4)}") - program_ref = user_code.item_hash - - # Register the program - result: ProgramMessage = synchronous.create_program( - account=account, - program_ref=program_ref, - entrypoint=entrypoint, - runtime=runtime, - storage_engine=StorageEnum.storage, - channel=channel, - memory=memory, - vcpus=vcpus, - timeout_seconds=timeout_seconds, - persistent=persistent, - encoding=encoding, - volumes=volumes, - subscriptions=subscriptions, - ) - logger.debug("Upload finished") - if print_messages or print_program_message: - echo(f"{result.json(indent=4)}") - - hash: str = result.item_hash - hash_base32 = b32encode(b16decode(hash.upper())).strip(b"=").lower().decode() - - echo( - f"Your program has been uploaded on Aleph .\n\n" - "Available on:\n" - f" {settings.VM_URL_PATH.format(hash=hash)}\n" - f" {settings.VM_URL_HOST.format(hash_base32=hash_base32)}\n" - "Visualise on:\n https://explorer.aleph.im/address/" - f"{result.chain}/{result.sender}/message/PROGRAM/{hash}\n" - ) - - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -@app.command() -def update( - hash: str, - path: Path, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - print_message: bool = True, - debug: bool = False, -): - """Update the code of an existing program""" - - _setup_logging(debug) - - account = _load_account(private_key, private_key_file) - path = path.absolute() - - try: - program_message: ProgramMessage = synchronous.get_message( - item_hash=hash, message_type=ProgramMessage - ) - code_ref = program_message.content.code.ref - code_message: StoreMessage = synchronous.get_message( - item_hash=code_ref, message_type=StoreMessage - ) - - try: - path, encoding = create_archive(path) - except BadZipFile: - echo("Invalid zip archive") - raise typer.Exit(3) - except FileNotFoundError: - echo("No such file or directory") - raise typer.Exit(4) - - if encoding != program_message.content.code.encoding: - logger.error( - f"Code must be encoded with the same encoding as the previous version " - f"('{encoding}' vs '{program_message.content.code.encoding}'" - ) - raise typer.Exit(1) - - # Upload the source code - with open(path, "rb") as fd: - logger.debug("Reading file") - # TODO: Read in lazy mode instead of copying everything in memory - file_content = fd.read() - logger.debug("Uploading file") - result = synchronous.create_store( - account=account, - file_content=file_content, - storage_engine=code_message.content.item_type, - channel=code_message.channel, - guess_mime_type=True, - ref=code_message.item_hash, - ) - logger.debug("Upload finished") - if print_message: - echo(f"{result.json(indent=4)}") - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -@app.command() -def unpersist( - hash: str, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - debug: bool = False, -): - """Stop a persistent virtual machine by making it non-persistent""" - - _setup_logging(debug) - - account = _load_account(private_key, private_key_file) - - existing: MessagesResponse = synchronous.get_messages(hashes=[hash]) - message: ProgramMessage = existing.messages[0] - content: ProgramContent = message.content.copy() - - content.on.persistent = False - content.replaces = message.item_hash - - result = synchronous.submit( - account=account, - content=content.dict(exclude_none=True), - message_type=message.type, - channel=message.channel, - ) - echo(f"{result.json(indent=4)}") - - -@app.command() -def amend( - hash: str, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - debug: bool = False, -): - """Amend an existing Aleph message.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - existing_message: AlephMessage = synchronous.get_message(item_hash=hash) - - editor: str = os.getenv("EDITOR", default="nano") - with tempfile.NamedTemporaryFile(suffix="json") as fd: - # Fill in message template - fd.write(existing_message.content.json(indent=4).encode()) - fd.seek(0) - - # Launch editor - subprocess.run([editor, fd.name], check=True) - - # Read new message - fd.seek(0) - new_content_json = fd.read() - - content_type = type(existing_message).__annotations__["content"] - new_content_dict = json.loads(new_content_json) - new_content = content_type(**new_content_dict) - new_content.ref = existing_message.item_hash - echo(new_content) - result = synchronous.submit( - account=account, - content=new_content.dict(), - message_type=existing_message.type, - channel=existing_message.channel, - ) - echo(f"{result.json(indent=4)}") - - -def forget_messages( - account: AccountFromPrivateKey, - hashes: List[str], - reason: Optional[str], - channel: str, -): - try: - result: ForgetMessage = synchronous.forget( - account=account, - hashes=hashes, - reason=reason, - channel=channel, - ) - echo(f"{result.json(indent=4)}") - finally: - # Prevent aiohttp unclosed connector warning - asyncio.run(get_fallback_session().close()) - - -@app.command() -def forget( - hashes: str, - reason: Optional[str] = None, - channel: str = settings.DEFAULT_CHANNEL, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - debug: bool = False, -): - """Forget an existing Aleph message.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - hash_list: List[str] = hashes.split(",") - forget_messages(account, hash_list, reason, channel) - - -@app.command() -def forget_aggregate( - key: str, - reason: Optional[str] = None, - channel: str = settings.DEFAULT_CHANNEL, - private_key: Optional[str] = settings.PRIVATE_KEY_STRING, - private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, - debug: bool = False, -): - """Forget all the messages composing an aggregate.""" - - _setup_logging(debug) - - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) - - message_response = synchronous.get_messages( - addresses=[account.get_address()], - message_type=MessageType.aggregate.value, - content_keys=[key], - ) - hash_list = [message["item_hash"] for message in message_response["messages"]] - forget_messages(account, hash_list, reason, channel) - - -@app.command() -def watch( - ref: str, - indent: Optional[int] = None, - debug: bool = False, -): - """Watch a hash for amends and print amend hashes""" - - _setup_logging(debug) - - original: AlephMessage = synchronous.get_message(item_hash=ref) - - for message in synchronous.watch_messages( - refs=[ref], addresses=[original.content.address] - ): - echo(f"{message.json(indent=indent)}") - + typer.echo(account.get_public_key()) if __name__ == "__main__": app() diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py new file mode 100644 index 00000000..ce9f3cc4 --- /dev/null +++ b/src/aleph_client/commands/aggregate.py @@ -0,0 +1,40 @@ +import typer +from typing import Optional +from aleph_client.types import AccountFromPrivateKey +from aleph_client.account import _load_account +from aleph_client.conf import settings +from pathlib import Path +from aleph_client import synchronous +from aleph_client.commands import help_strings + +from aleph_client.commands.message import forget_messages + +from aleph_client.commands.utils import setup_logging + +from aleph_message.models import MessageType + +app = typer.Typer() + +@app.command() +def forget( + key: str = typer.Argument(..., help="Aggregate item hash to be removed."), + reason: Optional[str] = typer.Option(None, help="A description of why the messages are being forgotten"), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help = help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help = help_strings.PRIVATE_KEY_FILE), + debug: bool = False, +): + """Forget all the messages composing an aggregate.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + message_response = synchronous.get_messages( + addresses=[account.get_address()], + message_type=MessageType.aggregate.value, + content_keys=[key], + ) + hash_list = [message["item_hash"] for message in message_response["messages"]] + forget_messages(account, hash_list, reason, channel) + diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py new file mode 100644 index 00000000..85864d0b --- /dev/null +++ b/src/aleph_client/commands/files.py @@ -0,0 +1,100 @@ +import typer +import logging +from typing import Optional +from aleph_client.types import AccountFromPrivateKey +from aleph_client.account import _load_account +from aleph_client.conf import settings +from pathlib import Path +import asyncio +from aleph_client import synchronous + +from aleph_client.commands import help_strings + +from aleph_client.asynchronous import ( + get_fallback_session, + StorageEnum, +) + +from aleph_client.commands.utils import setup_logging + +from aleph_message.models import StoreMessage + + +logger = logging.getLogger(__name__) +app = typer.Typer() + + +@app.command() +def pin( + hash: str = typer.Argument(..., help="IPFS hash to pin on Aleph.im"), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + ref: Optional[str] = typer.Option(None, help=help_strings.REF), + debug: bool = False, +): + """Persist a file from IPFS on Aleph.im.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + try: + result: StoreMessage = synchronous.create_store( + account=account, + file_hash=hash, + storage_engine=StorageEnum.ipfs, + channel=channel, + ref=ref, + ) + logger.debug("Upload finished") + typer.echo(f"{result.json(indent=4)}") + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) + + + +@app.command() +def upload( + path: Path = typer.Argument(..., help="Path of the file to upload"), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + ref: Optional[str] = typer.Option(None, help=help_strings.REF), + debug: bool = False, +): + """Upload and store a file on Aleph.im.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + try: + if not path.is_file(): + typer.echo(f"Error: File not found: '{path}'") + raise typer.Exit(code=1) + + with open(path, "rb") as fd: + logger.debug("Reading file") + # TODO: Read in lazy mode instead of copying everything in memory + file_content = fd.read() + storage_engine = ( + StorageEnum.ipfs + if len(file_content) > 4 * 1024 * 1024 + else StorageEnum.storage + ) + logger.debug("Uploading file") + result: StoreMessage = synchronous.create_store( + account=account, + file_content=file_content, + storage_engine=storage_engine, + channel=channel, + guess_mime_type=True, + ref=ref, + ) + logger.debug("Upload finished") + typer.echo(f"{result.json(indent=4)}") + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) diff --git a/src/aleph_client/commands/help_strings.py b/src/aleph_client/commands/help_strings.py new file mode 100644 index 00000000..76fd2dfc --- /dev/null +++ b/src/aleph_client/commands/help_strings.py @@ -0,0 +1,5 @@ +IPFS_HASH = "IPFS Content identifier (CID)" +CHANNEL = "Aleph network channel where the message is located" +PRIVATE_KEY = "Your private key. Cannot be used with --private-key-file" +PRIVATE_KEY_FILE = "Path to your private key file" +REF = "Checkout https://aleph-im.gitbook.io/aleph-js/api-resources-reference/posts" diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py new file mode 100644 index 00000000..2a6001c7 --- /dev/null +++ b/src/aleph_client/commands/message.py @@ -0,0 +1,192 @@ +import json +import os.path +import subprocess +from typing import Optional, Dict, List +from pathlib import Path +import tempfile +import asyncio + +import typer + +from aleph_message.models import ( + PostMessage, + ForgetMessage, + AlephMessage, +) + + +from aleph_client import synchronous +from aleph_client.commands import help_strings +from aleph_client.types import AccountFromPrivateKey +from aleph_client.account import _load_account +from aleph_client.conf import settings + +from aleph_client.asynchronous import ( + get_fallback_session, + StorageEnum, +) + +from aleph_client.commands.utils import ( + setup_logging, + input_multiline, +) + + +app = typer.Typer() + +@app.command() +def post( + path: Optional[Path] = typer.Option(None, help="Path to the content you want to post. If omitted, you can input your content directly"), + type: str = typer.Option("test", help="Text representing the message object type"), + ref: Optional[str] = typer.Option(None, help=help_strings.REF), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + debug: bool = False, +): + """Post a message on Aleph.im.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + storage_engine: str + content: Dict + + if path: + if not path.is_file(): + typer.echo(f"Error: File not found: '{path}'") + raise typer.Exit(code=1) + + file_size = os.path.getsize(path) + storage_engine = ( + StorageEnum.ipfs if file_size > 4 * 1024 * 1024 else StorageEnum.storage + ) + + with open(path, "r") as fd: + content = json.load(fd) + + else: + content_raw = input_multiline() + storage_engine = ( + StorageEnum.ipfs + if len(content_raw) > 4 * 1024 * 1024 + else StorageEnum.storage + ) + try: + content = json.loads(content_raw) + except json.decoder.JSONDecodeError: + typer.echo("Not valid JSON") + raise typer.Exit(code=2) + + try: + result: PostMessage = synchronous.create_post( + account=account, + post_content=content, + post_type=type, + ref=ref, + channel=channel, + inline=True, + storage_engine=storage_engine, + ) + typer.echo(result.json(indent=4)) + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) + + +@app.command() +def amend( + hash: str = typer.Argument(..., help="Hash reference of the message to amend"), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + debug: bool = False, +): + """Amend an existing Aleph message.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + existing_message: AlephMessage = synchronous.get_message(item_hash=hash) + + editor: str = os.getenv("EDITOR", default="nano") + with tempfile.NamedTemporaryFile(suffix="json") as fd: + # Fill in message template + fd.write(existing_message.content.json(indent=4).encode()) + fd.seek(0) + + # Launch editor + subprocess.run([editor, fd.name], check=True) + + # Read new message + fd.seek(0) + new_content_json = fd.read() + + content_type = type(existing_message).__annotations__["content"] + new_content_dict = json.loads(new_content_json) + new_content = content_type(**new_content_dict) + new_content.ref = existing_message.item_hash + typer.echo(new_content) + result = synchronous.submit( + account=account, + content=new_content.dict(), + message_type=existing_message.type, + channel=existing_message.channel, + ) + typer.echo(f"{result.json(indent=4)}") + + +def forget_messages( + account: AccountFromPrivateKey, + hashes: List[str], + reason: Optional[str], + channel: str, +): + try: + result: ForgetMessage = synchronous.forget( + account=account, + hashes=hashes, + reason=reason, + channel=channel, + ) + typer.echo(f"{result.json(indent=4)}") + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) + + +@app.command() +def forget( + hashes: str= typer.Argument(..., help="Comma separated list of hash references of messages to forget"), + reason: Optional[str] = typer.Option(None, help="A description of why the messages are being forgotten."), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + debug: bool = False, +): + """Forget an existing Aleph message.""" + + setup_logging(debug) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + hash_list: List[str] = hashes.split(",") + forget_messages(account, hash_list, reason, channel) + + +@app.command() +def watch( + ref: str = typer.Argument(..., help="Hash reference of the message to watch"), + indent: Optional[int] = typer.Option(None, help="Number of indents to use"), + debug: bool = False, +): + """Watch a hash for amends and print amend hashes""" + + setup_logging(debug) + + original: AlephMessage = synchronous.get_message(item_hash=ref) + + for message in synchronous.watch_messages( + refs=[ref], addresses=[original.content.address] + ): + typer.echo(f"{message.json(indent=indent)}") diff --git a/src/aleph_client/commands/program.py b/src/aleph_client/commands/program.py new file mode 100644 index 00000000..0b951236 --- /dev/null +++ b/src/aleph_client/commands/program.py @@ -0,0 +1,263 @@ +import typer +from typing import Optional, Dict, List +from aleph_client.types import AccountFromPrivateKey +from aleph_client.account import _load_account +from aleph_client.conf import settings +from pathlib import Path +import asyncio +from aleph_client import synchronous +import json +from zipfile import BadZipFile +from aleph_client.commands import help_strings + +import asyncio +import json +import logging +from base64 import b32encode, b16decode +from pathlib import Path +from typing import Optional, Dict, List +from zipfile import BadZipFile + +import typer +from aleph_message.models import ( + ProgramMessage, + StoreMessage, +) + +from aleph_client.types import AccountFromPrivateKey +from aleph_client.account import _load_account +from aleph_client.utils import create_archive + +logger = logging.getLogger(__name__) +app = typer.Typer() + + + +from aleph_client.asynchronous import ( + get_fallback_session, + StorageEnum, +) + +from aleph_client.commands.utils import ( + setup_logging, + input_multiline, + prompt_for_volumes, + yes_no_input +) + +from aleph_message.models import ( + ProgramMessage, + StoreMessage, + MessagesResponse, + ProgramContent, +) + +app = typer.Typer() +@app.command() +def upload( + path: Path = typer.Argument(..., help="Path to your source code"), + entrypoint: str = typer.Argument(..., help="Your program entrypoint"), + channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), + memory: int = typer.Option(settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB"), + vcpus: int = typer.Option(settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate."), + timeout_seconds: float = typer.Option(settings.DEFAULT_VM_TIMEOUT, help="If vm is not called after [timeout_seconds] it will shutdown"), + private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), + private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + print_messages: bool = typer.Option(False), + print_code_message: bool = typer.Option(False), + print_program_message: bool = typer.Option(False), + runtime: str = typer.Option(None, help="Hash of the runtime to use for your program. Defaults to aleph debian with Python3.8 and node. You can also create your own runtime and pin it"), + beta: bool = typer.Option(False), + debug: bool = False, + persistent: bool = False, +): + """Register a program to run on Aleph.im virtual machines from a zip archive.""" + + setup_logging(debug) + + path = path.absolute() + + try: + path_object, encoding = create_archive(path) + except BadZipFile: + typer.echo("Invalid zip archive") + raise typer.Exit(3) + except FileNotFoundError: + typer.echo("No such file or directory") + raise typer.Exit(4) + + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + + runtime = ( + runtime + or input(f"Ref of runtime ? [{settings.DEFAULT_RUNTIME_ID}] ") + or settings.DEFAULT_RUNTIME_ID + ) + + volumes = [] + for volume in prompt_for_volumes(): + volumes.append(volume) + typer.echo("\n") + + subscriptions: Optional[List[Dict]] + if beta and yes_no_input("Subscribe to messages ?", default=False): + content_raw = input_multiline() + try: + subscriptions = json.loads(content_raw) + except json.decoder.JSONDecodeError: + typer.echo("Not valid JSON") + raise typer.Exit(code=2) + else: + subscriptions = None + + try: + # Upload the source code + with open(path_object, "rb") as fd: + logger.debug("Reading file") + # TODO: Read in lazy mode instead of copying everything in memory + file_content = fd.read() + storage_engine = ( + StorageEnum.ipfs + if len(file_content) > 4 * 1024 * 1024 + else StorageEnum.storage + ) + logger.debug("Uploading file") + user_code: StoreMessage = synchronous.create_store( + account=account, + file_content=file_content, + storage_engine=storage_engine, + channel=channel, + guess_mime_type=True, + ref=None, + ) + logger.debug("Upload finished") + if print_messages or print_code_message: + typer.echo(f"{user_code.json(indent=4)}") + program_ref = user_code.item_hash + + # Register the program + result: ProgramMessage = synchronous.create_program( + account=account, + program_ref=program_ref, + entrypoint=entrypoint, + runtime=runtime, + storage_engine=StorageEnum.storage, + channel=channel, + memory=memory, + vcpus=vcpus, + timeout_seconds=timeout_seconds, + persistent=persistent, + encoding=encoding, + volumes=volumes, + subscriptions=subscriptions, + ) + logger.debug("Upload finished") + if print_messages or print_program_message: + typer.echo(f"{result.json(indent=4)}") + + hash: str = result.item_hash + hash_base32 = b32encode(b16decode(hash.upper())).strip(b"=").lower().decode() + + typer.echo( + f"Your program has been uploaded on Aleph .\n\n" + "Available on:\n" + f" {settings.VM_URL_PATH.format(hash=hash)}\n" + f" {settings.VM_URL_HOST.format(hash_base32=hash_base32)}\n" + "Visualise on:\n https://explorer.aleph.im/address/" + f"{result.chain}/{result.sender}/message/PROGRAM/{hash}\n" + ) + + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) + + +@app.command() +def update( + hash: str, + path: Path, + private_key: Optional[str] = settings.PRIVATE_KEY_STRING, + private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, + print_message: bool = True, + debug: bool = False, +): + """Update the code of an existing program""" + + setup_logging(debug) + + account = _load_account(private_key, private_key_file) + path = path.absolute() + + try: + program_message: ProgramMessage = synchronous.get_message( + item_hash=hash, message_type=ProgramMessage + ) + code_ref = program_message.content.code.ref + code_message: StoreMessage = synchronous.get_message( + item_hash=code_ref, message_type=StoreMessage + ) + + try: + path, encoding = create_archive(path) + except BadZipFile: + typer.echo("Invalid zip archive") + raise typer.Exit(3) + except FileNotFoundError: + typer.echo("No such file or directory") + raise typer.Exit(4) + + if encoding != program_message.content.code.encoding: + logger.error( + f"Code must be encoded with the same encoding as the previous version " + f"('{encoding}' vs '{program_message.content.code.encoding}'" + ) + raise typer.Exit(1) + + # Upload the source code + with open(path, "rb") as fd: + logger.debug("Reading file") + # TODO: Read in lazy mode instead of copying everything in memory + file_content = fd.read() + logger.debug("Uploading file") + result = synchronous.create_store( + account=account, + file_content=file_content, + storage_engine=code_message.content.item_type, + channel=code_message.channel, + guess_mime_type=True, + ref=code_message.item_hash, + ) + logger.debug("Upload finished") + if print_message: + typer.echo(f"{result.json(indent=4)}") + finally: + # Prevent aiohttp unclosed connector warning + asyncio.run(get_fallback_session().close()) + +@app.command() +def unpersist( + hash: str, + private_key: Optional[str] = settings.PRIVATE_KEY_STRING, + private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE, + debug: bool = False, +): + """Stop a persistent virtual machine by making it non-persistent""" + + setup_logging(debug) + + account = _load_account(private_key, private_key_file) + + existing: MessagesResponse = synchronous.get_messages(hashes=[hash]) + message: ProgramMessage = existing.messages[0] + content: ProgramContent = message.content.copy() + + content.on.persistent = False + content.replaces = message.item_hash + + result = synchronous.submit( + account=account, + content=content.dict(exclude_none=True), + message_type=message.type, + channel=message.channel, + ) + typer.echo(f"{result.json(indent=4)}") \ No newline at end of file diff --git a/src/aleph_client/commands/utils.py b/src/aleph_client/commands/utils.py new file mode 100644 index 00000000..0387cc89 --- /dev/null +++ b/src/aleph_client/commands/utils.py @@ -0,0 +1,69 @@ +import logging +from typer import echo +from typing import Optional + +def input_multiline() -> str: + """Prompt the user for a multiline input.""" + echo("Enter/Paste your content. Ctrl-D or Ctrl-Z ( windows ) to save it.") + contents = "" + while True: + try: + line = input() + except EOFError: + break + contents += line + "\n" + return contents + + +def setup_logging(debug: bool = False): + level = logging.DEBUG if debug else logging.WARNING + logging.basicConfig(level=level) + + +def yes_no_input(text: str, default: Optional[bool] = None): + while True: + if default is True: + response = input(f"{text} [Y/n] ") + elif default is False: + response = input(f"{text} [y/N] ") + else: + response = input(f"{text} ") + + if response.lower() in ("y", "yes"): + return True + elif response.lower() in ("n", "no"): + return False + elif response == "" and default is not None: + return default + else: + if default is None: + echo("Please enter 'y', 'yes', 'n' or 'no'") + else: + echo("Please enter 'y', 'yes', 'n', 'no' or nothing") + continue + + +def prompt_for_volumes(): + while yes_no_input("Add volume ?", default=False): + comment = input("Description: ") or None + mount = input("Mount: ") + persistent = yes_no_input("Persist on VM host ?", default=False) + if persistent: + name = input("Volume name: ") + size_mib = int(input("Size in MiB: ")) + yield { + "comment": comment, + "mount": mount, + "name": name, + "persistence": "host", + "size_mib": size_mib, + } + else: + ref = input("Ref: ") + use_latest = yes_no_input("Use latest version ?", default=True) + yield { + "comment": comment, + "mount": mount, + "ref": ref, + "use_latest": use_latest, + } From 31cedefd435a1e97a1a4f38a442e888ef75e49dd Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Mon, 21 Nov 2022 11:25:53 +0100 Subject: [PATCH 2/3] Fix: FastAPI tests failed, could not import `httpx` httpx is required in tests when using fastapi.testclient --- docker/Dockerfile | 2 +- setup.cfg | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 787d74ff..c6dd33b4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,7 +21,7 @@ RUN pip install --upgrade pip wheel twine # Preinstall dependencies for faster steps RUN pip install --upgrade secp256k1 coincurve aiohttp eciespy python-magic typer RUN pip install --upgrade 'aleph-message~=0.2.3' eth_account pynacl base58 -RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi requests +RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ COPY . . diff --git a/setup.cfg b/setup.cfg index bf583aa0..cbb366dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,8 @@ testing = pynacl base58 fastapi + # httpx is required in tests by fastapi.testclient + httpx requests aleph-pytezos==0.1.0 mqtt = From 20e8517bf4022837b09f46210a5c8bd236de9a75 Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Mon, 21 Nov 2022 21:57:47 +0100 Subject: [PATCH 3/3] Fixup: Reformat with black --- src/aleph_client/__main__.py | 37 +++++++++++++++---------- src/aleph_client/asynchronous.py | 2 +- src/aleph_client/commands/aggregate.py | 14 +++++++--- src/aleph_client/commands/files.py | 17 ++++++++---- src/aleph_client/commands/message.py | 38 ++++++++++++++++++++------ src/aleph_client/commands/program.py | 34 +++++++++++++++++------ src/aleph_client/commands/utils.py | 1 + src/aleph_client/synchronous.py | 2 +- 8 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/aleph_client/__main__.py b/src/aleph_client/__main__.py index 6711339e..eefbae4e 100644 --- a/src/aleph_client/__main__.py +++ b/src/aleph_client/__main__.py @@ -10,27 +10,35 @@ from aleph_client.types import AccountFromPrivateKey from aleph_client.account import _load_account from aleph_client.conf import settings -from .commands import ( - files, - message, - program, - help_strings, - aggregate -) - +from .commands import files, message, program, help_strings, aggregate app = typer.Typer() -app.add_typer(files.app, name="file", help="File uploading and pinning on IPFS and Aleph.im") -app.add_typer(message.app, name="message", help="Post, amend, watch and forget messages on Aleph.im") -app.add_typer(program.app, name="program", help="Upload and update programs on Aleph's VM") -app.add_typer(aggregate.app, name="aggregate", help="Manage aggregate messages on Aleph.im") +app.add_typer( + files.app, name="file", help="File uploading and pinning on IPFS and Aleph.im" +) +app.add_typer( + message.app, + name="message", + help="Post, amend, watch and forget messages on Aleph.im", +) +app.add_typer( + program.app, name="program", help="Upload and update programs on Aleph's VM" +) +app.add_typer( + aggregate.app, name="aggregate", help="Manage aggregate messages on Aleph.im" +) + @app.command() def whoami( - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), ): """ Display your public address. @@ -39,5 +47,6 @@ def whoami( account: AccountFromPrivateKey = _load_account(private_key, private_key_file) typer.echo(account.get_public_key()) + if __name__ == "__main__": app() diff --git a/src/aleph_client/asynchronous.py b/src/aleph_client/asynchronous.py index e4cc2b4d..6e86147f 100644 --- a/src/aleph_client/asynchronous.py +++ b/src/aleph_client/asynchronous.py @@ -311,7 +311,7 @@ async def create_program( timeout_seconds: float = settings.DEFAULT_VM_TIMEOUT, persistent: bool = False, encoding: Encoding = Encoding.zip, - volumes: List[Dict] = None, + volumes: Optional[List[Dict]] = None, subscriptions: Optional[List[Dict]] = None, ) -> ProgramMessage: volumes = volumes if volumes is not None else [] diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index ce9f3cc4..0395a08c 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -15,13 +15,20 @@ app = typer.Typer() + @app.command() def forget( key: str = typer.Argument(..., help="Aggregate item hash to be removed."), - reason: Optional[str] = typer.Option(None, help="A description of why the messages are being forgotten"), + reason: Optional[str] = typer.Option( + None, help="A description of why the messages are being forgotten" + ), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help = help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help = help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), debug: bool = False, ): """Forget all the messages composing an aggregate.""" @@ -37,4 +44,3 @@ def forget( ) hash_list = [message["item_hash"] for message in message_response["messages"]] forget_messages(account, hash_list, reason, channel) - diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 85864d0b..3f097793 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -28,8 +28,12 @@ def pin( hash: str = typer.Argument(..., help="IPFS hash to pin on Aleph.im"), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), ref: Optional[str] = typer.Option(None, help=help_strings.REF), debug: bool = False, ): @@ -54,13 +58,16 @@ def pin( asyncio.run(get_fallback_session().close()) - @app.command() def upload( path: Path = typer.Argument(..., help="Path of the file to upload"), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), ref: Optional[str] = typer.Option(None, help=help_strings.REF), debug: bool = False, ): diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py index 2a6001c7..2a1903df 100644 --- a/src/aleph_client/commands/message.py +++ b/src/aleph_client/commands/message.py @@ -34,14 +34,22 @@ app = typer.Typer() + @app.command() def post( - path: Optional[Path] = typer.Option(None, help="Path to the content you want to post. If omitted, you can input your content directly"), + path: Optional[Path] = typer.Option( + None, + help="Path to the content you want to post. If omitted, you can input your content directly", + ), type: str = typer.Option("test", help="Text representing the message object type"), ref: Optional[str] = typer.Option(None, help=help_strings.REF), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), debug: bool = False, ): """Post a message on Aleph.im.""" @@ -97,8 +105,12 @@ def post( @app.command() def amend( hash: str = typer.Argument(..., help="Hash reference of the message to amend"), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), debug: bool = False, ): """Amend an existing Aleph message.""" @@ -157,11 +169,19 @@ def forget_messages( @app.command() def forget( - hashes: str= typer.Argument(..., help="Comma separated list of hash references of messages to forget"), - reason: Optional[str] = typer.Option(None, help="A description of why the messages are being forgotten."), + hashes: str = typer.Argument( + ..., help="Comma separated list of hash references of messages to forget" + ), + reason: Optional[str] = typer.Option( + None, help="A description of why the messages are being forgotten." + ), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), debug: bool = False, ): """Forget an existing Aleph message.""" diff --git a/src/aleph_client/commands/program.py b/src/aleph_client/commands/program.py index 0b951236..fe904ce9 100644 --- a/src/aleph_client/commands/program.py +++ b/src/aleph_client/commands/program.py @@ -32,7 +32,6 @@ app = typer.Typer() - from aleph_client.asynchronous import ( get_fallback_session, StorageEnum, @@ -42,7 +41,7 @@ setup_logging, input_multiline, prompt_for_volumes, - yes_no_input + yes_no_input, ) from aleph_message.models import ( @@ -53,20 +52,36 @@ ) app = typer.Typer() + + @app.command() def upload( path: Path = typer.Argument(..., help="Path to your source code"), entrypoint: str = typer.Argument(..., help="Your program entrypoint"), channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL), - memory: int = typer.Option(settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB"), - vcpus: int = typer.Option(settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate."), - timeout_seconds: float = typer.Option(settings.DEFAULT_VM_TIMEOUT, help="If vm is not called after [timeout_seconds] it will shutdown"), - private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY), - private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE), + memory: int = typer.Option( + settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB" + ), + vcpus: int = typer.Option( + settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate." + ), + timeout_seconds: float = typer.Option( + settings.DEFAULT_VM_TIMEOUT, + help="If vm is not called after [timeout_seconds] it will shutdown", + ), + private_key: Optional[str] = typer.Option( + settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY + ), + private_key_file: Optional[Path] = typer.Option( + settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE + ), print_messages: bool = typer.Option(False), print_code_message: bool = typer.Option(False), print_program_message: bool = typer.Option(False), - runtime: str = typer.Option(None, help="Hash of the runtime to use for your program. Defaults to aleph debian with Python3.8 and node. You can also create your own runtime and pin it"), + runtime: str = typer.Option( + None, + help="Hash of the runtime to use for your program. Defaults to aleph debian with Python3.8 and node. You can also create your own runtime and pin it", + ), beta: bool = typer.Option(False), debug: bool = False, persistent: bool = False, @@ -234,6 +249,7 @@ def update( # Prevent aiohttp unclosed connector warning asyncio.run(get_fallback_session().close()) + @app.command() def unpersist( hash: str, @@ -260,4 +276,4 @@ def unpersist( message_type=message.type, channel=message.channel, ) - typer.echo(f"{result.json(indent=4)}") \ No newline at end of file + typer.echo(f"{result.json(indent=4)}") diff --git a/src/aleph_client/commands/utils.py b/src/aleph_client/commands/utils.py index 0387cc89..f46af556 100644 --- a/src/aleph_client/commands/utils.py +++ b/src/aleph_client/commands/utils.py @@ -2,6 +2,7 @@ from typer import echo from typing import Optional + def input_multiline() -> str: """Prompt the user for a multiline input.""" echo("Enter/Paste your content. Ctrl-D or Ctrl-Z ( windows ) to save it.") diff --git a/src/aleph_client/synchronous.py b/src/aleph_client/synchronous.py index 75472670..70d03886 100644 --- a/src/aleph_client/synchronous.py +++ b/src/aleph_client/synchronous.py @@ -77,7 +77,7 @@ def create_program( timeout_seconds: float = settings.DEFAULT_VM_TIMEOUT, persistent: bool = False, encoding: Encoding = Encoding.zip, - volumes: List[Dict] = None, + volumes: Optional[List[Dict]] = None, subscriptions: Optional[List[Dict]] = None, ): return wrap_async(asynchronous.create_program)(