6.0.0a2: --quote/--no-quote
This commit is contained in:
parent
972f0b0a61
commit
8d00120a13
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue