borealhost-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of borealhost-sdk might be problematic. Click here for more details.
- borealhost_sdk-0.1.0/.gitignore +36 -0
- borealhost_sdk-0.1.0/PKG-INFO +99 -0
- borealhost_sdk-0.1.0/README.md +77 -0
- borealhost_sdk-0.1.0/borealhost_sdk/__init__.py +82 -0
- borealhost_sdk-0.1.0/borealhost_sdk/client.py +779 -0
- borealhost_sdk-0.1.0/borealhost_sdk/config.py +7 -0
- borealhost_sdk-0.1.0/borealhost_sdk/exceptions.py +19 -0
- borealhost_sdk-0.1.0/pyproject.toml +39 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
db.sqlite3
|
|
8
|
+
|
|
9
|
+
# Node
|
|
10
|
+
node_modules/
|
|
11
|
+
dist/
|
|
12
|
+
bun.lock
|
|
13
|
+
|
|
14
|
+
# Secrets
|
|
15
|
+
.env
|
|
16
|
+
*.key
|
|
17
|
+
*.pem
|
|
18
|
+
secrets/
|
|
19
|
+
.ssh/
|
|
20
|
+
|
|
21
|
+
# Claude Code
|
|
22
|
+
.claude/
|
|
23
|
+
.mcp.json
|
|
24
|
+
|
|
25
|
+
# Deploy artifacts & large files
|
|
26
|
+
*.tar.gz
|
|
27
|
+
*.tar
|
|
28
|
+
*.mo
|
|
29
|
+
|
|
30
|
+
# Backups
|
|
31
|
+
*.bak.*
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
*.swp
|
|
36
|
+
*~
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: borealhost-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the BorealHost.ai REST API — agent-native web hosting
|
|
5
|
+
Project-URL: Homepage, https://borealhost.ai
|
|
6
|
+
Project-URL: Documentation, https://borealhost.ai/api/v1/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/alainsvrd/platform
|
|
8
|
+
Author-email: BorealHost <hello@borealhost.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: api,borealhost,hosting,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# BorealHost Python SDK
|
|
24
|
+
|
|
25
|
+
Python SDK for the [BorealHost.ai](https://borealhost.ai) REST API.
|
|
26
|
+
|
|
27
|
+
BorealHost is an agent-native Quebec-based web hosting platform. This SDK wraps the full public REST API (`/api/v1/*`) — purchase hosting, deploy sites, manage DNS, register domains, run SSH commands, and scale infrastructure.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install borealhost-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from borealhost_sdk import Client
|
|
39
|
+
|
|
40
|
+
client = Client(api_key="bh_...")
|
|
41
|
+
|
|
42
|
+
# List plans (no auth needed)
|
|
43
|
+
plans = client.list_plans()
|
|
44
|
+
|
|
45
|
+
# Manage sites
|
|
46
|
+
sites = client.list_sites()
|
|
47
|
+
status = client.get_site("my-site")
|
|
48
|
+
client.deploy("my-site")
|
|
49
|
+
|
|
50
|
+
# DNS
|
|
51
|
+
client.add_domain_dns("example.com", record_type="A", value="1.2.3.4", subdomain="www")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
API keys are in the format `bh_<48 hex chars>`.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Pass at construction
|
|
60
|
+
client = Client(api_key="bh_...")
|
|
61
|
+
|
|
62
|
+
# Or via environment variable
|
|
63
|
+
# export BOREALHOST_API_KEY="bh_..."
|
|
64
|
+
client = Client()
|
|
65
|
+
|
|
66
|
+
# Or set later
|
|
67
|
+
client = Client()
|
|
68
|
+
client.set_api_key("bh_...")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Custom base URL
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
client = Client(api_key="bh_...", base_url="https://staging.borealhost.ai")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Errors
|
|
78
|
+
|
|
79
|
+
All errors from the API raise `ApiError`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from borealhost_sdk import Client, ApiError
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
client.deploy("unknown-site")
|
|
86
|
+
except ApiError as e:
|
|
87
|
+
print(e.code) # "NOT_FOUND"
|
|
88
|
+
print(e.message) # "Site 'unknown-site' not found"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Related packages
|
|
92
|
+
|
|
93
|
+
- [`borealhost`](https://pypi.org/project/borealhost/) — command-line tool (`bh`)
|
|
94
|
+
- [`borealhost-mcp`](https://pypi.org/project/borealhost-mcp/) — MCP server for AI agents
|
|
95
|
+
|
|
96
|
+
## Links
|
|
97
|
+
|
|
98
|
+
- API docs: https://borealhost.ai/api/v1/docs/
|
|
99
|
+
- Homepage: https://borealhost.ai
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# BorealHost Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [BorealHost.ai](https://borealhost.ai) REST API.
|
|
4
|
+
|
|
5
|
+
BorealHost is an agent-native Quebec-based web hosting platform. This SDK wraps the full public REST API (`/api/v1/*`) — purchase hosting, deploy sites, manage DNS, register domains, run SSH commands, and scale infrastructure.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install borealhost-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from borealhost_sdk import Client
|
|
17
|
+
|
|
18
|
+
client = Client(api_key="bh_...")
|
|
19
|
+
|
|
20
|
+
# List plans (no auth needed)
|
|
21
|
+
plans = client.list_plans()
|
|
22
|
+
|
|
23
|
+
# Manage sites
|
|
24
|
+
sites = client.list_sites()
|
|
25
|
+
status = client.get_site("my-site")
|
|
26
|
+
client.deploy("my-site")
|
|
27
|
+
|
|
28
|
+
# DNS
|
|
29
|
+
client.add_domain_dns("example.com", record_type="A", value="1.2.3.4", subdomain="www")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Authentication
|
|
33
|
+
|
|
34
|
+
API keys are in the format `bh_<48 hex chars>`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Pass at construction
|
|
38
|
+
client = Client(api_key="bh_...")
|
|
39
|
+
|
|
40
|
+
# Or via environment variable
|
|
41
|
+
# export BOREALHOST_API_KEY="bh_..."
|
|
42
|
+
client = Client()
|
|
43
|
+
|
|
44
|
+
# Or set later
|
|
45
|
+
client = Client()
|
|
46
|
+
client.set_api_key("bh_...")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Custom base URL
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
client = Client(api_key="bh_...", base_url="https://staging.borealhost.ai")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Errors
|
|
56
|
+
|
|
57
|
+
All errors from the API raise `ApiError`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from borealhost_sdk import Client, ApiError
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
client.deploy("unknown-site")
|
|
64
|
+
except ApiError as e:
|
|
65
|
+
print(e.code) # "NOT_FOUND"
|
|
66
|
+
print(e.message) # "Site 'unknown-site' not found"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Related packages
|
|
70
|
+
|
|
71
|
+
- [`borealhost`](https://pypi.org/project/borealhost/) — command-line tool (`bh`)
|
|
72
|
+
- [`borealhost-mcp`](https://pypi.org/project/borealhost-mcp/) — MCP server for AI agents
|
|
73
|
+
|
|
74
|
+
## Links
|
|
75
|
+
|
|
76
|
+
- API docs: https://borealhost.ai/api/v1/docs/
|
|
77
|
+
- Homepage: https://borealhost.ai
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""BorealHost Python SDK.
|
|
2
|
+
|
|
3
|
+
The primary interface is the :class:`Client` class::
|
|
4
|
+
|
|
5
|
+
from borealhost_sdk import Client
|
|
6
|
+
client = Client(api_key="bh_...")
|
|
7
|
+
client.list_plans()
|
|
8
|
+
|
|
9
|
+
A module-level flat function interface is also provided for backward
|
|
10
|
+
compatibility and convenience. These functions operate on a shared default
|
|
11
|
+
``Client`` instance::
|
|
12
|
+
|
|
13
|
+
import borealhost_sdk as bh
|
|
14
|
+
bh.set_session_key("bh_...")
|
|
15
|
+
bh.list_plans()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .client import Client
|
|
19
|
+
from .exceptions import ApiError
|
|
20
|
+
|
|
21
|
+
__all__ = ["Client", "ApiError", "set_session_key", "get_session_key"]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Shared default client for the module-level flat function interface ──
|
|
27
|
+
|
|
28
|
+
_default = Client()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_session_key(key):
|
|
32
|
+
"""Set the API key on the shared default client.
|
|
33
|
+
|
|
34
|
+
Equivalent to ``Client().set_api_key(key)`` but mutates the shared
|
|
35
|
+
default client used by all module-level function calls.
|
|
36
|
+
"""
|
|
37
|
+
_default.set_api_key(key)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_session_key():
|
|
41
|
+
"""Return the API key of the shared default client (or empty string)."""
|
|
42
|
+
return _default.get_api_key()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_default_client():
|
|
46
|
+
"""Return the shared default client (internal use)."""
|
|
47
|
+
return _default
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Module-level flat function shims ──
|
|
51
|
+
#
|
|
52
|
+
# Every public method on ``Client`` is re-exposed as a top-level function
|
|
53
|
+
# that delegates to the shared default client. This preserves the flat
|
|
54
|
+
# ``borealhost_sdk.list_plans(...)`` interface without duplicating code.
|
|
55
|
+
#
|
|
56
|
+
# Adding a new method to ``Client`` automatically exposes it at the module
|
|
57
|
+
# level — no bookkeeping required.
|
|
58
|
+
|
|
59
|
+
def _make_shim(name):
|
|
60
|
+
method = getattr(Client, name)
|
|
61
|
+
|
|
62
|
+
def shim(*args, **kwargs):
|
|
63
|
+
return getattr(_default, name)(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
shim.__name__ = name
|
|
66
|
+
shim.__qualname__ = name
|
|
67
|
+
shim.__doc__ = method.__doc__ or f"Delegates to Client.{name}() on the shared default client."
|
|
68
|
+
return shim
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
for _name in dir(Client):
|
|
72
|
+
if _name.startswith("_"):
|
|
73
|
+
continue
|
|
74
|
+
if _name in ("set_api_key", "get_api_key"):
|
|
75
|
+
# These correspond to set_session_key / get_session_key above.
|
|
76
|
+
continue
|
|
77
|
+
_attr = getattr(Client, _name)
|
|
78
|
+
if callable(_attr):
|
|
79
|
+
globals()[_name] = _make_shim(_name)
|
|
80
|
+
__all__.append(_name)
|
|
81
|
+
|
|
82
|
+
del _name, _attr
|
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
"""BorealHost REST API client.
|
|
2
|
+
|
|
3
|
+
Wraps the public REST API at https://borealhost.ai/api/v1/ with a single
|
|
4
|
+
``Client`` class. All 100+ endpoints are exposed as instance methods.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
|
|
8
|
+
from borealhost_sdk import Client
|
|
9
|
+
|
|
10
|
+
client = Client(api_key="bh_...")
|
|
11
|
+
plans = client.list_plans()
|
|
12
|
+
client.deploy("my-site")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from .config import DEFAULT_API_KEY, DEFAULT_BASE_URL, DEFAULT_TIMEOUT
|
|
18
|
+
from .exceptions import ApiError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Client:
|
|
22
|
+
"""HTTP client for the BorealHost REST API.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: API key in ``bh_<48 hex chars>`` format. Falls back to the
|
|
26
|
+
``BOREALHOST_API_KEY`` env var.
|
|
27
|
+
base_url: API root (default ``https://borealhost.ai``, override with
|
|
28
|
+
``BOREALHOST_BASE_URL`` env var).
|
|
29
|
+
timeout: Default HTTP timeout in seconds.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, api_key=None, base_url=None, timeout=DEFAULT_TIMEOUT):
|
|
33
|
+
self.api_key = api_key if api_key is not None else DEFAULT_API_KEY
|
|
34
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
35
|
+
self.api_base = f"{self.base_url}/api/v1"
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
# Checkout secrets — keyed by checkout_id, auto-stored on create_checkout.
|
|
38
|
+
self._checkout_secrets = {}
|
|
39
|
+
|
|
40
|
+
# ── Session / auth helpers ──
|
|
41
|
+
|
|
42
|
+
def set_api_key(self, key):
|
|
43
|
+
"""Set the API key used by subsequent authenticated calls."""
|
|
44
|
+
self.api_key = key
|
|
45
|
+
|
|
46
|
+
def get_api_key(self):
|
|
47
|
+
"""Return the current API key (or empty string)."""
|
|
48
|
+
return self.api_key
|
|
49
|
+
|
|
50
|
+
def _headers(self, api_key=None):
|
|
51
|
+
h = {"Content-Type": "application/json"}
|
|
52
|
+
key = api_key if api_key is not None else self.api_key
|
|
53
|
+
if key:
|
|
54
|
+
h["Authorization"] = f"Bearer {key}"
|
|
55
|
+
return h
|
|
56
|
+
|
|
57
|
+
def _checkout_headers(self, checkout_id):
|
|
58
|
+
"""Build headers with checkout_secret if available."""
|
|
59
|
+
h = {"Content-Type": "application/json"}
|
|
60
|
+
secret = self._checkout_secrets.get(checkout_id)
|
|
61
|
+
if secret:
|
|
62
|
+
h["X-Checkout-Secret"] = secret
|
|
63
|
+
return h
|
|
64
|
+
|
|
65
|
+
def _handle(self, resp):
|
|
66
|
+
"""Parse API response envelope, raise ApiError on failure."""
|
|
67
|
+
try:
|
|
68
|
+
data = resp.json()
|
|
69
|
+
except ValueError:
|
|
70
|
+
raise ApiError(
|
|
71
|
+
code="INVALID_RESPONSE",
|
|
72
|
+
message=f"Non-JSON response (status {resp.status_code})",
|
|
73
|
+
status_code=resp.status_code,
|
|
74
|
+
)
|
|
75
|
+
if not data.get("ok", False):
|
|
76
|
+
error = data.get("error", {}) or {}
|
|
77
|
+
meta = data.get("meta", {}) or {}
|
|
78
|
+
raise ApiError(
|
|
79
|
+
code=error.get("code", "UNKNOWN"),
|
|
80
|
+
message=error.get("message", "Unknown error"),
|
|
81
|
+
status_code=resp.status_code,
|
|
82
|
+
request_id=meta.get("request_id"),
|
|
83
|
+
)
|
|
84
|
+
return data.get("data")
|
|
85
|
+
|
|
86
|
+
# ── Auth (unauthenticated) ──
|
|
87
|
+
|
|
88
|
+
def register(self, email=None, name="Agent Key"):
|
|
89
|
+
"""Register an agent account and get an API key."""
|
|
90
|
+
body = {"name": name}
|
|
91
|
+
if email:
|
|
92
|
+
body["email"] = email
|
|
93
|
+
resp = httpx.post(
|
|
94
|
+
f"{self.api_base}/auth/register/",
|
|
95
|
+
headers={"Content-Type": "application/json"},
|
|
96
|
+
json=body,
|
|
97
|
+
timeout=self.timeout,
|
|
98
|
+
)
|
|
99
|
+
return self._handle(resp)
|
|
100
|
+
|
|
101
|
+
def whoami(self):
|
|
102
|
+
"""Get current account info from API key."""
|
|
103
|
+
resp = httpx.get(f"{self.api_base}/auth/whoami/", headers=self._headers(), timeout=self.timeout)
|
|
104
|
+
return self._handle(resp)
|
|
105
|
+
|
|
106
|
+
# ── Plans (unauthenticated) ──
|
|
107
|
+
|
|
108
|
+
def list_plans(self, include_deprecated=False, track=None):
|
|
109
|
+
params = {}
|
|
110
|
+
if include_deprecated:
|
|
111
|
+
params["include_deprecated"] = "true"
|
|
112
|
+
if track:
|
|
113
|
+
params["track"] = track
|
|
114
|
+
resp = httpx.get(f"{self.api_base}/plans/", headers=self._headers(), params=params, timeout=self.timeout)
|
|
115
|
+
return self._handle(resp)
|
|
116
|
+
|
|
117
|
+
def get_plan(self, slug):
|
|
118
|
+
resp = httpx.get(f"{self.api_base}/plans/{slug}/", headers=self._headers(), timeout=self.timeout)
|
|
119
|
+
return self._handle(resp)
|
|
120
|
+
|
|
121
|
+
# ── ACP Checkout (unauthenticated) ──
|
|
122
|
+
|
|
123
|
+
def create_checkout(self, sku):
|
|
124
|
+
"""Create an ACP checkout session for a plan SKU."""
|
|
125
|
+
resp = httpx.post(
|
|
126
|
+
f"{self.api_base}/acp/checkouts/",
|
|
127
|
+
headers={"Content-Type": "application/json"},
|
|
128
|
+
json={"sku": sku},
|
|
129
|
+
timeout=self.timeout,
|
|
130
|
+
)
|
|
131
|
+
result = self._handle(resp)
|
|
132
|
+
# Store checkout_secret for subsequent update/complete calls
|
|
133
|
+
if result and result.get("checkout_secret"):
|
|
134
|
+
self._checkout_secrets[result["id"]] = result["checkout_secret"]
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
def get_checkout(self, checkout_id):
|
|
138
|
+
"""Get ACP checkout session status."""
|
|
139
|
+
resp = httpx.get(
|
|
140
|
+
f"{self.api_base}/acp/checkouts/{checkout_id}/",
|
|
141
|
+
headers=self._checkout_headers(checkout_id),
|
|
142
|
+
timeout=self.timeout,
|
|
143
|
+
)
|
|
144
|
+
return self._handle(resp)
|
|
145
|
+
|
|
146
|
+
def update_checkout(self, checkout_id, buyer_email=None, requested_slug=None):
|
|
147
|
+
"""Update ACP checkout with buyer info."""
|
|
148
|
+
body = {}
|
|
149
|
+
if buyer_email:
|
|
150
|
+
body["buyer_email"] = buyer_email
|
|
151
|
+
if requested_slug:
|
|
152
|
+
body["requested_slug"] = requested_slug
|
|
153
|
+
resp = httpx.post(
|
|
154
|
+
f"{self.api_base}/acp/checkouts/{checkout_id}/update/",
|
|
155
|
+
headers=self._checkout_headers(checkout_id),
|
|
156
|
+
json=body,
|
|
157
|
+
timeout=self.timeout,
|
|
158
|
+
)
|
|
159
|
+
return self._handle(resp)
|
|
160
|
+
|
|
161
|
+
def complete_checkout(self, checkout_id, payment_method="stripe_checkout", payment_method_id=None):
|
|
162
|
+
"""Complete ACP checkout with payment."""
|
|
163
|
+
body = {"payment_method": payment_method}
|
|
164
|
+
if payment_method_id:
|
|
165
|
+
body["payment_method_id"] = payment_method_id
|
|
166
|
+
# admin_bypass needs auth header; others use checkout_secret
|
|
167
|
+
headers = self._headers() if payment_method == "admin_bypass" else self._checkout_headers(checkout_id)
|
|
168
|
+
resp = httpx.post(
|
|
169
|
+
f"{self.api_base}/acp/checkouts/{checkout_id}/complete/",
|
|
170
|
+
headers=headers,
|
|
171
|
+
json=body,
|
|
172
|
+
timeout=60,
|
|
173
|
+
)
|
|
174
|
+
return self._handle(resp)
|
|
175
|
+
|
|
176
|
+
# ── Sites (authenticated) ──
|
|
177
|
+
|
|
178
|
+
def create_site(self, checkout_id):
|
|
179
|
+
"""Create a site from a completed ACP checkout."""
|
|
180
|
+
resp = httpx.post(
|
|
181
|
+
f"{self.api_base}/sites/create/",
|
|
182
|
+
headers=self._headers(),
|
|
183
|
+
json={"checkout_id": checkout_id},
|
|
184
|
+
timeout=60,
|
|
185
|
+
)
|
|
186
|
+
return self._handle(resp)
|
|
187
|
+
|
|
188
|
+
def list_sites(self):
|
|
189
|
+
resp = httpx.get(f"{self.api_base}/sites/", headers=self._headers(), timeout=self.timeout)
|
|
190
|
+
return self._handle(resp)
|
|
191
|
+
|
|
192
|
+
def get_site(self, slug):
|
|
193
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/", headers=self._headers(), timeout=self.timeout)
|
|
194
|
+
return self._handle(resp)
|
|
195
|
+
|
|
196
|
+
def manage_dns(self, slug, action, record_type, subdomain="", value="", ttl=3600, priority=None):
|
|
197
|
+
body = {"action": action, "type": record_type, "subdomain": subdomain, "value": value, "ttl": ttl}
|
|
198
|
+
if priority is not None:
|
|
199
|
+
body["priority"] = priority
|
|
200
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/dns/", headers=self._headers(), json=body, timeout=self.timeout)
|
|
201
|
+
return self._handle(resp)
|
|
202
|
+
|
|
203
|
+
def deploy(self, slug):
|
|
204
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/deploy/", headers=self._headers(), json={}, timeout=120)
|
|
205
|
+
return self._handle(resp)
|
|
206
|
+
|
|
207
|
+
def create_snapshot(self, slug, description=""):
|
|
208
|
+
body = {"description": description} if description else {}
|
|
209
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/snapshots/create/", headers=self._headers(), json=body, timeout=180)
|
|
210
|
+
return self._handle(resp)
|
|
211
|
+
|
|
212
|
+
def list_snapshots(self, slug):
|
|
213
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/snapshots/", headers=self._headers(), timeout=self.timeout)
|
|
214
|
+
return self._handle(resp)
|
|
215
|
+
|
|
216
|
+
def create_b2_snapshot(self, slug, description=""):
|
|
217
|
+
body = {"description": description} if description else {}
|
|
218
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/snapshots/b2/", headers=self._headers(), json=body, timeout=1800)
|
|
219
|
+
return self._handle(resp)
|
|
220
|
+
|
|
221
|
+
def delete_snapshot(self, slug, snapshot_id):
|
|
222
|
+
resp = httpx.delete(f"{self.api_base}/sites/{slug}/snapshots/{snapshot_id}/", headers=self._headers(), timeout=60)
|
|
223
|
+
return self._handle(resp)
|
|
224
|
+
|
|
225
|
+
def rollback_snapshot(self, slug, snapshot_id):
|
|
226
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/snapshots/{snapshot_id}/rollback/", headers=self._headers(), json={}, timeout=300)
|
|
227
|
+
return self._handle(resp)
|
|
228
|
+
|
|
229
|
+
def get_snapshot_usage(self, slug):
|
|
230
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/snapshots/usage/", headers=self._headers(), timeout=self.timeout)
|
|
231
|
+
return self._handle(resp)
|
|
232
|
+
|
|
233
|
+
def schedule_snapshot(self, slug, scheduled_at, description=""):
|
|
234
|
+
body = {"scheduled_at": scheduled_at, "description": description}
|
|
235
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/snapshots/schedule/", headers=self._headers(), json=body, timeout=self.timeout)
|
|
236
|
+
return self._handle(resp)
|
|
237
|
+
|
|
238
|
+
def cancel_scheduled_snapshot(self, slug, schedule_id):
|
|
239
|
+
resp = httpx.delete(f"{self.api_base}/sites/{slug}/snapshots/schedule/{schedule_id}/", headers=self._headers(), timeout=self.timeout)
|
|
240
|
+
return self._handle(resp)
|
|
241
|
+
|
|
242
|
+
# ── Backups (authenticated) ──
|
|
243
|
+
|
|
244
|
+
def list_backups(self, slug):
|
|
245
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/backups/", headers=self._headers(), timeout=self.timeout)
|
|
246
|
+
return self._handle(resp)
|
|
247
|
+
|
|
248
|
+
def create_backup(self, slug):
|
|
249
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/backups/create/", headers=self._headers(), json={}, timeout=self.timeout)
|
|
250
|
+
return self._handle(resp)
|
|
251
|
+
|
|
252
|
+
def restore_backup(self, slug, backup_id):
|
|
253
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/backups/{backup_id}/restore/", headers=self._headers(), json={}, timeout=self.timeout)
|
|
254
|
+
return self._handle(resp)
|
|
255
|
+
|
|
256
|
+
def get_metrics(self, slug, days=7):
|
|
257
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/metrics/", headers=self._headers(), params={"days": days}, timeout=self.timeout)
|
|
258
|
+
return self._handle(resp)
|
|
259
|
+
|
|
260
|
+
def scale(self, slug, new_plan):
|
|
261
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/scale/", headers=self._headers(), json={"new_plan": new_plan}, timeout=self.timeout)
|
|
262
|
+
return self._handle(resp)
|
|
263
|
+
|
|
264
|
+
def decommission(self, slug):
|
|
265
|
+
resp = httpx.delete(f"{self.api_base}/sites/{slug}/delete/", headers=self._headers(), timeout=self.timeout)
|
|
266
|
+
return self._handle(resp)
|
|
267
|
+
|
|
268
|
+
# ── Account Management (authenticated) ──
|
|
269
|
+
|
|
270
|
+
def update_account(self, email=None, language=None, first_name=None, last_name=None):
|
|
271
|
+
"""Update account profile fields."""
|
|
272
|
+
body = {}
|
|
273
|
+
if email is not None:
|
|
274
|
+
body["email"] = email
|
|
275
|
+
if language is not None:
|
|
276
|
+
body["language"] = language
|
|
277
|
+
if first_name is not None:
|
|
278
|
+
body["first_name"] = first_name
|
|
279
|
+
if last_name is not None:
|
|
280
|
+
body["last_name"] = last_name
|
|
281
|
+
resp = httpx.post(f"{self.api_base}/account/update/", headers=self._headers(), json=body, timeout=self.timeout)
|
|
282
|
+
return self._handle(resp)
|
|
283
|
+
|
|
284
|
+
def delete_account(self):
|
|
285
|
+
"""Soft-delete (anonymize) the account. Irreversible."""
|
|
286
|
+
resp = httpx.request(
|
|
287
|
+
"DELETE", f"{self.api_base}/account/delete/",
|
|
288
|
+
headers=self._headers(), json={"confirm": "DELETE"}, timeout=self.timeout,
|
|
289
|
+
)
|
|
290
|
+
return self._handle(resp)
|
|
291
|
+
|
|
292
|
+
def list_subscriptions(self):
|
|
293
|
+
"""List all subscriptions for the account."""
|
|
294
|
+
resp = httpx.get(f"{self.api_base}/account/subscriptions/", headers=self._headers(), timeout=self.timeout)
|
|
295
|
+
return self._handle(resp)
|
|
296
|
+
|
|
297
|
+
def get_billing_portal(self, flow=None):
|
|
298
|
+
"""Get Stripe billing portal URL."""
|
|
299
|
+
params = {}
|
|
300
|
+
if flow:
|
|
301
|
+
params["flow"] = flow
|
|
302
|
+
resp = httpx.get(f"{self.api_base}/account/billing-portal/", headers=self._headers(), params=params, timeout=self.timeout)
|
|
303
|
+
return self._handle(resp)
|
|
304
|
+
|
|
305
|
+
def rotate_key(self, key_id):
|
|
306
|
+
"""Atomically rotate an API key."""
|
|
307
|
+
resp = httpx.post(f"{self.api_base}/keys/{key_id}/rotate/", headers=self._headers(), json={}, timeout=self.timeout)
|
|
308
|
+
return self._handle(resp)
|
|
309
|
+
|
|
310
|
+
def create_api_key(self, name, scopes, site_slug=None, disabled_tools=None):
|
|
311
|
+
"""Create a new API key."""
|
|
312
|
+
body = {"name": name, "scopes": scopes}
|
|
313
|
+
if site_slug:
|
|
314
|
+
body["site_slug"] = site_slug
|
|
315
|
+
if disabled_tools:
|
|
316
|
+
body["disabled_tools"] = disabled_tools
|
|
317
|
+
resp = httpx.post(f"{self.api_base}/keys/create/", headers=self._headers(), json=body, timeout=self.timeout)
|
|
318
|
+
return self._handle(resp)
|
|
319
|
+
|
|
320
|
+
def list_api_keys(self):
|
|
321
|
+
"""List all API keys for the account."""
|
|
322
|
+
resp = httpx.get(f"{self.api_base}/keys/", headers=self._headers(), timeout=self.timeout)
|
|
323
|
+
return self._handle(resp)
|
|
324
|
+
|
|
325
|
+
def revoke_api_key(self, key_id):
|
|
326
|
+
"""Revoke (deactivate) an API key."""
|
|
327
|
+
resp = httpx.post(f"{self.api_base}/keys/{key_id}/revoke/", headers=self._headers(), json={}, timeout=self.timeout)
|
|
328
|
+
return self._handle(resp)
|
|
329
|
+
|
|
330
|
+
# ── File Management (authenticated) ──
|
|
331
|
+
|
|
332
|
+
def list_files(self, slug, path=""):
|
|
333
|
+
"""List directory contents for a site."""
|
|
334
|
+
params = {"path": path} if path else {}
|
|
335
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/files/", headers=self._headers(), params=params, timeout=self.timeout)
|
|
336
|
+
return self._handle(resp)
|
|
337
|
+
|
|
338
|
+
def read_file(self, slug, path):
|
|
339
|
+
"""Read a file from a site."""
|
|
340
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/files/read/", headers=self._headers(), params={"path": path}, timeout=self.timeout)
|
|
341
|
+
return self._handle(resp)
|
|
342
|
+
|
|
343
|
+
def write_file(self, slug, path, content):
|
|
344
|
+
"""Write content to a file on a site."""
|
|
345
|
+
resp = httpx.post(
|
|
346
|
+
f"{self.api_base}/sites/{slug}/files/write/",
|
|
347
|
+
headers=self._headers(), json={"path": path, "content": content}, timeout=self.timeout,
|
|
348
|
+
)
|
|
349
|
+
return self._handle(resp)
|
|
350
|
+
|
|
351
|
+
def upload_file(self, slug, path, content_b64):
|
|
352
|
+
"""Upload a base64-encoded file to a site."""
|
|
353
|
+
resp = httpx.post(
|
|
354
|
+
f"{self.api_base}/sites/{slug}/files/upload/",
|
|
355
|
+
headers=self._headers(), json={"path": path, "content_b64": content_b64}, timeout=self.timeout,
|
|
356
|
+
)
|
|
357
|
+
return self._handle(resp)
|
|
358
|
+
|
|
359
|
+
def mkdir(self, slug, path):
|
|
360
|
+
"""Create a directory on a site."""
|
|
361
|
+
resp = httpx.post(
|
|
362
|
+
f"{self.api_base}/sites/{slug}/files/mkdir/",
|
|
363
|
+
headers=self._headers(), json={"path": path}, timeout=self.timeout,
|
|
364
|
+
)
|
|
365
|
+
return self._handle(resp)
|
|
366
|
+
|
|
367
|
+
def delete_file(self, slug, path):
|
|
368
|
+
"""Delete a file or directory on a site."""
|
|
369
|
+
resp = httpx.post(
|
|
370
|
+
f"{self.api_base}/sites/{slug}/files/delete/",
|
|
371
|
+
headers=self._headers(), json={"path": path}, timeout=self.timeout,
|
|
372
|
+
)
|
|
373
|
+
return self._handle(resp)
|
|
374
|
+
|
|
375
|
+
# ── Apps (authenticated, VPS only) ──
|
|
376
|
+
|
|
377
|
+
def list_apps(self, slug):
|
|
378
|
+
"""List installed apps on a site."""
|
|
379
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/apps/", headers=self._headers(), timeout=self.timeout)
|
|
380
|
+
return self._handle(resp)
|
|
381
|
+
|
|
382
|
+
def install_app(self, slug, template, app_name, db_type="none", domain="", display_name=""):
|
|
383
|
+
"""Install an app template on a VPS site."""
|
|
384
|
+
body = {"template": template, "app_name": app_name, "db_type": db_type}
|
|
385
|
+
if domain:
|
|
386
|
+
body["domain"] = domain
|
|
387
|
+
if display_name:
|
|
388
|
+
body["display_name"] = display_name
|
|
389
|
+
resp = httpx.post(
|
|
390
|
+
f"{self.api_base}/sites/{slug}/apps/install/",
|
|
391
|
+
headers=self._headers(),
|
|
392
|
+
json=body,
|
|
393
|
+
timeout=60,
|
|
394
|
+
)
|
|
395
|
+
return self._handle(resp)
|
|
396
|
+
|
|
397
|
+
def get_app(self, slug, app_id):
|
|
398
|
+
"""Get app status and install log."""
|
|
399
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/apps/{app_id}/", headers=self._headers(), timeout=self.timeout)
|
|
400
|
+
return self._handle(resp)
|
|
401
|
+
|
|
402
|
+
# ── Logs (authenticated) ──
|
|
403
|
+
|
|
404
|
+
def get_logs(self, slug, log_type="error", lines=100, search=None):
|
|
405
|
+
"""Get container logs for a site."""
|
|
406
|
+
params = {"type": log_type, "lines": lines}
|
|
407
|
+
if search:
|
|
408
|
+
params["search"] = search
|
|
409
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/logs/", headers=self._headers(), params=params, timeout=self.timeout)
|
|
410
|
+
return self._handle(resp)
|
|
411
|
+
|
|
412
|
+
# ── Modules (authenticated) ──
|
|
413
|
+
|
|
414
|
+
def list_modules(self, slug):
|
|
415
|
+
"""List modules and availability for a site."""
|
|
416
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/modules/", headers=self._headers(), timeout=self.timeout)
|
|
417
|
+
return self._handle(resp)
|
|
418
|
+
|
|
419
|
+
def toggle_module(self, slug, module_name):
|
|
420
|
+
"""Toggle a module on/off for a site."""
|
|
421
|
+
resp = httpx.post(
|
|
422
|
+
f"{self.api_base}/sites/{slug}/modules/{module_name}/toggle/",
|
|
423
|
+
headers=self._headers(), json={}, timeout=self.timeout,
|
|
424
|
+
)
|
|
425
|
+
return self._handle(resp)
|
|
426
|
+
|
|
427
|
+
# ── Domains (authenticated) ──
|
|
428
|
+
|
|
429
|
+
def list_domains(self):
|
|
430
|
+
"""List all domains owned by the user."""
|
|
431
|
+
resp = httpx.get(f"{self.api_base}/domains/", headers=self._headers(), timeout=self.timeout)
|
|
432
|
+
return self._handle(resp)
|
|
433
|
+
|
|
434
|
+
def search_domain(self, domain):
|
|
435
|
+
"""Check domain availability and pricing."""
|
|
436
|
+
resp = httpx.get(f"{self.api_base}/domains/search/", headers=self._headers(), params={"domain": domain}, timeout=self.timeout)
|
|
437
|
+
return self._handle(resp)
|
|
438
|
+
|
|
439
|
+
def register_domain(self, domain, contact, period=1, subscription_id=None, ca_legal_type=None):
|
|
440
|
+
"""Register a domain with contact info and Stripe billing."""
|
|
441
|
+
body = {"domain": domain, "period": period, "contact": contact}
|
|
442
|
+
if subscription_id:
|
|
443
|
+
body["subscription_id"] = subscription_id
|
|
444
|
+
if ca_legal_type:
|
|
445
|
+
body["ca_legal_type"] = ca_legal_type
|
|
446
|
+
resp = httpx.post(
|
|
447
|
+
f"{self.api_base}/domains/register/",
|
|
448
|
+
headers=self._headers(), json=body, timeout=60,
|
|
449
|
+
)
|
|
450
|
+
return self._handle(resp)
|
|
451
|
+
|
|
452
|
+
def get_domain_detail(self, domain_name):
|
|
453
|
+
"""Get domain detail with infrastructure status."""
|
|
454
|
+
resp = httpx.get(f"{self.api_base}/domains/{domain_name}/", headers=self._headers(), timeout=self.timeout)
|
|
455
|
+
return self._handle(resp)
|
|
456
|
+
|
|
457
|
+
def list_domain_dns(self, domain_name):
|
|
458
|
+
"""List DNS records for a domain."""
|
|
459
|
+
resp = httpx.get(f"{self.api_base}/domains/{domain_name}/dns/", headers=self._headers(), timeout=self.timeout)
|
|
460
|
+
return self._handle(resp)
|
|
461
|
+
|
|
462
|
+
def add_domain_dns(self, domain_name, record_type, value, subdomain="", ttl=3600, priority=None):
|
|
463
|
+
"""Add a DNS record to a domain."""
|
|
464
|
+
body = {"type": record_type, "value": value, "subdomain": subdomain, "ttl": ttl}
|
|
465
|
+
if priority is not None:
|
|
466
|
+
body["priority"] = priority
|
|
467
|
+
resp = httpx.post(f"{self.api_base}/domains/{domain_name}/dns/add/", headers=self._headers(), json=body, timeout=self.timeout)
|
|
468
|
+
return self._handle(resp)
|
|
469
|
+
|
|
470
|
+
def delete_domain_dns(self, domain_name, record_id):
|
|
471
|
+
"""Delete a DNS record from a domain."""
|
|
472
|
+
resp = httpx.post(
|
|
473
|
+
f"{self.api_base}/domains/{domain_name}/dns/delete/",
|
|
474
|
+
headers=self._headers(), json={"record_id": record_id}, timeout=self.timeout,
|
|
475
|
+
)
|
|
476
|
+
return self._handle(resp)
|
|
477
|
+
|
|
478
|
+
def link_domain(self, domain_name, site_slug):
|
|
479
|
+
"""Link a domain to a site."""
|
|
480
|
+
resp = httpx.post(
|
|
481
|
+
f"{self.api_base}/domains/{domain_name}/link/",
|
|
482
|
+
headers=self._headers(), json={"site_slug": site_slug}, timeout=self.timeout,
|
|
483
|
+
)
|
|
484
|
+
return self._handle(resp)
|
|
485
|
+
|
|
486
|
+
def domain_settings(self, domain_name, auto_renew=None, whois_privacy=None, locked=None):
|
|
487
|
+
"""Update domain settings."""
|
|
488
|
+
body = {}
|
|
489
|
+
if auto_renew is not None:
|
|
490
|
+
body["auto_renew"] = auto_renew
|
|
491
|
+
if whois_privacy is not None:
|
|
492
|
+
body["whois_privacy"] = whois_privacy
|
|
493
|
+
if locked is not None:
|
|
494
|
+
body["locked"] = locked
|
|
495
|
+
resp = httpx.post(
|
|
496
|
+
f"{self.api_base}/domains/{domain_name}/settings/",
|
|
497
|
+
headers=self._headers(), json=body, timeout=self.timeout,
|
|
498
|
+
)
|
|
499
|
+
return self._handle(resp)
|
|
500
|
+
|
|
501
|
+
# ── SSH Access (authenticated) ──
|
|
502
|
+
|
|
503
|
+
def get_ssh_info(self, slug):
|
|
504
|
+
"""Get SSH connection info for a site."""
|
|
505
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/ssh/", headers=self._headers(), timeout=self.timeout)
|
|
506
|
+
return self._handle(resp)
|
|
507
|
+
|
|
508
|
+
def add_ssh_key(self, slug, public_key):
|
|
509
|
+
"""Inject an SSH public key into a site's container."""
|
|
510
|
+
resp = httpx.post(
|
|
511
|
+
f"{self.api_base}/sites/{slug}/ssh/keys/",
|
|
512
|
+
headers=self._headers(), json={"public_key": public_key}, timeout=self.timeout,
|
|
513
|
+
)
|
|
514
|
+
return self._handle(resp)
|
|
515
|
+
|
|
516
|
+
# ── WordPress Management (authenticated) ──
|
|
517
|
+
|
|
518
|
+
def list_plugins(self, slug):
|
|
519
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/wordpress/plugins/", headers=self._headers(), timeout=self.timeout)
|
|
520
|
+
return self._handle(resp)
|
|
521
|
+
|
|
522
|
+
def list_themes(self, slug):
|
|
523
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/wordpress/themes/", headers=self._headers(), timeout=self.timeout)
|
|
524
|
+
return self._handle(resp)
|
|
525
|
+
|
|
526
|
+
def manage_plugin(self, slug, action, plugin):
|
|
527
|
+
resp = httpx.post(
|
|
528
|
+
f"{self.api_base}/sites/{slug}/wordpress/plugins/manage/",
|
|
529
|
+
headers=self._headers(), json={"action": action, "plugin": plugin}, timeout=60,
|
|
530
|
+
)
|
|
531
|
+
return self._handle(resp)
|
|
532
|
+
|
|
533
|
+
def manage_theme(self, slug, action, theme):
|
|
534
|
+
resp = httpx.post(
|
|
535
|
+
f"{self.api_base}/sites/{slug}/wordpress/themes/manage/",
|
|
536
|
+
headers=self._headers(), json={"action": action, "theme": theme}, timeout=60,
|
|
537
|
+
)
|
|
538
|
+
return self._handle(resp)
|
|
539
|
+
|
|
540
|
+
def wp_check_updates(self, slug):
|
|
541
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/wordpress/updates/", headers=self._headers(), timeout=self.timeout)
|
|
542
|
+
return self._handle(resp)
|
|
543
|
+
|
|
544
|
+
def wp_update_all(self, slug):
|
|
545
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/wordpress/update-all/", headers=self._headers(), json={}, timeout=120)
|
|
546
|
+
return self._handle(resp)
|
|
547
|
+
|
|
548
|
+
# ── Cron (authenticated) ──
|
|
549
|
+
|
|
550
|
+
def list_cron(self, slug):
|
|
551
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/cron/", headers=self._headers(), timeout=15)
|
|
552
|
+
return self._handle(resp)
|
|
553
|
+
|
|
554
|
+
def add_cron(self, slug, schedule, command):
|
|
555
|
+
resp = httpx.post(
|
|
556
|
+
f"{self.api_base}/sites/{slug}/cron/add/",
|
|
557
|
+
headers=self._headers(), json={"schedule": schedule, "command": command}, timeout=15,
|
|
558
|
+
)
|
|
559
|
+
return self._handle(resp)
|
|
560
|
+
|
|
561
|
+
def delete_cron(self, slug, line_number):
|
|
562
|
+
resp = httpx.post(
|
|
563
|
+
f"{self.api_base}/sites/{slug}/cron/delete/",
|
|
564
|
+
headers=self._headers(), json={"line_number": line_number}, timeout=15,
|
|
565
|
+
)
|
|
566
|
+
return self._handle(resp)
|
|
567
|
+
|
|
568
|
+
# ── SSL (authenticated) ──
|
|
569
|
+
|
|
570
|
+
def get_ssl_info(self, slug):
|
|
571
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/ssl/", headers=self._headers(), timeout=self.timeout)
|
|
572
|
+
return self._handle(resp)
|
|
573
|
+
|
|
574
|
+
def renew_ssl(self, slug):
|
|
575
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/ssl/renew/", headers=self._headers(), json={}, timeout=120)
|
|
576
|
+
return self._handle(resp)
|
|
577
|
+
|
|
578
|
+
# ── PHP (authenticated) ──
|
|
579
|
+
|
|
580
|
+
def list_php_versions(self, slug):
|
|
581
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/php/", headers=self._headers(), timeout=15)
|
|
582
|
+
return self._handle(resp)
|
|
583
|
+
|
|
584
|
+
def switch_php(self, slug, version):
|
|
585
|
+
resp = httpx.post(
|
|
586
|
+
f"{self.api_base}/sites/{slug}/php/switch/",
|
|
587
|
+
headers=self._headers(), json={"version": version}, timeout=self.timeout,
|
|
588
|
+
)
|
|
589
|
+
return self._handle(resp)
|
|
590
|
+
|
|
591
|
+
# ── Cache (authenticated) ──
|
|
592
|
+
|
|
593
|
+
def get_cache_status(self, slug):
|
|
594
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/cache/", headers=self._headers(), timeout=15)
|
|
595
|
+
return self._handle(resp)
|
|
596
|
+
|
|
597
|
+
def flush_cache(self, slug):
|
|
598
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/cache/flush/", headers=self._headers(), json={}, timeout=self.timeout)
|
|
599
|
+
return self._handle(resp)
|
|
600
|
+
|
|
601
|
+
def toggle_cache(self, slug, enable):
|
|
602
|
+
resp = httpx.post(
|
|
603
|
+
f"{self.api_base}/sites/{slug}/cache/toggle/",
|
|
604
|
+
headers=self._headers(), json={"enable": enable}, timeout=self.timeout,
|
|
605
|
+
)
|
|
606
|
+
return self._handle(resp)
|
|
607
|
+
|
|
608
|
+
# ── Database (authenticated) ──
|
|
609
|
+
|
|
610
|
+
def wpdb_info(self, slug):
|
|
611
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/database/info/", headers=self._headers(), timeout=self.timeout)
|
|
612
|
+
return self._handle(resp)
|
|
613
|
+
|
|
614
|
+
def wpdb_optimize(self, slug):
|
|
615
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/database/optimize/", headers=self._headers(), json={}, timeout=60)
|
|
616
|
+
return self._handle(resp)
|
|
617
|
+
|
|
618
|
+
def wpdb_search_replace(self, slug, old, new, dry_run=True):
|
|
619
|
+
resp = httpx.post(
|
|
620
|
+
f"{self.api_base}/sites/{slug}/database/search-replace/",
|
|
621
|
+
headers=self._headers(), json={"old": old, "new": new, "dry_run": dry_run}, timeout=120,
|
|
622
|
+
)
|
|
623
|
+
return self._handle(resp)
|
|
624
|
+
|
|
625
|
+
def list_databases(self, slug):
|
|
626
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/database/databases/", headers=self._headers(), timeout=15)
|
|
627
|
+
return self._handle(resp)
|
|
628
|
+
|
|
629
|
+
def list_tables(self, slug, database):
|
|
630
|
+
resp = httpx.get(
|
|
631
|
+
f"{self.api_base}/sites/{slug}/database/tables/",
|
|
632
|
+
headers=self._headers(), params={"database": database}, timeout=15,
|
|
633
|
+
)
|
|
634
|
+
return self._handle(resp)
|
|
635
|
+
|
|
636
|
+
def execute_query(self, slug, database, query):
|
|
637
|
+
resp = httpx.post(
|
|
638
|
+
f"{self.api_base}/sites/{slug}/database/query/",
|
|
639
|
+
headers=self._headers(), json={"database": database, "query": query}, timeout=self.timeout,
|
|
640
|
+
)
|
|
641
|
+
return self._handle(resp)
|
|
642
|
+
|
|
643
|
+
# ── System (authenticated) ──
|
|
644
|
+
|
|
645
|
+
def get_stack_info(self, slug):
|
|
646
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/system/stack/", headers=self._headers(), timeout=15)
|
|
647
|
+
return self._handle(resp)
|
|
648
|
+
|
|
649
|
+
def get_resource_snapshot(self, slug):
|
|
650
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/system/resources/", headers=self._headers(), timeout=15)
|
|
651
|
+
return self._handle(resp)
|
|
652
|
+
|
|
653
|
+
# ── FTP Accounts (authenticated) ──
|
|
654
|
+
|
|
655
|
+
def list_ftp_accounts(self, slug):
|
|
656
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/ftp/", headers=self._headers(), timeout=15)
|
|
657
|
+
return self._handle(resp)
|
|
658
|
+
|
|
659
|
+
def create_ftp_account(self, slug, username, password, home_dir="/var/www/html"):
|
|
660
|
+
resp = httpx.post(
|
|
661
|
+
f"{self.api_base}/sites/{slug}/ftp/create/",
|
|
662
|
+
headers=self._headers(), json={"username": username, "password": password, "home_dir": home_dir},
|
|
663
|
+
timeout=self.timeout,
|
|
664
|
+
)
|
|
665
|
+
return self._handle(resp)
|
|
666
|
+
|
|
667
|
+
def remove_ftp_account(self, slug, username):
|
|
668
|
+
resp = httpx.post(
|
|
669
|
+
f"{self.api_base}/sites/{slug}/ftp/remove/",
|
|
670
|
+
headers=self._headers(), json={"username": username}, timeout=15,
|
|
671
|
+
)
|
|
672
|
+
return self._handle(resp)
|
|
673
|
+
|
|
674
|
+
def ftp_set_password(self, slug, username, password):
|
|
675
|
+
resp = httpx.post(
|
|
676
|
+
f"{self.api_base}/sites/{slug}/ftp/password/",
|
|
677
|
+
headers=self._headers(), json={"username": username, "password": password}, timeout=15,
|
|
678
|
+
)
|
|
679
|
+
return self._handle(resp)
|
|
680
|
+
|
|
681
|
+
# ── Alert Rules (authenticated) ──
|
|
682
|
+
|
|
683
|
+
def list_alert_rules(self, slug):
|
|
684
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/alerts/", headers=self._headers(), timeout=15)
|
|
685
|
+
return self._handle(resp)
|
|
686
|
+
|
|
687
|
+
def create_alert_rule(self, slug, metric, threshold, operator="gt", severity="warning",
|
|
688
|
+
cooldown_minutes=30, notify_email=True, notify_webhook=""):
|
|
689
|
+
body = {
|
|
690
|
+
"metric": metric, "threshold": threshold, "operator": operator,
|
|
691
|
+
"severity": severity, "cooldown_minutes": cooldown_minutes,
|
|
692
|
+
"notify_email": notify_email, "notify_webhook": notify_webhook,
|
|
693
|
+
}
|
|
694
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/alerts/create/", headers=self._headers(), json=body, timeout=15)
|
|
695
|
+
return self._handle(resp)
|
|
696
|
+
|
|
697
|
+
def delete_alert_rule(self, slug, rule_id):
|
|
698
|
+
resp = httpx.post(f"{self.api_base}/sites/{slug}/alerts/{rule_id}/delete/", headers=self._headers(), json={}, timeout=15)
|
|
699
|
+
return self._handle(resp)
|
|
700
|
+
|
|
701
|
+
# ── Security Scanning (authenticated) ──
|
|
702
|
+
|
|
703
|
+
def malware_scan(self, slug, path=""):
|
|
704
|
+
body = {"path": path} if path else {}
|
|
705
|
+
resp = httpx.post(
|
|
706
|
+
f"{self.api_base}/sites/{slug}/security/scan/",
|
|
707
|
+
headers=self._headers(), json=body, timeout=320,
|
|
708
|
+
)
|
|
709
|
+
return self._handle(resp)
|
|
710
|
+
|
|
711
|
+
# ── Cloudflare CDN (authenticated) ──
|
|
712
|
+
|
|
713
|
+
def cf_proxy_status(self, slug):
|
|
714
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/cloudflare/proxy/", headers=self._headers(), timeout=self.timeout)
|
|
715
|
+
return self._handle(resp)
|
|
716
|
+
|
|
717
|
+
def cf_set_proxy(self, slug, proxied):
|
|
718
|
+
resp = httpx.post(
|
|
719
|
+
f"{self.api_base}/sites/{slug}/cloudflare/proxy/set/",
|
|
720
|
+
headers=self._headers(), json={"proxied": proxied}, timeout=self.timeout,
|
|
721
|
+
)
|
|
722
|
+
return self._handle(resp)
|
|
723
|
+
|
|
724
|
+
def cf_cache_purge(self, slug, urls=None):
|
|
725
|
+
body = {"urls": urls} if urls else {}
|
|
726
|
+
resp = httpx.post(
|
|
727
|
+
f"{self.api_base}/sites/{slug}/cloudflare/cache/purge/",
|
|
728
|
+
headers=self._headers(), json=body, timeout=self.timeout,
|
|
729
|
+
)
|
|
730
|
+
return self._handle(resp)
|
|
731
|
+
|
|
732
|
+
def cf_analytics(self, slug, minutes=1440):
|
|
733
|
+
resp = httpx.get(
|
|
734
|
+
f"{self.api_base}/sites/{slug}/cloudflare/analytics/",
|
|
735
|
+
headers=self._headers(), params={"minutes": minutes}, timeout=self.timeout,
|
|
736
|
+
)
|
|
737
|
+
return self._handle(resp)
|
|
738
|
+
|
|
739
|
+
# ── Firewall (authenticated) ──
|
|
740
|
+
|
|
741
|
+
def list_firewall_rules(self, slug):
|
|
742
|
+
resp = httpx.get(f"{self.api_base}/sites/{slug}/firewall/", headers=self._headers(), timeout=15)
|
|
743
|
+
return self._handle(resp)
|
|
744
|
+
|
|
745
|
+
def add_firewall_rule(self, slug, ip, action="deny"):
|
|
746
|
+
resp = httpx.post(
|
|
747
|
+
f"{self.api_base}/sites/{slug}/firewall/add/",
|
|
748
|
+
headers=self._headers(), json={"ip": ip, "action": action}, timeout=15,
|
|
749
|
+
)
|
|
750
|
+
return self._handle(resp)
|
|
751
|
+
|
|
752
|
+
def remove_firewall_rule(self, slug, ip):
|
|
753
|
+
resp = httpx.post(
|
|
754
|
+
f"{self.api_base}/sites/{slug}/firewall/remove/",
|
|
755
|
+
headers=self._headers(), json={"ip": ip}, timeout=15,
|
|
756
|
+
)
|
|
757
|
+
return self._handle(resp)
|
|
758
|
+
|
|
759
|
+
# ── Key Claim (challenge-response) ──
|
|
760
|
+
|
|
761
|
+
def request_api_key(self, site_slug):
|
|
762
|
+
"""Request a claim token for challenge-response key provisioning."""
|
|
763
|
+
resp = httpx.post(
|
|
764
|
+
f"{self.api_base}/keys/claim/request/",
|
|
765
|
+
headers={"Content-Type": "application/json"},
|
|
766
|
+
json={"site_slug": site_slug},
|
|
767
|
+
timeout=15,
|
|
768
|
+
)
|
|
769
|
+
return self._handle(resp)
|
|
770
|
+
|
|
771
|
+
def claim_api_key(self, claim_token):
|
|
772
|
+
"""Claim an API key using a claim token from the container."""
|
|
773
|
+
resp = httpx.post(
|
|
774
|
+
f"{self.api_base}/keys/claim/",
|
|
775
|
+
headers={"Content-Type": "application/json"},
|
|
776
|
+
json={"claim_token": claim_token},
|
|
777
|
+
timeout=15,
|
|
778
|
+
)
|
|
779
|
+
return self._handle(resp)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Exceptions raised by the BorealHost SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiError(Exception):
|
|
5
|
+
"""Raised when the BorealHost API returns an error envelope.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
code: Short error code from the API (e.g. "NOT_FOUND", "UNAUTHORIZED").
|
|
9
|
+
message: Human-readable error message.
|
|
10
|
+
status_code: HTTP status code.
|
|
11
|
+
request_id: API request ID for support/debugging (if present).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, code="UNKNOWN", message="Unknown error", status_code=None, request_id=None):
|
|
15
|
+
self.code = code
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.request_id = request_id
|
|
19
|
+
super().__init__(f"{code}: {message}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "borealhost-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the BorealHost.ai REST API — agent-native web hosting"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "BorealHost", email = "hello@borealhost.ai" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["borealhost", "hosting", "sdk", "api"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://borealhost.ai"
|
|
32
|
+
Documentation = "https://borealhost.ai/api/v1/docs/"
|
|
33
|
+
Repository = "https://github.com/alainsvrd/platform"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["borealhost_sdk"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build]
|
|
39
|
+
include = ["borealhost_sdk/**"]
|