|
|
|
@ -1,8 +1,7 @@
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from time import sleep
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
from pydantic import parse_obj_as
|
|
|
|
@ -59,6 +58,40 @@ def _is_onion_url(url: str) -> bool:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_response_error_text(response: httpx.Response) -> str:
|
|
|
|
|
"""Get a response's error text. Assumes the response is actually an error."""
|
|
|
|
|
if (
|
|
|
|
|
"content-type" in response.headers
|
|
|
|
|
and response.headers["content-type"] == "application/json"
|
|
|
|
|
):
|
|
|
|
|
error = response.json()
|
|
|
|
|
if "detail" in error:
|
|
|
|
|
if isinstance(error["detail"], str):
|
|
|
|
|
return error["detail"]
|
|
|
|
|
else:
|
|
|
|
|
return str(error["detail"])
|
|
|
|
|
|
|
|
|
|
return response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_response(response: httpx.Response) -> None:
|
|
|
|
|
status_code_first_digit = response.status_code // 100
|
|
|
|
|
if status_code_first_digit == 2:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
error_response_text = _get_response_error_text(response)
|
|
|
|
|
if response.status_code == 429:
|
|
|
|
|
raise exceptions.SporeStackTooManyRequestsError(error_response_text)
|
|
|
|
|
elif status_code_first_digit == 4:
|
|
|
|
|
raise exceptions.SporeStackServerError(error_response_text)
|
|
|
|
|
elif status_code_first_digit == 5:
|
|
|
|
|
# User should probably retry.
|
|
|
|
|
raise exceptions.SporeStackServerError(error_response_text)
|
|
|
|
|
else:
|
|
|
|
|
# How did we get here?
|
|
|
|
|
raise exceptions.SporeStackServerError(error_response_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class APIClient:
|
|
|
|
|
api_endpoint: str = API_ENDPOINT
|
|
|
|
@ -70,67 +103,6 @@ class APIClient:
|
|
|
|
|
proxy = _get_tor_proxy()
|
|
|
|
|
self._httpx_client = httpx.Client(headers=headers, proxies=proxy)
|
|
|
|
|
|
|
|
|
|
def _api_request(
|
|
|
|
|
self,
|
|
|
|
|
url: str,
|
|
|
|
|
empty_post: bool = False,
|
|
|
|
|
json_params: Optional[Dict[str, Any]] = None,
|
|
|
|
|
params: Optional[Dict[str, Any]] = None,
|
|
|
|
|
retry: bool = False,
|
|
|
|
|
) -> Any:
|
|
|
|
|
try:
|
|
|
|
|
if empty_post is True:
|
|
|
|
|
request = self._httpx_client.post(url, timeout=POST_TIMEOUT)
|
|
|
|
|
elif json_params is None:
|
|
|
|
|
request = self._httpx_client.get(url, timeout=GET_TIMEOUT)
|
|
|
|
|
else:
|
|
|
|
|
request = self._httpx_client.post(
|
|
|
|
|
url,
|
|
|
|
|
json=json_params,
|
|
|
|
|
timeout=POST_TIMEOUT,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if retry is True:
|
|
|
|
|
log.warning(f"Got an error, but retrying: {e}")
|
|
|
|
|
sleep(5)
|
|
|
|
|
# Try again.
|
|
|
|
|
return self._api_request(
|
|
|
|
|
url,
|
|
|
|
|
empty_post=empty_post,
|
|
|
|
|
json_params=json_params,
|
|
|
|
|
retry=retry,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
status_code_first_digit = request.status_code // 100
|
|
|
|
|
if status_code_first_digit == 2:
|
|
|
|
|
try:
|
|
|
|
|
return request.json()
|
|
|
|
|
except Exception:
|
|
|
|
|
return request.content
|
|
|
|
|
elif status_code_first_digit == 4:
|
|
|
|
|
log.debug("HTTP status code: {request.status_code}")
|
|
|
|
|
raise exceptions.SporeStackUserError(request.content.decode("utf-8"))
|
|
|
|
|
elif status_code_first_digit == 5:
|
|
|
|
|
if retry is True:
|
|
|
|
|
log.warning(request.content.decode("utf-8"))
|
|
|
|
|
log.warning("Got a 500, retrying in 5 seconds...")
|
|
|
|
|
sleep(5)
|
|
|
|
|
# Try again if we get a 500
|
|
|
|
|
return self._api_request(
|
|
|
|
|
url,
|
|
|
|
|
empty_post=empty_post,
|
|
|
|
|
json_params=json_params,
|
|
|
|
|
retry=retry,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise exceptions.SporeStackServerError(str(request.content))
|
|
|
|
|
else:
|
|
|
|
|
# Not sure why we'd get this.
|
|
|
|
|
request.raise_for_status()
|
|
|
|
|
raise Exception("Stuff broke strangely. Please contact SporeStack support.")
|
|
|
|
|
|
|
|
|
|
def server_launch(
|
|
|
|
|
self,
|
|
|
|
|
machine_id: str,
|
|
|
|
@ -155,31 +127,30 @@ class APIClient:
|
|
|
|
|
autorenew=autorenew,
|
|
|
|
|
)
|
|
|
|
|
url = self.api_endpoint + api.ServerLaunch.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url=url, json_params=request.dict())
|
|
|
|
|
response = self._httpx_client.post(url=url, json=request.dict())
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_topup(
|
|
|
|
|
self,
|
|
|
|
|
machine_id: str,
|
|
|
|
|
days: int,
|
|
|
|
|
token: str,
|
|
|
|
|
token: str | None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Topup a server."""
|
|
|
|
|
request = api.ServerTopup.Request(days=days, token=token)
|
|
|
|
|
url = self.api_endpoint + api.ServerTopup.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url=url, json_params=request.dict())
|
|
|
|
|
response = self._httpx_client.post(url=url, json=request.dict())
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_quote(self, days: int, flavor: str) -> api.ServerQuote.Response:
|
|
|
|
|
"""Get a quote for how much a server will cost."""
|
|
|
|
|
|
|
|
|
|
url = self.api_endpoint + api.ServerQuote.url
|
|
|
|
|
response = self._httpx_client.get(
|
|
|
|
|
url=url,
|
|
|
|
|
url,
|
|
|
|
|
params={"days": days, "flavor": flavor},
|
|
|
|
|
)
|
|
|
|
|
if response.status_code == 422:
|
|
|
|
|
raise exceptions.SporeStackUserError(response.json()["detail"])
|
|
|
|
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
return api.ServerQuote.Response.parse_obj(response.json())
|
|
|
|
|
|
|
|
|
|
def autorenew_enable(self, machine_id: str) -> None:
|
|
|
|
@ -189,7 +160,8 @@ class APIClient:
|
|
|
|
|
url = self.api_endpoint + api.ServerEnableAutorenew.url.format(
|
|
|
|
|
machine_id=machine_id
|
|
|
|
|
)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def autorenew_disable(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
@ -198,35 +170,40 @@ class APIClient:
|
|
|
|
|
url = self.api_endpoint + api.ServerDisableAutorenew.url.format(
|
|
|
|
|
machine_id=machine_id
|
|
|
|
|
)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_start(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Power on the server.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerStart.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_stop(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Power off the server.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerStop.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_delete(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Delete the server.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerDelete.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_forget(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Forget about a destroyed/deleted server.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerForget.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_rebuild(self, machine_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
@ -235,15 +212,17 @@ class APIClient:
|
|
|
|
|
Deletes all of the data on the server!
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerRebuild.url.format(machine_id=machine_id)
|
|
|
|
|
self._api_request(url, empty_post=True)
|
|
|
|
|
response = self._httpx_client.post(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
def server_info(self, machine_id: str) -> api.ServerInfo.Response:
|
|
|
|
|
"""
|
|
|
|
|
Returns info about the server.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServerInfo.url.format(machine_id=machine_id)
|
|
|
|
|
response = self._api_request(url)
|
|
|
|
|
response_object = api.ServerInfo.Response.parse_obj(response)
|
|
|
|
|
response = self._httpx_client.get(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.ServerInfo.Response.parse_obj(response.json())
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def servers_launched_from_token(
|
|
|
|
@ -253,22 +232,27 @@ class APIClient:
|
|
|
|
|
Returns info of servers launched from a given token.
|
|
|
|
|
"""
|
|
|
|
|
url = self.api_endpoint + api.ServersLaunchedFromToken.url.format(token=token)
|
|
|
|
|
response = self._api_request(url)
|
|
|
|
|
response_object = api.ServersLaunchedFromToken.Response.parse_obj(response)
|
|
|
|
|
response = self._httpx_client.get(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.ServersLaunchedFromToken.Response.parse_obj(
|
|
|
|
|
response.json()
|
|
|
|
|
)
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def flavors(self) -> api.Flavors.Response:
|
|
|
|
|
"""Returns available flavors (server sizes)."""
|
|
|
|
|
url = self.api_endpoint + api.Flavors.url
|
|
|
|
|
response = self._api_request(url)
|
|
|
|
|
response_object = api.Flavors.Response.parse_obj(response)
|
|
|
|
|
response = self._httpx_client.get(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.Flavors.Response.parse_obj(response.json())
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def operating_systems(self) -> api.OperatingSystems.Response:
|
|
|
|
|
"""Returns available operating systems."""
|
|
|
|
|
url = self.api_endpoint + api.OperatingSystems.url
|
|
|
|
|
response = self._api_request(url)
|
|
|
|
|
response_object = api.OperatingSystems.Response.parse_obj(response)
|
|
|
|
|
response = self._httpx_client.get(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.OperatingSystems.Response.parse_obj(response.json())
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def token_add(
|
|
|
|
@ -276,30 +260,28 @@ class APIClient:
|
|
|
|
|
token: str,
|
|
|
|
|
dollars: int,
|
|
|
|
|
currency: str,
|
|
|
|
|
retry: bool = False,
|
|
|
|
|
) -> api.TokenAdd.Response:
|
|
|
|
|
"""Add balance (money) to a token."""
|
|
|
|
|
request = api.TokenAdd.Request(dollars=dollars, currency=currency)
|
|
|
|
|
url = self.api_endpoint + api.TokenAdd.url.format(token=token)
|
|
|
|
|
response = self._api_request(url=url, json_params=request.dict(), retry=retry)
|
|
|
|
|
response_object = api.TokenAdd.Response.parse_obj(response)
|
|
|
|
|
request = api.TokenAdd.Request(dollars=dollars, currency=currency)
|
|
|
|
|
response = self._httpx_client.post(url, json=request.dict())
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.TokenAdd.Response.parse_obj(response.json())
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def token_balance(self, token: str) -> api.TokenBalance.Response:
|
|
|
|
|
"""Return a token's balance."""
|
|
|
|
|
url = self.api_endpoint + api.TokenBalance.url.format(token=token)
|
|
|
|
|
response = self._api_request(url=url)
|
|
|
|
|
response_object = api.TokenBalance.Response.parse_obj(response)
|
|
|
|
|
response = self._httpx_client.get(url)
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
response_object = api.TokenBalance.Response.parse_obj(response.json())
|
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
|
|
def token_get_messages(self, token: str) -> List[api.TokenMessage]:
|
|
|
|
|
"""Get messages for/from the token."""
|
|
|
|
|
url = self.api_endpoint + f"/token/{token}/messages"
|
|
|
|
|
log.debug(f"Token send message URL: {url}")
|
|
|
|
|
response = self._httpx_client.get(url=url)
|
|
|
|
|
if response.status_code == 422:
|
|
|
|
|
raise exceptions.SporeStackUserError(response.json()["detail"])
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
_handle_response(response)
|
|
|
|
|
|
|
|
|
|
return parse_obj_as(List[api.TokenMessage], response.json())
|
|
|
|
|
|
|
|
|
@ -307,7 +289,4 @@ class APIClient:
|
|
|
|
|
"""Send a message to SporeStack support."""
|
|
|
|
|
url = self.api_endpoint + f"/token/{token}/messages"
|
|
|
|
|
response = self._httpx_client.post(url=url, json={"message": message})
|
|
|
|
|
if response.status_code == 422:
|
|
|
|
|
raise exceptions.SporeStackUserError(response.json()["detail"])
|
|
|
|
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
_handle_response(response)
|
|
|
|
|