v10.4.0: Implement Rich for prettier CLI output

This commit is contained in:
Administrator 2023-05-12 16:29:12 +00:00
parent 4311d86658
commit 5ff095af3f
7 changed files with 137 additions and 21 deletions

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -2,4 +2,4 @@
__all__ = ["api", "api_client", "exceptions"]
__version__ = "10.3.0"
__version__ = "10.4.0"

View File

@ -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"

View File

@ -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:
"""

View File

@ -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):