diff --git a/LICENSE.txt b/LICENSE.txt index fbcef06b..eb145da9 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Aleph.im project +Copyright (c) 2019-2023 Aleph.im Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 00000000..717ac02c --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ + +# aleph-sdk-python +Python SDK for the Aleph.im network, next generation network of decentralized big data applications. + +Development follows the [Aleph Whitepaper](https://github.com/aleph-im/aleph-whitepaper). + +## Documentation +Documentation (albeit still vastly incomplete as it is a work in progress) can be found at [http://aleph-client.readthedocs.io/](http://aleph-client.readthedocs.io/) or built from this repo with: + +```shell +$ python setup.py docs +``` + +## Requirements +### Linux +Some cryptographic functionalities use curve secp256k1 and require installing [libsecp256k1](https://github.com/bitcoin-core/secp256k1). + +```shell +$ apt-get install -y python3-pip libsecp256k1-dev +``` +Using some chains may also require installing `libgmp3-dev`. + +### macOs +```shell +$ brew tap cuber/homebrew-libsecp256k1 +$ brew install libsecp256k1 +``` + +## Installation +Using pip and [PyPI](https://pypi.org/project/aleph-sdk-python/): + +```shell +$ pip install aleph-sdk-python[ethereum,solana,tezos] +``` + +## Installation for development +To install from source and still be able to modify the source code: + +```shell +$ pip install -e .[testing] +``` +or +```shell +$ python setup.py develop +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index a1b09133..00000000 --- a/README.rst +++ /dev/null @@ -1,63 +0,0 @@ -============ -aleph-client -============ - -Python Client for the aleph.im network, next generation network of decentralized big data applications. -Developement follows the `Aleph Whitepaper `_. - -Documentation -============= - -Documentation (albeit still vastly incomplete as it is a work in progress) can be found at http://aleph-client.readthedocs.io/ or built from this repo with: - - $ python setup.py docs - - -Requirements -============ - -- Linux : - -Some cryptographic functionalities use curve secp256k1 and require installing -`libsecp256k1 `_. - - $ apt-get install -y python3-pip libsecp256k1-dev - -- macOs : - - $ brew tap cuber/homebrew-libsecp256k1 - $ brew install libsecp256k1 - - -Installation -============ - -Using pip and `PyPI `_: - - $ pip install aleph-client - - -Installation for development -============================ - -If you want NULS2 support you will need to install nuls2-python (currently only available on github): - - $ pip install git+https://github.com/aleph-im/nuls2-python.git - - -To install from source and still be able to modify the source code: - - $ pip install -e . - or - $ python setup.py develop - - - -Using Docker -============ - -Use the Aleph client and it's CLI from within Docker or Podman with: - - $ docker run --rm -ti -v $(pwd)/data:/data ghcr.io/aleph-im/aleph-client/aleph-client:master --help - -Warning: This will use an ephemeral key that will be discarded when stopping the container. diff --git a/docker/python-3.10.dockerfile b/docker/python-3.10.dockerfile index 5f67e52a..9d208b4d 100644 --- a/docker/python-3.10.dockerfile +++ b/docker/python-3.10.dockerfile @@ -20,7 +20,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 'aleph-message~=0.3.0' eth_account pynacl base58 RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ @@ -36,6 +36,3 @@ ENV ALEPH_PRIVATE_KEY_FILE=/data/secret.key WORKDIR /home/user USER user -RUN aleph --install-completion bash -ENTRYPOINT ["aleph"] -CMD ["--help"] diff --git a/docker/python-3.11.dockerfile b/docker/python-3.11.dockerfile index 0938f2b0..7c822f54 100644 --- a/docker/python-3.11.dockerfile +++ b/docker/python-3.11.dockerfile @@ -20,7 +20,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 'aleph-message~=0.3.0' eth_account pynacl base58 RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ @@ -36,6 +36,3 @@ ENV ALEPH_PRIVATE_KEY_FILE=/data/secret.key WORKDIR /home/user USER user -RUN aleph --install-completion bash -ENTRYPOINT ["aleph"] -CMD ["--help"] diff --git a/docker/python-3.9.dockerfile b/docker/python-3.9.dockerfile index 418596e9..3ae958b9 100644 --- a/docker/python-3.9.dockerfile +++ b/docker/python-3.9.dockerfile @@ -20,7 +20,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 'aleph-message~=0.3.0' eth_account pynacl base58 RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ @@ -36,6 +36,3 @@ ENV ALEPH_PRIVATE_KEY_FILE=/data/secret.key WORKDIR /home/user USER user -RUN aleph --install-completion bash -ENTRYPOINT ["aleph"] -CMD ["--help"] diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile index 49e58633..3de895b1 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-20.04.dockerfile @@ -25,7 +25,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 'aleph-message~=0.3.0' eth_account pynacl base58 RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ @@ -42,6 +42,3 @@ ENV ALEPH_PRIVATE_KEY_FILE=/data/secret.key WORKDIR /home/user USER user -RUN aleph --install-completion bash -ENTRYPOINT ["aleph"] -CMD ["--help"] diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 330a454b..302daa37 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -25,7 +25,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 'aleph-message~=0.3.0' eth_account pynacl base58 RUN pip install --upgrade pytest pytest-cov pytest-asyncio mypy types-setuptools pytest-asyncio fastapi httpx requests WORKDIR /opt/aleph-client/ @@ -42,6 +42,3 @@ ENV ALEPH_PRIVATE_KEY_FILE=/data/secret.key WORKDIR /home/user USER user -RUN aleph --install-completion bash -ENTRYPOINT ["aleph"] -CMD ["--help"] diff --git a/docker/with-ipfs.dockerfile b/docker/with-ipfs.dockerfile index d405e21d..f3446707 100644 --- a/docker/with-ipfs.dockerfile +++ b/docker/with-ipfs.dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.10 # === Install IPFS === RUN apt-get install -y wget diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cf20fc22..00000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# ============================================================================= -# DEPRECATION WARNING: -# -# The file `requirements.txt` does not influence the package dependencies and -# will not be automatically created in the next version of PyScaffold (v4.x). -# -# Please have look at the docs for better alternatives -# (`Dependency Management` section). -# ============================================================================= -# -# Add your pinned requirements so that they can be easily installed with: -# pip install -r requirements.txt -# Remember to also add them in setup.cfg but unpinned. -# Example: -# numpy==1.13.3 -# scipy==1.0 -# -sphinxcontrib-plantuml \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 65daf3df..f7be73be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = eciespy typing_extensions typer - aleph-message~=0.2.3 + aleph-message~=0.3.0 eth_account>=0.4.0 python-magic # The usage of test_requires is discouraged, see `Dependency Management` docs diff --git a/src/aleph_client/commands/__init__.py b/src/aleph_client/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py deleted file mode 100644 index e83bfcda..00000000 --- a/src/aleph_client/commands/account.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging -import os -from typing import Optional - -import typer - -from aleph_client.chains.common import generate_key -from aleph_client.commands import help_strings -from aleph_client.commands.utils import setup_logging -from aleph_client.conf import settings - -logger = logging.getLogger(__name__) -app = typer.Typer() - - -@app.command() -def create( - from_private_key: Optional[str] = typer.Option(None, help=help_strings.PRIVATE_KEY), - debug: bool = False, -): - """Create or import a private key.""" - - setup_logging(debug) - - typer.echo("Generating private key file.") - private_key_file = typer.prompt( - "Enter file in which to save the key", settings.PRIVATE_KEY_FILE - ) - - if os.path.exists(private_key_file): - typer.echo(f"Error: key already exists: '{private_key_file}'") - exit(1) - - private_key = None - if from_private_key is not None: - private_key = from_private_key.encode() - else: - private_key = generate_key() - - if private_key is None: - typer.echo("An unexpected error occurred!") - exit(1) - - os.makedirs(os.path.dirname(private_key_file), exist_ok=True) - with open(private_key_file, "wb") as prvfile: - prvfile.write(private_key) - typer.echo(f"Private key created => {private_key_file}") diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py deleted file mode 100644 index af6f5aa7..00000000 --- a/src/aleph_client/commands/aggregate.py +++ /dev/null @@ -1,46 +0,0 @@ -from pathlib import Path -from typing import Optional - -import typer -from aleph_message.models import MessageType - -from aleph_client import UserSession -from aleph_client.account import _load_account -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_client.conf import settings -from aleph_client.types import AccountFromPrivateKey - -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) - - with UserSession(api_server=settings.API_HOST) as session: - message_response = session.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 deleted file mode 100644 index a7d3c079..00000000 --- a/src/aleph_client/commands/files.py +++ /dev/null @@ -1,93 +0,0 @@ -import logging -from pathlib import Path -from typing import Optional - -import typer - -from aleph_client import AuthenticatedUserSession -from aleph_client.account import _load_account -from aleph_client.commands import help_strings -from aleph_client.commands.utils import setup_logging -from aleph_client.conf import settings -from aleph_client.types import AccountFromPrivateKey, StorageEnum - -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) - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - message, _status = session.create_store( - file_hash=hash, - storage_engine=StorageEnum.ipfs, - channel=channel, - ref=ref, - ) - logger.debug("Upload finished") - typer.echo(f"{message.json(indent=4)}") - - -@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) - - 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") - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - message, status = session.create_store( - file_content=file_content, - storage_engine=storage_engine, - channel=channel, - guess_mime_type=True, - ref=ref, - ) - logger.debug("Upload finished") - typer.echo(f"{message.json(indent=4)}") diff --git a/src/aleph_client/commands/help_strings.py b/src/aleph_client/commands/help_strings.py deleted file mode 100644 index 76fd2dfc..00000000 --- a/src/aleph_client/commands/help_strings.py +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 74295519..00000000 --- a/src/aleph_client/commands/message.py +++ /dev/null @@ -1,192 +0,0 @@ -import json -import os.path -import subprocess -import tempfile -from pathlib import Path -from typing import Dict, List, Optional - -import typer -from aleph_message.models import AlephMessage - -from aleph_client import AuthenticatedUserSession, UserSession -from aleph_client.account import _load_account -from aleph_client.commands import help_strings -from aleph_client.commands.utils import input_multiline, setup_logging -from aleph_client.conf import settings -from aleph_client.types import AccountFromPrivateKey, StorageEnum - -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) - 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) - - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - message, status = session.create_post( - post_content=content, - post_type=type, - ref=ref, - channel=channel, - inline=True, - storage_engine=storage_engine, - ) - typer.echo(message.json(indent=4)) - - -@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) - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - existing_message: AlephMessage = session.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) - message, _status = session.submit( - content=new_content.dict(), - message_type=existing_message.type, - channel=existing_message.channel, - ) - typer.echo(f"{message.json(indent=4)}") - - -def forget_messages( - account: AccountFromPrivateKey, - hashes: List[str], - reason: Optional[str], - channel: str, -): - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - message, status = session.forget( - hashes=hashes, - reason=reason, - channel=channel, - ) - typer.echo(f"{message.json(indent=4)}") - - -@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) - - with UserSession(api_server=settings.API_HOST) as session: - original: AlephMessage = session.get_message(item_hash=ref) - - for message in session.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 deleted file mode 100644 index d6ee3017..00000000 --- a/src/aleph_client/commands/program.py +++ /dev/null @@ -1,294 +0,0 @@ -import json -import logging -from base64 import b16decode, b32encode -from pathlib import Path -from typing import List, Mapping, Optional -from zipfile import BadZipFile - -import typer -from aleph_message.models import ( - MessagesResponse, - ProgramContent, - ProgramMessage, - StoreMessage, -) - -from aleph_client import AuthenticatedUserSession -from aleph_client.account import _load_account -from aleph_client.commands import help_strings -from aleph_client.commands.utils import ( - input_multiline, - prompt_for_volumes, - setup_logging, - volume_to_dict, - yes_no_input, -) -from aleph_client.conf import settings -from aleph_client.types import AccountFromPrivateKey, StorageEnum -from aleph_client.utils import create_archive - -logger = logging.getLogger(__name__) -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, - persistent_volume: Optional[List[str]] = typer.Option( - None, - help="""Takes 3 parameters - A persistent volume is allocated on the host machine at any time - eg: Use , to seperate the parameters and no spaces - --persistent_volume persistence=host,name=my-volume,size=100 ./my-program main:app - """, - ), - ephemeral_volume: Optional[List[str]] = typer.Option( - None, - help="""Takes 1 parameter Only - Ephemeral volumes can move and be removed by the host,Garbage collected basically, when the VM isn't running - eg: Use , to seperate the parameters and no spaces - --ephemeral-volume size_mib=100 ./my-program main:app """, - ), - immutable_volume: Optional[List[str]] = typer.Option( - None, - help="""Takes 3 parameters - Immutable volume is one whose contents do not change - eg: Use , to seperate the parameters and no spaces - --immutable-volume ref=25a393222692c2f73489dc6710ae87605a96742ceef7b91de4d7ec34bb688d94,use_latest=true,mount=/mnt/volume ./my-program main:app - """, - ), -): - """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 = [] - - # Check if the volumes are empty - if ( - persistent_volume is None - or ephemeral_volume is None - or immutable_volume is None - ): - for volume in prompt_for_volumes(): - volumes.append(volume) - typer.echo("\n") - - # else Parse all the volumes that have passed as the cli parameters and put it into volume list - else: - if len(persistent_volume) > 0: - persistent_volume_dict = volume_to_dict(volume=persistent_volume) - volumes.append(persistent_volume_dict) - if len(ephemeral_volume) > 0: - ephemeral_volume_dict = volume_to_dict(volume=ephemeral_volume) - volumes.append(ephemeral_volume_dict) - if len(immutable_volume) > 0: - immutable_volume_dict = volume_to_dict(volume=immutable_volume) - volumes.append(immutable_volume_dict) - - subscriptions: Optional[List[Mapping]] - 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 - - # Upload the source code - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - 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, _status = session.create_store( - 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 - message, status = session.create_program( - 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"{message.json(indent=4)}") - - hash: str = message.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"{message.chain}/{message.sender}/message/PROGRAM/{hash}\n" - ) - - -@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() - - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - program_message: ProgramMessage = session.get_message( - item_hash=hash, message_type=ProgramMessage - ) - code_ref = program_message.content.code.ref - code_message: StoreMessage = session.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") - message, status = session.create_store( - 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"{message.json(indent=4)}") - - -@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) - - with AuthenticatedUserSession( - account=account, api_server=settings.API_HOST - ) as session: - existing: MessagesResponse = session.get_messages(hashes=[hash]) - message: ProgramMessage = existing.messages[0] - content: ProgramContent = message.content.copy() - - content.on.persistent = False - content.replaces = message.item_hash - - message, _status = session.submit( - content=content.dict(exclude_none=True), - message_type=message.type, - channel=message.channel, - ) - typer.echo(f"{message.json(indent=4)}") diff --git a/src/aleph_client/commands/utils.py b/src/aleph_client/commands/utils.py deleted file mode 100644 index 396aab92..00000000 --- a/src/aleph_client/commands/utils.py +++ /dev/null @@ -1,89 +0,0 @@ -import logging -from typing import Dict, List, Optional, Union - -from typer import echo - - -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, - } - - -def volume_to_dict(volume: List[str]) -> Optional[Dict[str, Union[str, int]]]: - if not volume: - return None - dict_store: Dict[str, Union[str, int]] = {} - for word in volume: - split_values = word.split(",") - for param in split_values: - p = param.split("=") - if p[1].isdigit(): - dict_store[p[0]] = int(p[1]) - elif p[1] in ["True", "true", "False", "false"]: - dict_store[p[0]] = bool(p[1].capitalize()) - else: - dict_store[p[0]] = p[1] - - return dict_store diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index de4b5963..2db3a5b3 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -126,9 +126,9 @@ async def test_create_store(mock_session_with_post_success): async def test_create_program(mock_session_with_post_success): async with mock_session_with_post_success as session: program_message, message_status = await session.create_program( - program_ref="FAKE-HASH", + program_ref="cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", entrypoint="main:app", - runtime="FAKE-HASH", + runtime="facefacefacefacefacefacefacefacefacefacefacefacefacefacefaceface", channel="TEST", )