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

This commit is contained in:
Administrator 2022-04-01 22:56:17 +00:00
parent 972f0b0a61
commit 8d00120a13
6 changed files with 109 additions and 90 deletions

View File

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ### Removed
- affiliate_amount - affiliate_amount

View File

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

View File

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

View File

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

View File

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

View File

@ -31,41 +31,12 @@ def test_launch(mock_api_request: MagicMock) -> None:
ssh_key="id-rsa...", ssh_key="id-rsa...",
flavor="aflavor", 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") @patch("sporestack.api_client._api_request")
def test_topup(mock_api_request: MagicMock) -> None: def test_topup(mock_api_request: MagicMock) -> None:
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
api_client.topup("dummymachineid", currency="xmr", days=1) 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") @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: def test_token_enable(mock_api_request: MagicMock) -> None:
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
api_client.token_enable("dummytoken", currency="xmr", dollars=20) 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( mock_api_request.assert_called_once_with(
url="https://api.sporestack.com/token/dummytoken/enable", url="https://api.sporestack.com/token/dummytoken/enable",
json_params=json_params, json_params=json_params,