v10.4.0: Implement Rich for prettier CLI output
This commit is contained in:
parent
4311d86658
commit
5ff095af3f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
__all__ = ["api", "api_client", "exceptions"]
|
||||
|
||||
__version__ = "10.3.0"
|
||||
__version__ = "10.4.0"
|
||||
|
|
|
@ -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"
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue