diff --git a/src/aleph_client/__init__.py b/src/aleph_client/__init__.py index a5ab5dd2..f37c29b0 100644 --- a/src/aleph_client/__init__.py +++ b/src/aleph_client/__init__.py @@ -1,11 +1,3 @@ -from importlib.metadata import PackageNotFoundError, version - -try: - # Change here if project is renamed and does not equal the package name - __version__ = version("aleph-client") -except PackageNotFoundError: - __version__ = "unknown" - # Deprecation check moved_types = ["__version__", "AlephClient", "AuthenticatedAlephClient", "synchronous", "asynchronous"] diff --git a/src/aleph_client/__main__.py b/src/aleph_client/__main__.py index c86b1f15..a55a805e 100644 --- a/src/aleph_client/__main__.py +++ b/src/aleph_client/__main__.py @@ -16,7 +16,7 @@ ) from aleph_client.utils import AsyncTyper -app = AsyncTyper(no_args_is_help=True) +app = AsyncTyper(no_args_is_help=True, pretty_exceptions_enable=False) app.add_typer(account.app, name="account", help="Manage accounts") app.add_typer( diff --git a/src/aleph_client/commands/about.py b/src/aleph_client/commands/about.py index 942312f3..734a2196 100644 --- a/src/aleph_client/commands/about.py +++ b/src/aleph_client/commands/about.py @@ -1,7 +1,5 @@ from __future__ import annotations -from importlib.metadata import version as importlib_version - import typer from aleph_client.utils import AsyncTyper @@ -10,6 +8,8 @@ def get_version(value: bool): + from importlib.metadata import version as importlib_version + __version__ = "NaN" dist_name = "aleph-client" if value: diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 85c20657..2eec9e06 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -1,28 +1,10 @@ from __future__ import annotations -import asyncio import logging from pathlib import Path from typing import Annotated, Optional -import aiohttp import typer -from aleph.sdk.account import _load_account -from aleph.sdk.chains.common import generate_key -from aleph.sdk.chains.solana import parse_private_key as parse_solana_private_key -from aleph.sdk.conf import ( - MainConfiguration, - load_main_configuration, - save_main_configuration, - settings, -) -from aleph.sdk.evm_utils import ( - get_chains_with_holding, - get_chains_with_super_token, - get_compatible_chains, -) -from aleph.sdk.utils import bytes_from_hex, displayable_amount -from aleph_message.models import Chain from rich.console import Console from rich.panel import Panel from rich.prompt import Prompt @@ -30,13 +12,8 @@ from rich.text import Text from typer.colors import GREEN, RED +from aleph_message.models.base import Chain from aleph_client.commands import help_strings -from aleph_client.commands.utils import ( - input_multiline, - setup_logging, - validated_prompt, - yes_no_input, -) from aleph_client.utils import AsyncTyper, list_unlinked_keys logger = logging.getLogger(__name__) @@ -54,6 +31,10 @@ async def create( debug: Annotated[bool, typer.Option()] = False, ): """Create or import a private key.""" + from aleph.sdk.conf import MainConfiguration, save_main_configuration, settings + from aleph_message.models import Chain + + from aleph_client.commands.utils import setup_logging, validated_prompt setup_logging(debug) @@ -82,10 +63,18 @@ async def create( ) ) if chain == Chain.SOL: + from aleph.sdk.chains.solana import ( + parse_private_key as parse_solana_private_key, + ) + private_key_bytes = parse_solana_private_key(private_key) else: + from aleph.sdk.utils import bytes_from_hex + private_key_bytes = bytes_from_hex(private_key) else: + from aleph.sdk.chains.common import generate_key + private_key_bytes = generate_key() if not private_key_bytes: typer.secho("An unexpected error occurred!", fg=RED) @@ -120,14 +109,17 @@ async def create( @app.command(name="address") def display_active_address( - private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING, - private_key_file: Annotated[ - Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = settings.PRIVATE_KEY_FILE, + private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, + private_key_file: Annotated[Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)] = None, ): """ Display your public address(es). """ + from aleph.sdk.conf import settings + from aleph_message.models import Chain + + private_key = private_key or settings.PRIVATE_KEY_STRING + private_key_file = private_key_file or settings.PRIVATE_KEY_FILE if private_key is not None: private_key_file = None @@ -135,6 +127,8 @@ def display_active_address( typer.secho("No private key available", fg=RED) raise typer.Exit(code=1) + from aleph.sdk.account import _load_account + evm_address = _load_account(private_key, private_key_file, chain=Chain.ETH).get_address() sol_address = _load_account(private_key, private_key_file, chain=Chain.SOL).get_address() @@ -150,6 +144,13 @@ def display_active_chain(): """ Display the currently active chain. """ + from aleph.sdk.conf import load_main_configuration, settings + from aleph.sdk.evm_utils import ( + get_chains_with_holding, + get_chains_with_super_token, + get_compatible_chains, + ) + from aleph_message.models import Chain config_file_path = Path(settings.CONFIG_FILE) config = load_main_configuration(config_file_path) @@ -180,18 +181,23 @@ def display_active_chain(): @app.command(name="path") def path_directory(): """Display the directory path where your private keys, config file, and other settings are stored.""" + from aleph.sdk.conf import settings + console.print(f"Aleph Home directory: [yellow]{settings.CONFIG_HOME}[/yellow]") @app.command() def show( - private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING, - private_key_file: Annotated[ - Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = settings.PRIVATE_KEY_FILE, + private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, + private_key_file: Annotated[Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)] = None, ): """Display current configuration.""" + from aleph.sdk.conf import settings + + private_key = private_key or settings.PRIVATE_KEY_STRING + private_key_file = private_key_file or settings.PRIVATE_KEY_FILE + display_active_address(private_key=private_key, private_key_file=private_key_file) display_active_chain() @@ -204,6 +210,7 @@ def export_private_key( """ Display your private key. """ + from aleph_message.models import Chain if private_key: private_key_file = None @@ -211,6 +218,8 @@ def export_private_key( typer.secho("No private key available", fg=RED) raise typer.Exit(code=1) + from aleph.sdk.account import _load_account + evm_pk = _load_account(private_key, private_key_file, chain=Chain.ETH).export_private_key() sol_pk = _load_account(private_key, private_key_file, chain=Chain.SOL).export_private_key() @@ -225,15 +234,23 @@ def export_private_key( @app.command("sign-bytes") def sign_bytes( message: Annotated[Optional[str], typer.Option(help="Message to sign")] = None, - private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING, - private_key_file: Annotated[ - Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = settings.PRIVATE_KEY_FILE, + private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, + private_key_file: Annotated[Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)] = None, chain: Annotated[Optional[Chain], typer.Option(help=help_strings.ADDRESS_CHAIN)] = None, debug: Annotated[bool, typer.Option()] = False, ): """Sign a message using your private key.""" + import asyncio + + from aleph.sdk.account import _load_account + from aleph.sdk.conf import settings + + from aleph_client.commands.utils import input_multiline, setup_logging + + private_key = private_key or settings.PRIVATE_KEY_STRING + private_key_file = private_key_file or settings.PRIVATE_KEY_FILE + setup_logging(debug) account = _load_account(private_key, private_key_file, chain=chain) @@ -248,9 +265,13 @@ def sign_bytes( async def get_balance(address: str) -> dict: + from aleph.sdk.conf import settings + balance_data: dict = {} uri = f"{settings.API_HOST}/api/v0/addresses/{address}/balance" - async with aiohttp.ClientSession() as session: + from aiohttp import ClientSession + + async with ClientSession() as session: response = await session.get(uri) if response.status == 200: balance_data = await response.json() @@ -264,19 +285,25 @@ async def get_balance(address: str) -> dict: @app.command() async def balance( address: Annotated[Optional[str], typer.Option(help="Address")] = None, - private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING, - private_key_file: Annotated[ - Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = settings.PRIVATE_KEY_FILE, + private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, + private_key_file: Annotated[Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)] = None, chain: Annotated[Optional[Chain], typer.Option(help=help_strings.ADDRESS_CHAIN)] = None, ): """Display your ALEPH balance.""" + from aleph.sdk.account import _load_account + from aleph.sdk.conf import settings + + private_key = private_key or settings.PRIVATE_KEY_STRING + private_key_file = private_key_file or settings.PRIVATE_KEY_FILE + account = _load_account(private_key, private_key_file, chain=chain) if account and not address: address = account.get_address() if address: + from aleph.sdk.utils import displayable_amount + try: balance_data = await get_balance(address) infos = [ @@ -325,6 +352,9 @@ async def balance( async def list_accounts(): """Display available private keys, along with currenlty active chain and account (from config file).""" + from aleph.sdk.conf import load_main_configuration, settings + from aleph.sdk.evm_utils import get_chains_with_holding, get_chains_with_super_token + config_file_path = Path(settings.CONFIG_FILE) config = load_main_configuration(config_file_path) unlinked_keys, _ = await list_unlinked_keys() @@ -354,6 +384,8 @@ async def list_accounts(): active_address = None if config and config.path and active_chain: + from aleph.sdk.account import _load_account + account = _load_account(private_key_path=config.path, chain=active_chain) active_address = account.get_address() @@ -375,6 +407,9 @@ async def configure( chain: Annotated[Optional[Chain], typer.Option(help="New active chain")] = None, ): """Configure current private key file and active chain (default selection)""" + from aleph.sdk.conf import MainConfiguration, save_main_configuration, settings + + from aleph_client.commands.utils import yes_no_input unlinked_keys, config = await list_unlinked_keys() @@ -423,6 +458,8 @@ async def configure( # Configure active chain if not chain and config and hasattr(config, "chain"): + from aleph_message.models import Chain + if not yes_no_input( f"Active chain: [bright_cyan]{config.chain}[/bright_cyan]\n[yellow]Keep current active chain?[/yellow]", default="y", diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index 118109dc..b4a9d98c 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -7,10 +7,7 @@ from typing import Annotated, Optional import typer -from aiohttp import ClientResponseError, ClientSession -from aleph.sdk.account import _load_account -from aleph.sdk.client import AuthenticatedAlephHttpClient -from aleph.sdk.conf import settings +# from aleph.sdk.conf import settings from aleph.sdk.types import AccountFromPrivateKey from aleph.sdk.utils import extended_json_encoder from aleph_message.models import Chain, MessageType @@ -57,8 +54,11 @@ async def forget( ) -> bool: """Delete an aggregate by key or subkeys""" + from aleph.sdk.client import AuthenticatedAlephHttpClient + setup_logging(debug) + from aleph.sdk.account import _load_account account: AccountFromPrivateKey = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address @@ -129,9 +129,11 @@ async def post( debug: bool = False, ) -> bool: """Create or update an aggregate by key or subkey""" + from aleph.sdk.client import AuthenticatedAlephHttpClient setup_logging(debug) + from aleph.sdk.account import _load_account account: AccountFromPrivateKey = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address @@ -191,6 +193,9 @@ async def get( debug: bool = False, ) -> Optional[dict]: """Fetch an aggregate by key or subkeys""" + from aiohttp.client_exceptions import ClientResponseError + from aleph.sdk.account import _load_account + from aleph.sdk.client import AuthenticatedAlephHttpClient setup_logging(debug) @@ -227,6 +232,8 @@ async def list_aggregates( debug: bool = False, ) -> Optional[dict]: """Display all aggregates associated to an account""" + from aiohttp.client import ClientSession + from aleph.sdk.account import _load_account setup_logging(debug) @@ -302,6 +309,7 @@ async def authorize( ): """Grant specific publishing permissions to an address to act on behalf of this account""" + from aleph.sdk.account import _load_account setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) @@ -376,6 +384,7 @@ async def revoke( ): """Revoke all publishing permissions from an address acting on behalf of this account""" + from aleph.sdk.account import _load_account setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) @@ -431,6 +440,7 @@ async def permissions( ) -> Optional[dict]: """Display all permissions emitted by an account""" + from aleph.sdk.account import _load_account setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index f991394c..a71de0ea 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Annotated, Optional -import aiohttp import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.account import _load_account @@ -263,7 +262,9 @@ async def list_files( query_params = GetAccountFilesQueryParams(pagination=pagination, page=page, sort_order=sort_order) uri = f"{settings.API_HOST}/api/v0/addresses/{address}/files" - async with aiohttp.ClientSession() as session: + from aiohttp.client import ClientSession + + async with ClientSession() as session: response = await session.get(uri, params=query_params.dict()) if response.status == 200: files_data = await response.json() diff --git a/src/aleph_client/commands/instance/__init__.py b/src/aleph_client/commands/instance/__init__.py index fd55e395..4added06 100644 --- a/src/aleph_client/commands/instance/__init__.py +++ b/src/aleph_client/commands/instance/__init__.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import builtins import json import logging @@ -9,7 +8,6 @@ from pathlib import Path from typing import Annotated, Any, Optional, Union, cast -import aiohttp import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.account import _load_account @@ -56,7 +54,6 @@ from aleph_client.commands import help_strings from aleph_client.commands.account import get_balance -from aleph_client.commands.instance.display import CRNTable from aleph_client.commands.instance.network import ( fetch_crn_info, fetch_crn_list, @@ -364,9 +361,11 @@ async def create( crn = None if is_stream or confidential or gpu: if crn_url: + from aiohttp.client_exceptions import InvalidURL + try: crn_url = sanitize_url(crn_url) - except aiohttp.InvalidURL as e: + except InvalidURL as e: echo(f"Invalid URL provided: {crn_url}") raise typer.Exit(1) from e @@ -390,6 +389,8 @@ async def create( except Exception as e: raise typer.Exit(1) from e + from aleph_client.commands.instance.display import CRNTable + while not crn: crn_table = CRNTable( only_latest_crn_version=True, @@ -563,7 +564,9 @@ async def create( return item_hash, crn_url, payment_chain # Wait for the instance message to be processed - async with aiohttp.ClientSession() as session: + from aiohttp.client import ClientSession + + async with ClientSession() as session: await wait_for_processed_instance(session, item_hash) # Pay-As-You-Go @@ -579,6 +582,7 @@ async def create( update_type=FlowUpdate.INCREASE, ) if flow_hash_crn: + import asyncio await asyncio.sleep(5) # 2nd flow tx fails if no delay flow_hash_community = await account.manage_flow( receiver=community_wallet_address, @@ -779,6 +783,7 @@ async def delete( if flow_hash_crn: echo(f"CRN flow has been deleted successfully (Tx: {flow_hash_crn})") if flow_com_percent > Decimal("0"): + import asyncio await asyncio.sleep(5) flow_hash_community = await account.manage_flow( community_wallet_address, @@ -805,6 +810,7 @@ async def _show_instances(messages: builtins.list[InstanceMessage]): table.add_column("Logs", style="blue", overflow="fold") await fetch_crn_list() # Precache CRN list + import asyncio scheduler_responses = dict(await asyncio.gather(*[fetch_vm_info(message) for message in messages])) uninitialized_confidential_found = False for message in messages: @@ -1078,15 +1084,17 @@ async def logs( account = _load_account(private_key, private_key_file, chain=chain) + from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError + async with VmClient(account, domain) as manager: try: async for log in manager.get_logs(vm_id=vm_id): log_data = json.loads(log) if "message" in log_data: echo(log_data["message"]) - except aiohttp.ClientConnectorError as e: + except ClientConnectorError as e: echo(f"Unable to connect to domain: {domain}\nError: {e}") - except aiohttp.ClientResponseError: + except ClientResponseError: echo(f"No VM associated with {vm_id} are currently running on {domain}") @@ -1448,6 +1456,7 @@ async def confidential_create( return 1 # Safe delay to ensure instance is starting and is ready + import asyncio echo("Waiting 10sec before to start...") await asyncio.sleep(10) diff --git a/src/aleph_client/commands/instance/display.py b/src/aleph_client/commands/instance/display.py index 2d6e9dcb..0032cec0 100644 --- a/src/aleph_client/commands/instance/display.py +++ b/src/aleph_client/commands/instance/display.py @@ -1,15 +1,15 @@ from __future__ import annotations -import asyncio import logging +import typing from typing import Optional +if typing.TYPE_CHECKING: + from textual.widgets import DataTable, Label, ProgressBar + from textual.widgets._data_table import RowKey + from textual.app import App -from textual.containers import Horizontal -from textual.css.query import NoMatches from textual.reactive import reactive -from textual.widgets import DataTable, Footer, Label, ProgressBar -from textual.widgets._data_table import RowKey from aleph_client.commands.instance.network import ( fetch_crn_list, @@ -72,6 +72,9 @@ def __init__( def compose(self): """Create child widgets for the app.""" + from textual.containers import Horizontal + from textual.widgets import DataTable, Footer, Label, ProgressBar + self.table = DataTable(cursor_type="row", name="Select CRN") self.table.add_column("Score", key="score") self.table.add_column("Name", key="name") @@ -98,6 +101,7 @@ def compose(self): async def on_mount(self): self.table.styles.height = "95%" + import asyncio task = asyncio.create_task(self.fetch_node_list()) self.tasks.add(task) task.add_done_callback(self.tasks.discard) @@ -113,6 +117,7 @@ async def fetch_node_list(self): self.tasks = set() # Fetch all CRNs + import asyncio for crn in list(self.crns.values()): task = asyncio.create_task(self.add_crn_info(crn)) self.tasks.add(task) @@ -180,6 +185,8 @@ async def add_crn_info(self, crn: CRNInfo): def make_progress(self, task): """Called automatically to advance the progress bar.""" + from textual.css.query import NoMatches + try: self.progress_bar.advance(1) self.loader_label_end.update(f" Available: {self.active_crns} Match: {self.filtered_crns}") @@ -203,6 +210,8 @@ def sort_reverse(self, sort_type: str) -> bool: return reverse def sort_by(self, column, sort_func=lambda row: row.lower(), invert=False): + from textual.widgets import DataTable + table = self.query_one(DataTable) reverse = self.sort_reverse(column) table.sort( diff --git a/src/aleph_client/commands/instance/network.py b/src/aleph_client/commands/instance/network.py index bc57580b..309289c2 100644 --- a/src/aleph_client/commands/instance/network.py +++ b/src/aleph_client/commands/instance/network.py @@ -5,13 +5,6 @@ from json import JSONDecodeError from typing import Optional -from aiohttp import ( - ClientConnectorError, - ClientResponseError, - ClientSession, - ClientTimeout, - InvalidURL, -) from aleph.sdk import AlephHttpClient from aleph.sdk.conf import settings from aleph.sdk.exceptions import ForgottenMessageError, MessageNotFoundError @@ -57,6 +50,12 @@ async def call_program_crn_list() -> Optional[dict]: Returns: dict: Dictionary containing the compute resource node list. """ + from aiohttp.client import ClientSession, ClientTimeout + from aiohttp.client_exceptions import ( + ClientConnectorError, + ClientResponseError, + InvalidURL, + ) try: async with ClientSession(timeout=ClientTimeout(total=60)) as session: @@ -88,6 +87,7 @@ async def fetch_latest_crn_version() -> str: Returns: str: Latest crn version as x.x.x. """ + from aiohttp.client import ClientSession async with ClientSession() as session: try: @@ -172,6 +172,7 @@ async def fetch_vm_info(message: InstanceMessage) -> tuple[str, dict[str, str]]: Returns: VM information. """ + from aiohttp.client import ClientSession async with ClientSession() as session: chain = safe_getattr(message, "content.payment.chain.value") @@ -198,6 +199,8 @@ async def fetch_vm_info(message: InstanceMessage) -> tuple[str, dict[str, str]]: "tac_url": "", "tac_accepted": "", } + from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError + try: # Fetch from the scheduler API directly if no payment or no receiver (hold-tier non-confidential) if is_hold and not is_confidential and not has_gpu: @@ -275,6 +278,7 @@ async def fetch_settings() -> dict: Returns: dict: Dictionary containing the settings. """ + from aiohttp.client import ClientSession async with ClientSession() as session: try: diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py index 7b62a20e..916f9ea2 100644 --- a/src/aleph_client/commands/message.py +++ b/src/aleph_client/commands/message.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import json import os.path import subprocess @@ -24,14 +23,6 @@ from aleph_message.status import MessageStatus from aleph_client.commands import help_strings -from aleph_client.commands.utils import ( - colorful_json, - colorful_message_json, - colorized_status, - input_multiline, - setup_logging, - str_to_datetime, -) from aleph_client.utils import AsyncTyper app = AsyncTyper(no_args_is_help=True) @@ -41,6 +32,12 @@ async def get( item_hash: Annotated[str, typer.Argument(help="Item hash of the message")], ): + from aleph_client.commands.utils import ( + colorful_json, + colorful_message_json, + colorized_status, + ) + async with AlephHttpClient(api_server=settings.API_HOST) as client: message: Optional[AlephMessage] = None try: @@ -75,6 +72,8 @@ async def find( end_date: Annotated[Optional[str], typer.Option()] = None, ignore_invalid_messages: Annotated[bool, typer.Option()] = True, ): + from aleph_client.commands.utils import colorful_json, str_to_datetime + parsed_message_types = ( [MessageType(message_type) for message_type in message_types.split(",")] if message_types else None ) @@ -129,6 +128,8 @@ async def post( ): """Post a message on aleph.im.""" + from aleph_client.commands.utils import input_multiline, setup_logging + setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) @@ -179,6 +180,8 @@ async def amend( ): """Amend an existing aleph.im message.""" + from aleph_client.commands.utils import setup_logging + setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) @@ -242,6 +245,8 @@ async def forget( ): """Forget an existing aleph.im message.""" + from aleph_client.commands.utils import setup_logging + setup_logging(debug) hash_list: list[ItemHash] = [ItemHash(h) for h in hashes.split(",")] @@ -259,6 +264,8 @@ async def watch( ): """Watch a hash for amends and print amend hashes""" + from aleph_client.commands.utils import setup_logging + setup_logging(debug) async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -287,6 +294,10 @@ def sign( ): """Sign an aleph message with a private key. If no --message is provided, the message will be read from stdin.""" + import asyncio + + from aleph_client.commands.utils import input_multiline, setup_logging + setup_logging(debug) account: AccountFromPrivateKey = _load_account(private_key, private_key_file) diff --git a/src/aleph_client/commands/node.py b/src/aleph_client/commands/node.py index f71b966e..5dbc06c7 100644 --- a/src/aleph_client/commands/node.py +++ b/src/aleph_client/commands/node.py @@ -7,7 +7,6 @@ import unicodedata from typing import Annotated, Optional -import aiohttp import typer from aleph.sdk.conf import settings from rich import text @@ -38,7 +37,9 @@ def __init__(self, **kwargs): async def _fetch_nodes() -> NodeInfo: """Fetch node aggregates and format it as NodeInfo""" - async with aiohttp.ClientSession() as session: + from aiohttp.client import ClientSession + + async with ClientSession() as session: async with session.get(node_link) as resp: if resp.status != 200: logger.error("Unable to fetch node information") diff --git a/src/aleph_client/commands/pricing.py b/src/aleph_client/commands/pricing.py index c72f3a85..046f4162 100644 --- a/src/aleph_client/commands/pricing.py +++ b/src/aleph_client/commands/pricing.py @@ -5,7 +5,6 @@ from enum import Enum from typing import Annotated, Optional -import aiohttp import typer from aleph.sdk.conf import settings from aleph.sdk.utils import displayable_amount, safe_getattr @@ -310,7 +309,9 @@ def display_table_for( async def fetch_pricing() -> Pricing: """Fetch pricing aggregate and format it as Pricing""" - async with aiohttp.ClientSession() as session: + from aiohttp.client import ClientSession + + async with ClientSession() as session: async with session.get(pricing_link) as resp: if resp.status != 200: logger.error("Unable to fetch pricing aggregate") diff --git a/src/aleph_client/commands/program.py b/src/aleph_client/commands/program.py index 3974048a..d2a26a85 100644 --- a/src/aleph_client/commands/program.py +++ b/src/aleph_client/commands/program.py @@ -10,7 +10,6 @@ from typing import Annotated, Any, Optional, cast from zipfile import BadZipFile -import aiohttp import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.account import _load_account @@ -845,9 +844,12 @@ async def runtime_checker( program_url = settings.VM_URL_PATH.format(hash=program_hash) versions: dict echo("Query runtime checker to retrieve versions...") + + from aiohttp.client import ClientSession, ClientTimeout + try: - timeout = aiohttp.ClientTimeout(total=settings.HTTP_REQUEST_TIMEOUT) - async with aiohttp.ClientSession(timeout=timeout) as session: + timeout = ClientTimeout(total=settings.HTTP_REQUEST_TIMEOUT) + async with ClientSession(timeout=timeout) as session: async with session.get(program_url) as resp: resp.raise_for_status() versions = await resp.json() diff --git a/src/aleph_client/commands/utils.py b/src/aleph_client/commands/utils.py index dc428bfe..8c49205e 100644 --- a/src/aleph_client/commands/utils.py +++ b/src/aleph_client/commands/utils.py @@ -1,15 +1,17 @@ from __future__ import annotations -import asyncio import logging import os import shutil import sys +import typing from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable, Optional, TypeVar, Union -from aiohttp import ClientSession +if typing.TYPE_CHECKING: + from aiohttp import ClientSession + from aleph.sdk import AlephHttpClient from aleph.sdk.chains.ethereum import ETHAccount from aleph.sdk.conf import settings @@ -275,6 +277,8 @@ def is_environment_interactive() -> bool: async def wait_for_processed_instance(session: ClientSession, item_hash: ItemHash): """Wait for a message to be processed by CCN""" + import asyncio + while True: url = f"{settings.API_HOST.rstrip('/')}/api/v0/messages/{item_hash}" message = await fetch_json(session, url) @@ -290,10 +294,13 @@ async def wait_for_processed_instance(session: ClientSession, item_hash: ItemHas async def wait_for_confirmed_flow(account: ETHAccount, receiver: str): """Wait for a flow to be confirmed on-chain""" + import asyncio + while True: flow = await account.get_flow(receiver) if flow: return + echo("Flow transaction is still pending, waiting 10sec...") await asyncio.sleep(10) diff --git a/src/aleph_client/models.py b/src/aleph_client/models.py index 93214127..79125b8d 100644 --- a/src/aleph_client/models.py +++ b/src/aleph_client/models.py @@ -1,7 +1,6 @@ from datetime import datetime from typing import Any, Optional -from aiohttp import InvalidURL from aleph.sdk.types import StoredContent from aleph_message.models import ItemHash from aleph_message.models.execution.environment import CpuProperties, GpuDeviceClass @@ -157,6 +156,8 @@ def from_unsanitized_input( machine_usage = MachineUsage.parse_obj(system_usage) if system_usage else None ipv6_check = crn.get("ipv6_check") ipv6 = bool(ipv6_check and all(ipv6_check.values())) + from aiohttp.client_exceptions import InvalidURL + try: url = sanitize_url(crn["address"]) except InvalidURL: diff --git a/src/aleph_client/utils.py b/src/aleph_client/utils.py index cc3c5aaa..7eba51ca 100644 --- a/src/aleph_client/utils.py +++ b/src/aleph_client/utils.py @@ -1,13 +1,12 @@ from __future__ import annotations -import asyncio import inspect import logging import os import re import subprocess import sys -from asyncio import ensure_future +import typing from functools import lru_cache, partial, wraps from pathlib import Path from shutil import make_archive @@ -15,21 +14,16 @@ from urllib.parse import ParseResult, urlparse from zipfile import BadZipFile, ZipFile -import aiohttp import typer -from aiohttp import ClientSession -from aleph.sdk.conf import MainConfiguration, load_main_configuration, settings -from aleph.sdk.types import GenericMessage -from aleph_message.models.base import MessageType -from aleph_message.models.execution.base import Encoding -logger = logging.getLogger(__name__) +if typing.TYPE_CHECKING: + from aiohttp.client import ClientSession + from aleph.sdk.conf import MainConfiguration + from aleph.sdk.types import GenericMessage + from aleph_message.models.base import MessageType + from aleph_message.models.execution.base import Encoding -try: - import magic -except ImportError: - logger.info("Could not import library 'magic', MIME type detection disabled") - magic = None # type:ignore +logger = logging.getLogger(__name__) def try_open_zip(path: Path) -> None: @@ -44,7 +38,11 @@ def try_open_zip(path: Path) -> None: def create_archive(path: Path) -> tuple[Path, Encoding]: """Create a zip archive from a directory""" + from aleph_message.models.execution.base import Encoding + if os.path.isdir(path): + from aleph.sdk.conf import settings + if settings.CODE_USES_SQUASHFS: logger.debug("Creating squashfs archive...") archive_path = Path(f"{path}.squashfs") @@ -57,6 +55,9 @@ def create_archive(path: Path) -> tuple[Path, Encoding]: archive_path = Path(f"{path}.zip") return archive_path, Encoding.zip elif os.path.isfile(path): + from aleph.sdk.utils import import_magic + + magic = import_magic() if path.suffix == ".squashfs" or (magic and magic.from_file(path).startswith("Squashfs filesystem")): return path, Encoding.squashfs else: @@ -77,6 +78,7 @@ class AsyncTyper(typer.Typer): @staticmethod def maybe_run_async(decorator, f): if inspect.iscoroutinefunction(f): + import asyncio @wraps(f) def runner(*args, **kwargs): @@ -120,6 +122,8 @@ async def list_unlinked_keys() -> tuple[list[Path], Optional[MainConfiguration]] - A list of unlinked private key files as Path objects. - The active MainConfiguration object (the single account in the config file). """ + from aleph.sdk.conf import load_main_configuration, settings + config_home: Union[str, Path] = settings.CONFIG_HOME if settings.CONFIG_HOME else Path.home() private_key_dir = Path(config_home, "private-keys") @@ -158,6 +162,16 @@ async def list_unlinked_keys() -> tuple[list[Path], Optional[MainConfiguration]] ] +def raise_invalid_url(msg: str) -> None: + """ + Raise an InvalidURL exception with the given message. + Imports aiohttp lazily to improve response time when the URL is valid. + """ + from aiohttp.client_exceptions import InvalidURL + + raise InvalidURL(msg) + + def sanitize_url(url: str) -> str: """Ensure that the URL is valid and not obviously irrelevant. @@ -168,22 +182,23 @@ def sanitize_url(url: str) -> str: """ if not url: msg = "Empty URL" - raise aiohttp.InvalidURL(msg) + raise_invalid_url(msg) parsed_url: ParseResult = urlparse(url) if parsed_url.scheme not in ["http", "https"]: msg = f"Invalid URL scheme: {parsed_url.scheme}" - raise aiohttp.InvalidURL(msg) + raise_invalid_url(msg) if parsed_url.hostname in FORBIDDEN_HOSTS: logger.debug( f"Invalid URL {url} hostname {parsed_url.hostname} is in the forbidden host list " f"({', '.join(FORBIDDEN_HOSTS)})" ) msg = "Invalid URL host" - raise aiohttp.InvalidURL(msg) + raise_invalid_url(msg) return url.strip("/") def async_lru_cache(async_function): + from asyncio import ensure_future @lru_cache(maxsize=0 if "pytest" in sys.modules else 1) def cached_async_function(*args, **kwargs): diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py deleted file mode 100644 index 8422fb81..00000000 --- a/tests/unit/test_init.py +++ /dev/null @@ -1,5 +0,0 @@ -from aleph_client import __version__ - - -def test_version(): - assert __version__ != ""