diff --git a/pyproject.toml b/pyproject.toml index da8f02d9..9b3c019a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ auth0-python = "^4.4.0" algokit-utils = "v2.1.0" multiformats = "^0.2.1" aiohttp = "3.9.1" -yaspin = "^3.0.1" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" diff --git a/src/algokit/cli/tasks/ipfs.py b/src/algokit/cli/tasks/ipfs.py index ee3517eb..5fc06419 100644 --- a/src/algokit/cli/tasks/ipfs.py +++ b/src/algokit/cli/tasks/ipfs.py @@ -2,7 +2,6 @@ from pathlib import Path import click -from yaspin import yaspin # type: ignore # noqa: PGH003 from algokit.core.tasks.ipfs import ( MAX_FILE_SIZE, @@ -15,6 +14,7 @@ set_pinata_jwt, upload_to_pinata, ) +from algokit.core.utils import run_with_animation logger = logging.getLogger(__name__) @@ -72,17 +72,21 @@ def logout_command() -> None: def upload(file_path: Path, name: str | None) -> None: pinata_jwt = get_pinata_jwt() if not pinata_jwt: - raise click.ClickException("You are not logged in! Please login using `algokit ipfs login`.") + raise click.ClickException("You are not logged in! Please login using `algokit task ipfs login`.") try: total = file_path.stat().st_size if total > MAX_FILE_SIZE: raise click.ClickException("File size exceeds 100MB limit!") - with yaspin(text="Uploading", color="yellow") as spinner: - cid = upload_to_pinata(file_path, pinata_jwt, name) - spinner.ok("✅ ") - logger.info(f"File uploaded successfully!\nCID: {cid}") + def upload() -> str: + return upload_to_pinata(file_path, pinata_jwt, name) + + cid = run_with_animation( + target_function=upload, + animation_text="Uploading", + ) + logger.info(f"File uploaded successfully!\n CID: {cid}") except click.ClickException as ex: raise ex diff --git a/src/algokit/cli/tasks/mint.py b/src/algokit/cli/tasks/mint.py index 00e06e0d..7b371951 100644 --- a/src/algokit/cli/tasks/mint.py +++ b/src/algokit/cli/tasks/mint.py @@ -207,7 +207,7 @@ def mint( # noqa: PLR0913 pinata_jwt = get_pinata_jwt() if not pinata_jwt: - raise click.ClickException("You are not logged in! Please login using `algokit ipfs login`.") + raise click.ClickException("You are not logged in! Please login using `algokit task ipfs login`.") client = load_algod_client(network) validate_balance( diff --git a/src/algokit/core/tasks/ipfs.py b/src/algokit/core/tasks/ipfs.py index f861a025..d0b8aa6d 100644 --- a/src/algokit/core/tasks/ipfs.py +++ b/src/algokit/core/tasks/ipfs.py @@ -57,8 +57,8 @@ def get_pinata_jwt() -> str | None: old_api_key = keyring.get_password("algokit_web3_storage", "algokit_web3_storage_access_token") if old_api_key: logger.warning( - "You are using the old Web3 Storage API key. Please login again using `algokit ipfs login` with Pinata " - "ipfs provider. Follow the instructions on https://docs.pinata.cloud/docs/getting-started " + "You are using the old Web3 Storage API key. Please login again using `algokit task ipfs login` with " + "Pinata ipfs provider. Follow the instructions on https://docs.pinata.cloud/docs/getting-started" "to create an account and obtain a JWT." ) keyring.delete_password("algokit_web3_storage", "algokit_web3_storage_access_token") diff --git a/src/algokit/core/utils.py b/src/algokit/core/utils.py index b1770704..ff78a3fd 100644 --- a/src/algokit/core/utils.py +++ b/src/algokit/core/utils.py @@ -1,5 +1,14 @@ +import codecs import re import socket +import sys +import threading +import time +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +CLEAR_LINE = "\033[K" def extract_version_triple(version_str: str) -> str: @@ -26,3 +35,44 @@ def is_network_available(host: str = "8.8.8.8", port: int = 53, timeout: float = return True except OSError: return False + + +def animate(name: str, stop_event: threading.Event) -> None: + spinner = { + "interval": 100, + "frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + } + + while not stop_event.is_set(): + for frame in spinner["frames"]: # type: ignore # noqa: PGH003 + if stop_event.is_set(): + break + try: + text = codecs.decode(frame, "utf-8") + except Exception: + text = frame + output = f"\r{text} {name}" + sys.stdout.write(output) + sys.stdout.write(CLEAR_LINE) + sys.stdout.flush() + time.sleep(0.001 * spinner["interval"]) # type: ignore # noqa: PGH003 + + sys.stdout.write("\r ") + + +def run_with_animation(target_function: Callable[[], Any], animation_text: str = "Loading") -> Any: # noqa: ANN401 + with ThreadPoolExecutor(max_workers=2) as executor: + stop_event = threading.Event() + animation_future = executor.submit(animate, animation_text, stop_event) + function_future = executor.submit(target_function) + + try: + result = function_future.result() + except Exception as e: + stop_event.set() + animation_future.result() + raise e + else: + stop_event.set() + animation_future.result() + return result diff --git a/tests/tasks/TestIpfsUpload.test_ipfs_upload_http_error.approved.txt b/tests/tasks/TestIpfsUpload.test_ipfs_upload_http_error.approved.txt index 9b2fc92e..b75254ab 100644 --- a/tests/tasks/TestIpfsUpload.test_ipfs_upload_http_error.approved.txt +++ b/tests/tasks/TestIpfsUpload.test_ipfs_upload_http_error.approved.txt @@ -1,8 +1,5 @@ +⠋ UploadingHTTP Request: POST https://api.pinata.cloud/pinning/pinFileToIPFS "HTTP/1.1 500 Internal Server Error" - -⠋ UploadingDEBUG: HTTP Request: POST https://api.pinata.cloud/pinning/pinFileToIPFS "HTTP/1.1 500 Internal Server Error" - - -DEBUG: Pinata error: 500. {"ok": false, "cid": "test"} + DEBUG: Pinata error: 500. {"ok": false, "cid": "test"} Error: PinataInternalServerError('Pinata error: 500') diff --git a/tests/tasks/TestIpfsUpload.test_ipfs_upload_successful.approved.txt b/tests/tasks/TestIpfsUpload.test_ipfs_upload_successful.approved.txt index d2a54405..e8785ab3 100644 --- a/tests/tasks/TestIpfsUpload.test_ipfs_upload_successful.approved.txt +++ b/tests/tasks/TestIpfsUpload.test_ipfs_upload_successful.approved.txt @@ -1,9 +1,5 @@ +⠋ UploadingHTTP Request: POST https://api.pinata.cloud/pinning/pinFileToIPFS "HTTP/1.1 200 OK" - -⠋ UploadingDEBUG: HTTP Request: POST https://api.pinata.cloud/pinning/pinFileToIPFS "HTTP/1.1 200 OK" - - -✅ Uploading -File uploaded successfully! -CID: test + File uploaded successfully! + CID: test diff --git a/tests/tasks/test_mint.test_mint_token_no_pinata_jwt_error.approved.txt b/tests/tasks/test_mint.test_mint_token_no_pinata_jwt_error.approved.txt index c3f5e185..044d3c9d 100644 --- a/tests/tasks/test_mint.test_mint_token_no_pinata_jwt_error.approved.txt +++ b/tests/tasks/test_mint.test_mint_token_no_pinata_jwt_error.approved.txt @@ -1,2 +1,2 @@ Enter the mnemonic phrase (25 words separated by whitespace): -Error: You are not logged in! Please login using `algokit ipfs login`. +Error: You are not logged in! Please login using `algokit task ipfs login`.