diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a1320..a5a9ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `burn_rate` from `TokenInfo` is deprecated. Use `burn_rate_cents` or `burn_rate_usd` instead. - `--no-local` will become the default for `sporestack server list`. +- If you want the CLI features, you will have to `pip install sporestack[cli]` instead of just `pip install sporestack`. ## [Unreleased] - Nothing yet. +## [10.4.0 - 2023-05-12] + +## Changed + +- `pip install sporestack[cli]` recommended if you wish to use CLI features. This will be required in version 11. +- Implement [Rich](https://github.com/Textualize/rich) for much prettier output on `token info`, `server regions`, `server flavors`, and `server operating-systems`. Other commands to follow. + ## [10.3.0 - 2023-05-12] ## Added diff --git a/integration-test.sh b/integration-test.sh index 048977c..903ec2f 100755 --- a/integration-test.sh +++ b/integration-test.sh @@ -36,6 +36,7 @@ sporestack server launch --no-quote --token importediminvalid --operating-system sporestack server flavors | grep vcpu sporestack server operating-systems | grep debian-11 sporestack server regions | grep sfo3 +sporestack api-changelog if [ -z "$REAL_TESTING_TOKEN" ]; then rm -r $SPORESTACK_DIR diff --git a/pyproject.toml b/pyproject.toml index f1fe66f..1e35c27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,13 +51,22 @@ authors = [ {name = "SporeStack", email="support@sporestack.com"} ] readme = "README.md" requires-python = "~=3.7" dynamic = ["version", "description"] -keywords = ["bitcoin", "monero", "vps"] +keywords = ["bitcoin", "monero", "vps", "server"] license = {file = "LICENSE.txt"} dependencies = [ "pydantic", "httpx[socks]", "segno", "typer", + "rich", +] + +# These will be made mandatory for v11 +[project.optional-dependencies] +cli = [ + "segno", + "typer", + "rich", ] [project.urls] diff --git a/src/sporestack/__init__.py b/src/sporestack/__init__.py index 42a969c..6ddee49 100644 --- a/src/sporestack/__init__.py +++ b/src/sporestack/__init__.py @@ -2,4 +2,4 @@ __all__ = ["api", "api_client", "exceptions"] -__version__ = "10.3.0" +__version__ = "10.4.0" diff --git a/src/sporestack/_cli_utils.py b/src/sporestack/_cli_utils.py new file mode 100644 index 0000000..684eee4 --- /dev/null +++ b/src/sporestack/_cli_utils.py @@ -0,0 +1,24 @@ +def cents_to_usd(cents: int) -> str: + """cents_to_usd: Convert cents to USD string.""" + return f"${cents * 0.01:,.2f}" + + +def mb_string(megabytes: int) -> str: + """Returns a formatted string for megabytes.""" + if megabytes < 1024: + return f"{megabytes} MiB" + + return f"{megabytes // 1024} GiB" + + +def gb_string(gigabytes: int) -> str: + """Returns a formatted string for gigabytes.""" + if gigabytes < 1000: + return f"{gigabytes} GiB" + + return f"{gigabytes / 1000} TiB" + + +def tb_string(terabytes: float) -> str: + """Returns a formatted string for terabytes.""" + return f"{terabytes} TiB" diff --git a/src/sporestack/cli.py b/src/sporestack/cli.py index f22276e..8346f1c 100644 --- a/src/sporestack/cli.py +++ b/src/sporestack/cli.py @@ -556,34 +556,81 @@ def rebuild( @server_cli.command() def flavors() -> None: """Shows available flavors.""" + from rich.console import Console + from rich.table import Table + + from ._cli_utils import cents_to_usd, gb_string, mb_string, tb_string from .api_client import APIClient + console = Console() + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Flavor Slug (--flavor)") + table.add_column("vCPU Cores") + table.add_column("Memory") + table.add_column("Disk") + table.add_column("Bandwidth (per month)") + table.add_column("Price per day") + table.add_column("Price per month (30 days)") + api_client = APIClient(api_endpoint=get_api_endpoint()) flavors = api_client.flavors().flavors - for flavor in flavors: - typer.echo(f"{flavor}: {flavors[flavor]}") + for flavor_slug in flavors: + flavor = flavors[flavor_slug] + price_per_30_days = flavor.price * 30 + table.add_row( + flavor_slug, + str(flavor.cores), + mb_string(flavor.memory), + gb_string(flavor.disk), + tb_string(flavor.bandwidth_per_month), + f"[green]{cents_to_usd(flavor.price)}[/green]", + f"[green]{cents_to_usd(price_per_30_days)}[/green]", + ) + + console.print(table) @server_cli.command() def operating_systems() -> None: """Show available operating systems.""" + from rich.console import Console + from rich.table import Table + from .api_client import APIClient + console = Console() + + table = Table(show_header=True, header_style="bold magenta") api_client = APIClient(api_endpoint=get_api_endpoint()) + table.add_column("Operating System (--operating-system)") os_list = api_client.operating_systems().operating_systems for operating_system in os_list: - typer.echo(f"{operating_system}") + table.add_row(operating_system) + + console.print(table) @server_cli.command() def regions() -> None: """Shows regions that servers can be launched in.""" + from rich.console import Console + from rich.table import Table + from .api_client import APIClient + console = Console() + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Region Slug (--region)") + table.add_column("Region Name") + api_client = APIClient(api_endpoint=get_api_endpoint()) regions = api_client.regions().regions for region in regions: - typer.echo(f"{region}: {regions[region].name}") + table.add_row(region, regions[region].name) + + console.print(table) def load_token(token: str) -> str: @@ -744,14 +791,22 @@ def balance(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: api_client = APIClient(api_endpoint=get_api_endpoint()) - typer.echo(api_client.token_balance(token=_token).usd) + typer.echo(api_client.token_info(token=_token).balance_usd) @token_cli.command(name="info") def token_info(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: - """Show information about a token, including balance.""" + """ + Show information about a token, including balance. + + Burn Rate is calculated per day of servers set to autorenew. + + Days Remaining is for servers set to autorenew, given the remaining balance. + """ _token = load_token(token) + from rich import print + from .api_client import APIClient from .client import Client @@ -759,12 +814,16 @@ def token_info(token: str = typer.Argument(DEFAULT_TOKEN)) -> None: client = Client(api_client=api_client, client_token=_token) info = client.token.info() - typer.echo(f"Balance: {info.balance_usd} ({info.balance_cents} cents)") - typer.echo(f"Total servers: {info.servers}") - typer.echo(f"Burn rate: {info.burn_rate_usd} per day (of servers set to autorenew)") - typer.echo( - f"Days remaining: {info.days_remaining} (for servers set to autorenew, " - "given the remaining balance)" + print(f"[bold]Token Information for {token} ({_token})[/bold]") + print(f"Balance: [green]{info.balance_usd}") + print(f"Total Servers: {info.servers}") + print( + f"Burn Rate: [red]{info.burn_rate_usd}[/red] " + "(per day of servers set to autorenew)" + ) + print( + f"Days Remaining: {info.days_remaining} " + "(for servers set to autorenew, given the remaining balance)" ) @@ -834,6 +893,25 @@ def version() -> None: typer.echo(__version__) +@cli.command() +def api_changelog() -> None: + """Shows the API changelog.""" + from rich.console import Console + from rich.markdown import Markdown + + from .api_client import APIClient + + api_client = APIClient(api_endpoint=get_api_endpoint()) + console = Console() + console.print(Markdown(api_client.changelog())) + + +# TODO +# @cli.command() +# def cli_changelog() -> None: +# """Shows the Python library/CLI changelog.""" + + @cli.command() def api_endpoint() -> None: """ diff --git a/src/sporestack/models.py b/src/sporestack/models.py index b1d9685..757c312 100644 --- a/src/sporestack/models.py +++ b/src/sporestack/models.py @@ -10,11 +10,6 @@ from typing import Optional from pydantic import BaseModel -class NetworkInterface(BaseModel): - ipv4: str - ipv6: str - - class Payment(BaseModel): txid: Optional[str] uri: Optional[str] @@ -37,8 +32,9 @@ class Flavor(BaseModel): ipv4: str # IPv6 connectivity: "/128" ipv6: str - # Gigabytes of bandwidth per day - bandwidth: int + """Gigabytes of bandwidth per day.""" + bandwidth_per_month: float + """Gigabytes of bandwidth per month.""" class OperatingSystem(BaseModel):