Browse Source

6.0.0a2: --quote/--no-quote

master 6.0.0a2
SporeStack 3 months ago
parent
commit
8d00120a13
  1. 6
      CHANGELOG.md
  2. 2
      setup.cfg
  3. 58
      src/sporestack/api.py
  4. 8
      src/sporestack/api_client.py
  5. 94
      src/sporestack/cli.py
  6. 31
      tests/test_api_client.py

6
CHANGELOG.md

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [6.0.0a2 - 2022-04-01]
### Added
- `--quote` / `--no-quote` to launch/topup. Prompt by default if price to draw from token is acceptable.
### Removed
- affiliate_amount

2
setup.cfg

@ -1,6 +1,6 @@
[metadata]
name = sporestack
version = 6.0.0a1
version = 6.0.0a2
description = SporeStack.com library and client. Launch servers with Monero or Bitcoin.
long_description = file: README.md
long_description_content_type = text/markdown

58
src/sporestack/api.py

@ -11,6 +11,8 @@ from pydantic import BaseModel
from .models import NetworkInterface, Payment
LATEST_API_VERSION = 3
class TokenEnable:
url = "/token/{token}/enable"
@ -19,6 +21,7 @@ class TokenEnable:
class Request(BaseModel):
currency: str
dollars: int
affiliate_token: Optional[str] = None
class Response(BaseModel):
token: str
@ -32,6 +35,7 @@ class TokenAdd:
class Request(BaseModel):
currency: str
dollars: int
affiliate_token: Optional[str] = None
class Response(BaseModel):
token: str
@ -55,29 +59,35 @@ class ServerLaunch:
class Request(BaseModel):
machine_id: str
days: int
currency: str
flavor: str
ssh_key: str
operating_system: str
region: Optional[str]
organization: Optional[str]
settlement_token: Optional[str]
affiliate_token: Optional[str]
currency: Optional[str] = None
"""Currency only needs to be set if not paying with a token."""
region: Optional[str] = None
organization: Optional[str] = None
token: Optional[str] = None
quote: bool = False
affiliate_token: Optional[str] = None
affiliate_amount: None = None
"""Deprecated field"""
settlement_token: Optional[str] = None
"""Deprecated field. Use token instead."""
class Response(BaseModel):
created_at: Optional[int]
payment: Payment
expiration: Optional[int]
expiration: int
machine_id: str
network_interfaces: List[NetworkInterface]
region: str
latest_api_version: int
created: bool
paid: bool
warning: Optional[str]
txid: Optional[str]
operating_system: str
flavor: str
network_interfaces: List[NetworkInterface] = []
created_at: int = 0
region: Optional[str] = None
latest_api_version: int = LATEST_API_VERSION
created: bool = False
paid: bool = False
warning: Optional[str] = None
txid: Optional[str] = None
class ServerTopup:
@ -87,18 +97,24 @@ class ServerTopup:
class Request(BaseModel):
machine_id: str
days: int
currency: str
settlement_token: Optional[str]
affiliate_token: Optional[str]
token: Optional[str] = None
quote: bool = False
currency: Optional[str] = None
"""Currency only needs to be set if not paying with a token."""
affiliate_token: Optional[str] = None
affiliate_amount: None = None
"""Deprecated field"""
settlement_token: Optional[str] = None
"""Deprecated field. Use token instead."""
class Response(BaseModel):
machine_id: str
payment: Payment
paid: bool
warning: Optional[str]
expiration: int
txid: Optional[str]
latest_api_version: int
paid: bool = False
warning: Optional[str] = None
txid: Optional[str] = None
latest_api_version: int = LATEST_API_VERSION
class ServerInfo:

8
src/sporestack/api_client.py

@ -140,17 +140,19 @@ def launch(
token: Optional[str] = None,
retry: bool = False,
affiliate_token: Optional[str] = None,
quote: bool = False,
) -> api.ServerLaunch.Response:
request = api.ServerLaunch.Request(
machine_id=machine_id,
days=days,
currency=currency,
settlement_token=token,
token=token,
affiliate_token=affiliate_token,
flavor=flavor,
region=region,
operating_system=operating_system,
ssh_key=ssh_key,
quote=quote,
)
url = api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id)
response = _api_request(url=url, json_params=request.dict(), retry=retry)
@ -167,6 +169,7 @@ def topup(
token: Optional[str] = None,
retry: bool = False,
affiliate_token: Optional[str] = None,
quote: bool = False,
) -> api.ServerTopup.Response:
"""
Topup a server.
@ -175,8 +178,9 @@ def topup(
machine_id=machine_id,
days=days,
currency=currency,
settlement_token=token,
token=token,
affiliate_token=affiliate_token,
quote=quote,
)
url = api_endpoint + api.ServerTopup.url.format(machine_id=machine_id)
response = _api_request(url=url, json_params=request.dict(), retry=retry)

94
src/sporestack/cli.py

@ -116,9 +116,10 @@ def launch(
flavor: str = DEFAULT_FLAVOR,
token: str = DEFAULT_TOKEN,
region: Optional[str] = None,
quote: bool = typer.Option(True, help="Require manual price confirmation."),
) -> None:
"""
Attempts to launch a server.
Launch a server on SporeStack.
"""
from . import utils
@ -141,40 +142,45 @@ def launch(
machine_id = utils.random_machine_id()
response = api_client.launch(
machine_id=machine_id,
days=days,
flavor=flavor,
operating_system=operating_system,
ssh_key=ssh_key,
currency="settlement",
region=region,
token=_token,
api_endpoint=get_api_endpoint(),
retry=True,
)
if quote:
response = api_client.launch(
machine_id=machine_id,
days=days,
flavor=flavor,
operating_system=operating_system,
ssh_key=ssh_key,
currency="settlement",
region=region,
token=_token,
api_endpoint=get_api_endpoint(),
retry=True,
quote=True,
)
if response.created is False:
tries = 360
while tries > 0:
typer.echo("Waiting for server to build...", err=True)
tries = tries + 1
# Waiting for server to spin up.
time.sleep(10)
response = api_client.launch(
machine_id=machine_id,
days=days,
flavor=flavor,
operating_system=operating_system,
ssh_key=ssh_key,
currency="settlement",
region=region,
token=_token,
api_endpoint=get_api_endpoint(),
retry=True,
)
if response.created is True:
break
msg = f"Is {response.payment.usd} for {days} day(s) of {flavor} okay?"
typer.echo(msg, err=True)
input("[Press ctrl+c to cancel, or enter to accept.]")
tries = 360
while tries > 0:
response = api_client.launch(
machine_id=machine_id,
days=days,
flavor=flavor,
operating_system=operating_system,
ssh_key=ssh_key,
currency="settlement",
region=region,
token=_token,
api_endpoint=get_api_endpoint(),
retry=True,
)
if response.created is True:
break
typer.echo("Waiting for server to build...", err=True)
tries = tries + 1
# Waiting for server to spin up.
time.sleep(10)
if response.created is False:
typer.echo("Server creation failed, tries exceeded.", err=True)
@ -192,9 +198,10 @@ def topup(
hostname: str,
days: int = typer.Option(...),
token: str = DEFAULT_TOKEN,
quote: bool = typer.Option(True, help="Require manual price confirmation."),
) -> None:
"""
tops up an existing vm.
Extend an existing SporeStack server's lifetime.
"""
if not machine_exists(hostname):
@ -206,6 +213,20 @@ def topup(
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if quote:
response = api_client.topup(
machine_id=machine_id,
days=days,
currency="settlement",
api_endpoint=get_api_endpoint(),
token=_token,
retry=True,
quote=True,
)
typer.echo(f"Is {response.payment.usd} for {days} day(s) okay?", err=True)
input("[Press ctrl+c to cancel, or enter to accept.]")
response = api_client.topup(
machine_id=machine_id,
days=days,
@ -214,6 +235,7 @@ def topup(
token=_token,
retry=True,
)
assert response.payment.paid is True
machine_info["expiration"] = response.expiration
save_machine_info(machine_info, overwrite=True)
@ -428,7 +450,7 @@ def load_token(token: str) -> str:
def save_token(token: str, key: str) -> None:
token_file = token_path().joinpath(f"{token}.json")
if token_file.exists():
msg = "Token '{token}' already exists in {token_file}. Aborting!"
msg = f"Token '{token}' already exists in {token_file}. Aborting!"
typer.echo(msg, err=True)
raise typer.Exit(code=1)

31
tests/test_api_client.py

@ -31,41 +31,12 @@ def test_launch(mock_api_request: MagicMock) -> None:
ssh_key="id-rsa...",
flavor="aflavor",
)
json_params = {
"machine_id": "dummymachineid",
"days": 1,
"currency": "xmr",
"flavor": "aflavor",
"ssh_key": "id-rsa...",
"operating_system": "freebsd-12",
"region": None,
"organization": None,
"settlement_token": None,
"affiliate_token": None,
}
mock_api_request.assert_called_once_with(
url="https://api.sporestack.com/server/dummymachineid/launch",
json_params=json_params,
retry=False,
)
@patch("sporestack.api_client._api_request")
def test_topup(mock_api_request: MagicMock) -> None:
with pytest.raises(ValidationError):
api_client.topup("dummymachineid", currency="xmr", days=1)
json_params = {
"machine_id": "dummymachineid",
"days": 1,
"currency": "xmr",
"settlement_token": None,
"affiliate_token": None,
}
mock_api_request.assert_called_once_with(
url="https://api.sporestack.com/server/dummymachineid/topup",
json_params=json_params,
retry=False,
)
@patch("sporestack.api_client._api_request")
@ -122,7 +93,7 @@ def test_token_balance(mock_api_request: MagicMock) -> None:
def test_token_enable(mock_api_request: MagicMock) -> None:
with pytest.raises(ValidationError):
api_client.token_enable("dummytoken", currency="xmr", dollars=20)
json_params = {"currency": "xmr", "dollars": 20}
json_params = {"currency": "xmr", "dollars": 20, "affiliate_token": None}
mock_api_request.assert_called_once_with(
url="https://api.sporestack.com/token/dummytoken/enable",
json_params=json_params,

Loading…
Cancel
Save