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.
|
- `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`.
|
- `--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]
|
## [Unreleased]
|
||||||
|
|
||||||
- Nothing yet.
|
- 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]
|
## [10.3.0 - 2023-05-12]
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
|
@ -36,6 +36,7 @@ sporestack server launch --no-quote --token importediminvalid --operating-system
|
||||||
sporestack server flavors | grep vcpu
|
sporestack server flavors | grep vcpu
|
||||||
sporestack server operating-systems | grep debian-11
|
sporestack server operating-systems | grep debian-11
|
||||||
sporestack server regions | grep sfo3
|
sporestack server regions | grep sfo3
|
||||||
|
sporestack api-changelog
|
||||||
|
|
||||||
if [ -z "$REAL_TESTING_TOKEN" ]; then
|
if [ -z "$REAL_TESTING_TOKEN" ]; then
|
||||||
rm -r $SPORESTACK_DIR
|
rm -r $SPORESTACK_DIR
|
||||||
|
|
|
@ -51,13 +51,22 @@ authors = [ {name = "SporeStack", email="support@sporestack.com"} ]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = "~=3.7"
|
requires-python = "~=3.7"
|
||||||
dynamic = ["version", "description"]
|
dynamic = ["version", "description"]
|
||||||
keywords = ["bitcoin", "monero", "vps"]
|
keywords = ["bitcoin", "monero", "vps", "server"]
|
||||||
license = {file = "LICENSE.txt"}
|
license = {file = "LICENSE.txt"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic",
|
"pydantic",
|
||||||
"httpx[socks]",
|
"httpx[socks]",
|
||||||
"segno",
|
"segno",
|
||||||
"typer",
|
"typer",
|
||||||
|
"rich",
|
||||||
|
]
|
||||||
|
|
||||||
|
# These will be made mandatory for v11
|
||||||
|
[project.optional-dependencies]
|
||||||
|
cli = [
|
||||||
|
"segno",
|
||||||
|
"typer",
|
||||||
|
"rich",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
__all__ = ["api", "api_client", "exceptions"]
|
__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()
|
@server_cli.command()
|
||||||
def flavors() -> None:
|
def flavors() -> None:
|
||||||
"""Shows available flavors."""
|
"""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
|
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())
|
api_client = APIClient(api_endpoint=get_api_endpoint())
|
||||||
flavors = api_client.flavors().flavors
|
flavors = api_client.flavors().flavors
|
||||||
for flavor in flavors:
|
for flavor_slug in flavors:
|
||||||
typer.echo(f"{flavor}: {flavors[flavor]}")
|
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()
|
@server_cli.command()
|
||||||
def operating_systems() -> None:
|
def operating_systems() -> None:
|
||||||
"""Show available operating systems."""
|
"""Show available operating systems."""
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
from .api_client import APIClient
|
from .api_client import APIClient
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
table = Table(show_header=True, header_style="bold magenta")
|
||||||
api_client = APIClient(api_endpoint=get_api_endpoint())
|
api_client = APIClient(api_endpoint=get_api_endpoint())
|
||||||
|
table.add_column("Operating System (--operating-system)")
|
||||||
os_list = api_client.operating_systems().operating_systems
|
os_list = api_client.operating_systems().operating_systems
|
||||||
for operating_system in os_list:
|
for operating_system in os_list:
|
||||||
typer.echo(f"{operating_system}")
|
table.add_row(operating_system)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
@server_cli.command()
|
@server_cli.command()
|
||||||
def regions() -> None:
|
def regions() -> None:
|
||||||
"""Shows regions that servers can be launched in."""
|
"""Shows regions that servers can be launched in."""
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
from .api_client import APIClient
|
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())
|
api_client = APIClient(api_endpoint=get_api_endpoint())
|
||||||
regions = api_client.regions().regions
|
regions = api_client.regions().regions
|
||||||
for region in 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:
|
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())
|
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")
|
@token_cli.command(name="info")
|
||||||
def token_info(token: str = typer.Argument(DEFAULT_TOKEN)) -> None:
|
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)
|
_token = load_token(token)
|
||||||
|
|
||||||
|
from rich import print
|
||||||
|
|
||||||
from .api_client import APIClient
|
from .api_client import APIClient
|
||||||
from .client import Client
|
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)
|
client = Client(api_client=api_client, client_token=_token)
|
||||||
|
|
||||||
info = client.token.info()
|
info = client.token.info()
|
||||||
typer.echo(f"Balance: {info.balance_usd} ({info.balance_cents} cents)")
|
print(f"[bold]Token Information for {token} ({_token})[/bold]")
|
||||||
typer.echo(f"Total servers: {info.servers}")
|
print(f"Balance: [green]{info.balance_usd}")
|
||||||
typer.echo(f"Burn rate: {info.burn_rate_usd} per day (of servers set to autorenew)")
|
print(f"Total Servers: {info.servers}")
|
||||||
typer.echo(
|
print(
|
||||||
f"Days remaining: {info.days_remaining} (for servers set to autorenew, "
|
f"Burn Rate: [red]{info.burn_rate_usd}[/red] "
|
||||||
"given the remaining balance)"
|
"(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__)
|
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()
|
@cli.command()
|
||||||
def api_endpoint() -> None:
|
def api_endpoint() -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -10,11 +10,6 @@ from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class NetworkInterface(BaseModel):
|
|
||||||
ipv4: str
|
|
||||||
ipv6: str
|
|
||||||
|
|
||||||
|
|
||||||
class Payment(BaseModel):
|
class Payment(BaseModel):
|
||||||
txid: Optional[str]
|
txid: Optional[str]
|
||||||
uri: Optional[str]
|
uri: Optional[str]
|
||||||
|
@ -37,8 +32,9 @@ class Flavor(BaseModel):
|
||||||
ipv4: str
|
ipv4: str
|
||||||
# IPv6 connectivity: "/128"
|
# IPv6 connectivity: "/128"
|
||||||
ipv6: str
|
ipv6: str
|
||||||
# Gigabytes of bandwidth per day
|
"""Gigabytes of bandwidth per day."""
|
||||||
bandwidth: int
|
bandwidth_per_month: float
|
||||||
|
"""Gigabytes of bandwidth per month."""
|
||||||
|
|
||||||
|
|
||||||
class OperatingSystem(BaseModel):
|
class OperatingSystem(BaseModel):
|
||||||
|
|
Loading…
Reference in New Issue