wayscloud 0.1.0__tar.gz → 0.2.3__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.
- {wayscloud-0.1.0/wayscloud.egg-info → wayscloud-0.2.3}/PKG-INFO +4 -2
- {wayscloud-0.1.0 → wayscloud-0.2.3}/pyproject.toml +4 -2
- {wayscloud-0.1.0 → wayscloud-0.2.3}/setup.cfg +1 -1
- wayscloud-0.2.3/wayscloud/__init__.py +25 -0
- wayscloud-0.2.3/wayscloud/_version.py +1 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/client.py +11 -2
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/exceptions.py +16 -1
- wayscloud-0.2.3/wayscloud/services/apps.py +104 -0
- wayscloud-0.2.3/wayscloud/services/database.py +75 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/dns.py +23 -22
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/iot.py +33 -57
- wayscloud-0.2.3/wayscloud/services/redis.py +77 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/sms.py +17 -8
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/storage.py +26 -21
- wayscloud-0.2.3/wayscloud/services/vps.py +192 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3/wayscloud.egg-info}/PKG-INFO +4 -2
- wayscloud-0.1.0/wayscloud/__init__.py +0 -5
- wayscloud-0.1.0/wayscloud/_version.py +0 -1
- wayscloud-0.1.0/wayscloud/services/apps.py +0 -132
- wayscloud-0.1.0/wayscloud/services/database.py +0 -69
- wayscloud-0.1.0/wayscloud/services/redis.py +0 -67
- wayscloud-0.1.0/wayscloud/services/vps.py +0 -127
- {wayscloud-0.1.0 → wayscloud-0.2.3}/LICENSE +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/README.md +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/tests/test_smoke.py +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/py.typed +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/__init__.py +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/account.py +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/SOURCES.txt +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/dependency_links.txt +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/requires.txt +0 -0
- {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wayscloud
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Official Python SDK for the WAYSCloud API
|
|
5
5
|
Home-page: https://wayscloud.services
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://wayscloud.services
|
|
8
|
-
Project-URL: Documentation, https://docs.wayscloud.services
|
|
8
|
+
Project-URL: Documentation, https://docs.wayscloud.services/integrations/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/wayscloudas/wayscloud-provision-api
|
|
10
|
+
Project-URL: Issues, https://github.com/wayscloudas/wayscloud-provision-api/issues
|
|
9
11
|
Classifier: Development Status :: 4 - Beta
|
|
10
12
|
Classifier: Intended Audience :: Developers
|
|
11
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wayscloud"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Official Python SDK for the WAYSCloud API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -18,7 +18,9 @@ classifiers = [
|
|
|
18
18
|
|
|
19
19
|
[project.urls]
|
|
20
20
|
Homepage = "https://wayscloud.services"
|
|
21
|
-
Documentation = "https://docs.wayscloud.services"
|
|
21
|
+
Documentation = "https://docs.wayscloud.services/integrations/sdk"
|
|
22
|
+
Repository = "https://github.com/wayscloudas/wayscloud-provision-api"
|
|
23
|
+
Issues = "https://github.com/wayscloudas/wayscloud-provision-api/issues"
|
|
22
24
|
|
|
23
25
|
[tool.setuptools.packages.find]
|
|
24
26
|
include = ["wayscloud*"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .client import WaysCloudClient
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
WaysCloudError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
NotImplementedError_,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServerError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"WaysCloudClient",
|
|
16
|
+
"WaysCloudError",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"ConflictError",
|
|
19
|
+
"NotFoundError",
|
|
20
|
+
"NotImplementedError_",
|
|
21
|
+
"RateLimitError",
|
|
22
|
+
"ServerError",
|
|
23
|
+
"ValidationError",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -15,7 +15,10 @@ import httpx
|
|
|
15
15
|
from ._version import __version__
|
|
16
16
|
from .exceptions import (
|
|
17
17
|
AuthenticationError,
|
|
18
|
+
ConflictError,
|
|
18
19
|
NotFoundError,
|
|
20
|
+
NotImplementedError_,
|
|
21
|
+
RateLimitError,
|
|
19
22
|
ServerError,
|
|
20
23
|
ValidationError,
|
|
21
24
|
WaysCloudError,
|
|
@@ -75,7 +78,7 @@ class WaysCloudClient:
|
|
|
75
78
|
self._headers["X-API-Key"] = api_key
|
|
76
79
|
|
|
77
80
|
# Persistent HTTP client for connection pooling
|
|
78
|
-
self._http = httpx.Client(timeout=self.timeout)
|
|
81
|
+
self._http = httpx.Client(timeout=self.timeout, follow_redirects=True)
|
|
79
82
|
|
|
80
83
|
# Lazy service instances
|
|
81
84
|
self._vps: Optional[Any] = None
|
|
@@ -201,8 +204,14 @@ class WaysCloudClient:
|
|
|
201
204
|
raise AuthenticationError(message=message, status_code=status, detail=detail)
|
|
202
205
|
elif status == 404:
|
|
203
206
|
raise NotFoundError(message=message, status_code=status, detail=detail)
|
|
204
|
-
elif status
|
|
207
|
+
elif status in (400, 422):
|
|
205
208
|
raise ValidationError(message=message, status_code=status, detail=detail)
|
|
209
|
+
elif status == 409:
|
|
210
|
+
raise ConflictError(message=message, status_code=status, detail=detail)
|
|
211
|
+
elif status == 429:
|
|
212
|
+
raise RateLimitError(message=message, status_code=status, detail=detail)
|
|
213
|
+
elif status == 501:
|
|
214
|
+
raise NotImplementedError_(message=message, status_code=status, detail=detail)
|
|
206
215
|
elif status >= 500:
|
|
207
216
|
raise ServerError(message=message, status_code=status, detail=detail)
|
|
208
217
|
else:
|
|
@@ -19,7 +19,22 @@ class NotFoundError(WaysCloudError):
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ValidationError(WaysCloudError):
|
|
22
|
-
"""422 -- request validation
|
|
22
|
+
"""400/422 -- request validation or input error."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConflictError(WaysCloudError):
|
|
27
|
+
"""409 -- state conflict (resource busy, duplicate, wrong state)."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimitError(WaysCloudError):
|
|
32
|
+
"""429 -- too many requests or resource limit reached."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotImplementedError_(WaysCloudError):
|
|
37
|
+
"""501 -- feature not yet available."""
|
|
23
38
|
pass
|
|
24
39
|
|
|
25
40
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""App Platform service -- container app management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppService:
|
|
9
|
+
"""Manage container apps on the WAYSCloud App Platform.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/apps (API key auth).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(self) -> list[dict]:
|
|
18
|
+
"""List all apps."""
|
|
19
|
+
data = self._client.get("/v1/apps")
|
|
20
|
+
return data.get("apps", data) if isinstance(data, dict) else data
|
|
21
|
+
|
|
22
|
+
def get(self, app_id: str) -> dict:
|
|
23
|
+
"""Get details of a specific app."""
|
|
24
|
+
return self._client.get(f"/v1/apps/{app_id}")
|
|
25
|
+
|
|
26
|
+
def create(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
plan: str = "app-basic",
|
|
30
|
+
region: str = "no",
|
|
31
|
+
port: int = 8080,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""Create a new app."""
|
|
34
|
+
return self._client.post(
|
|
35
|
+
"/v1/apps",
|
|
36
|
+
json={"name": name, "plan": plan, "region": region, "port": port},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def update(self, app_id: str, **kwargs) -> dict:
|
|
40
|
+
"""Update an app. Pass any updatable fields as kwargs."""
|
|
41
|
+
return self._client.patch(f"/v1/apps/{app_id}", json=kwargs)
|
|
42
|
+
|
|
43
|
+
def delete(self, app_id: str) -> dict:
|
|
44
|
+
"""Delete an app (permanent)."""
|
|
45
|
+
return self._client.delete(f"/v1/apps/{app_id}")
|
|
46
|
+
|
|
47
|
+
def deploy(self, app_id: str, image: str) -> dict:
|
|
48
|
+
"""Deploy an app from a container image URI."""
|
|
49
|
+
return self._client.post(
|
|
50
|
+
f"/v1/apps/{app_id}/deploy/image",
|
|
51
|
+
json={"image_uri": image},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def start(self, app_id: str) -> dict:
|
|
55
|
+
"""Start an app."""
|
|
56
|
+
return self._client.post(f"/v1/apps/{app_id}/start")
|
|
57
|
+
|
|
58
|
+
def stop(self, app_id: str) -> dict:
|
|
59
|
+
"""Stop an app."""
|
|
60
|
+
return self._client.post(f"/v1/apps/{app_id}/stop")
|
|
61
|
+
|
|
62
|
+
def restart(self, app_id: str) -> dict:
|
|
63
|
+
"""Restart an app."""
|
|
64
|
+
return self._client.post(f"/v1/apps/{app_id}/restart")
|
|
65
|
+
|
|
66
|
+
def logs(self, app_id: str, lines: int = 100) -> list:
|
|
67
|
+
"""Get app logs."""
|
|
68
|
+
data = self._client.get(
|
|
69
|
+
f"/v1/apps/{app_id}/logs",
|
|
70
|
+
params={"lines": lines},
|
|
71
|
+
)
|
|
72
|
+
if isinstance(data, dict):
|
|
73
|
+
return data.get("logs", data.get("lines", []))
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
def plans(self) -> list[dict]:
|
|
77
|
+
"""List available app plans."""
|
|
78
|
+
data = self._client.get("/v1/apps/plans")
|
|
79
|
+
return data if isinstance(data, list) else data.get("plans", [])
|
|
80
|
+
|
|
81
|
+
def regions(self) -> list[dict]:
|
|
82
|
+
"""List available app regions."""
|
|
83
|
+
data = self._client.get("/v1/apps/regions")
|
|
84
|
+
return data if isinstance(data, list) else data.get("regions", [])
|
|
85
|
+
|
|
86
|
+
# ── Environment variables ─────────────────────────────────────
|
|
87
|
+
# env_vars are part of the app detail and updated via PATCH
|
|
88
|
+
|
|
89
|
+
def env_vars(self, app_id: str) -> dict:
|
|
90
|
+
"""Get environment variables for an app (from app detail)."""
|
|
91
|
+
data = self.get(app_id)
|
|
92
|
+
return data.get("env_vars", {}) if isinstance(data, dict) else {}
|
|
93
|
+
|
|
94
|
+
def set_env(self, app_id: str, key: str, value: str) -> dict:
|
|
95
|
+
"""Set an environment variable on an app."""
|
|
96
|
+
return self.update(app_id, env_vars={key: value})
|
|
97
|
+
|
|
98
|
+
def unset_env(self, app_id: str, key: str) -> dict:
|
|
99
|
+
"""Remove an environment variable from an app."""
|
|
100
|
+
return self.update(app_id, env_vars={key: None})
|
|
101
|
+
|
|
102
|
+
# domains(), add_domain(), remove_domain() removed.
|
|
103
|
+
# No public /v1/apps/{id}/domains endpoint exists (404 verified).
|
|
104
|
+
# Domain management is dashboard-only.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Database service -- Database-as-a-Service management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatabaseService:
|
|
9
|
+
"""Manage PostgreSQL and MariaDB databases.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/databases (PAT auth).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(self) -> list[dict]:
|
|
18
|
+
"""List all databases."""
|
|
19
|
+
data = self._client.get("/v1/databases")
|
|
20
|
+
return data if isinstance(data, list) else data.get("databases", [])
|
|
21
|
+
|
|
22
|
+
def get(self, db_type: str, name: str) -> dict:
|
|
23
|
+
"""Get details of a specific database.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
db_type: Database type (postgresql or mariadb).
|
|
27
|
+
name: Database name.
|
|
28
|
+
"""
|
|
29
|
+
return self._client.get(f"/v1/databases/{db_type}/{name}")
|
|
30
|
+
|
|
31
|
+
def create(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
db_type: str = "postgresql",
|
|
35
|
+
tier: str = "standard",
|
|
36
|
+
description: Optional[str] = None,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Create a new database. Returns credentials on success.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Database name.
|
|
42
|
+
db_type: Database type (postgresql or mariadb).
|
|
43
|
+
tier: Database tier (standard or encrypted).
|
|
44
|
+
description: Optional description.
|
|
45
|
+
"""
|
|
46
|
+
body: dict = {"name": name, "type": db_type, "tier": tier}
|
|
47
|
+
if description is not None:
|
|
48
|
+
body["description"] = description
|
|
49
|
+
return self._client.post("/v1/databases", json=body)
|
|
50
|
+
|
|
51
|
+
def delete(self, db_type: str, name: str) -> dict:
|
|
52
|
+
"""Delete a database (permanent)."""
|
|
53
|
+
return self._client.delete(f"/v1/databases/{db_type}/{name}")
|
|
54
|
+
|
|
55
|
+
# ── Firewall ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def firewall_rules(self, db_type: str, name: str) -> list[dict]:
|
|
58
|
+
"""List firewall rules for a database."""
|
|
59
|
+
data = self._client.get(f"/v1/databases/{db_type}/{name}/firewall")
|
|
60
|
+
return data if isinstance(data, list) else data.get("rules", [])
|
|
61
|
+
|
|
62
|
+
def add_firewall_rule(self, db_type: str, name: str, source: str) -> dict:
|
|
63
|
+
"""Add a firewall rule to allow access from a CIDR."""
|
|
64
|
+
return self._client.post(
|
|
65
|
+
f"/v1/databases/{db_type}/{name}/firewall",
|
|
66
|
+
json={"source": source},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def remove_firewall_rule(self, db_type: str, name: str, rule_id: str) -> dict:
|
|
70
|
+
"""Remove a firewall rule."""
|
|
71
|
+
return self._client.delete(f"/v1/databases/{db_type}/{name}/firewall/{rule_id}")
|
|
72
|
+
|
|
73
|
+
# credentials() removed — no public /v1/databases/.../credentials endpoint.
|
|
74
|
+
# Connection credentials are returned in create() response.
|
|
75
|
+
# tiers() removed — no public endpoint. Dashboard-only discovery.
|
|
@@ -6,38 +6,47 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class DNSService:
|
|
9
|
-
"""Manage DNS zones, records, and DNSSEC.
|
|
9
|
+
"""Manage DNS zones, records, and DNSSEC.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/dns (API key auth).
|
|
12
|
+
"""
|
|
10
13
|
|
|
11
14
|
def __init__(self, client):
|
|
12
15
|
self._client = client
|
|
13
16
|
|
|
14
17
|
# ── Zones ─────────────────────────────────────────────────────
|
|
15
18
|
|
|
16
|
-
def
|
|
19
|
+
def list(self) -> list[dict]:
|
|
17
20
|
"""List all DNS zones."""
|
|
18
|
-
data = self._client.get("/
|
|
21
|
+
data = self._client.get("/v1/dns/zones")
|
|
19
22
|
return data.get("zones", data) if isinstance(data, dict) else data
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
# Alias for backward compatibility
|
|
25
|
+
zones = list
|
|
26
|
+
|
|
27
|
+
def get(self, zone_name: str) -> dict:
|
|
22
28
|
"""Get details of a specific DNS zone."""
|
|
23
|
-
return self._client.get(f"/
|
|
29
|
+
return self._client.get(f"/v1/dns/zones/{zone_name}")
|
|
30
|
+
|
|
31
|
+
# Alias
|
|
32
|
+
get_zone = get
|
|
24
33
|
|
|
25
34
|
def create_zone(self, name: str, zone_type: str = "master") -> dict:
|
|
26
35
|
"""Create a new DNS zone."""
|
|
27
36
|
return self._client.post(
|
|
28
|
-
"/
|
|
37
|
+
"/v1/dns/zones",
|
|
29
38
|
json={"zone_name": name, "zone_type": zone_type},
|
|
30
39
|
)
|
|
31
40
|
|
|
32
41
|
def delete_zone(self, zone_name: str) -> dict:
|
|
33
42
|
"""Delete a DNS zone and all its records (permanent)."""
|
|
34
|
-
return self._client.delete(f"/
|
|
43
|
+
return self._client.delete(f"/v1/dns/zones/{zone_name}")
|
|
35
44
|
|
|
36
45
|
# ── Records ───────────────────────────────────────────────────
|
|
37
46
|
|
|
38
47
|
def records(self, zone_name: str) -> list[dict]:
|
|
39
48
|
"""List all records in a DNS zone."""
|
|
40
|
-
data = self._client.get(f"/
|
|
49
|
+
data = self._client.get(f"/v1/dns/zones/{zone_name}/records")
|
|
41
50
|
return data.get("records", data) if isinstance(data, dict) else data
|
|
42
51
|
|
|
43
52
|
def create_record(
|
|
@@ -58,10 +67,7 @@ class DNSService:
|
|
|
58
67
|
}
|
|
59
68
|
if priority is not None:
|
|
60
69
|
body["priority"] = priority
|
|
61
|
-
return self._client.post(
|
|
62
|
-
f"/api/v1/dashboard/dns/zones/{zone_name}/records",
|
|
63
|
-
json=body,
|
|
64
|
-
)
|
|
70
|
+
return self._client.post(f"/v1/dns/zones/{zone_name}/records", json=body)
|
|
65
71
|
|
|
66
72
|
def update_record(self, zone_name: str, record_id: str, **kwargs) -> dict:
|
|
67
73
|
"""Update a DNS record. Pass value=, ttl=, and/or priority= as kwargs."""
|
|
@@ -71,27 +77,22 @@ class DNSService:
|
|
|
71
77
|
for key in ("ttl", "priority"):
|
|
72
78
|
if key in kwargs and kwargs[key] is not None:
|
|
73
79
|
body[key] = kwargs[key]
|
|
74
|
-
return self._client.
|
|
75
|
-
f"/api/v1/dashboard/dns/zones/{zone_name}/records/{record_id}",
|
|
76
|
-
json=body,
|
|
77
|
-
)
|
|
80
|
+
return self._client.put(f"/v1/dns/zones/{zone_name}/records/{record_id}", json=body)
|
|
78
81
|
|
|
79
82
|
def delete_record(self, zone_name: str, record_id: str) -> dict:
|
|
80
83
|
"""Delete a DNS record (permanent)."""
|
|
81
|
-
return self._client.delete(
|
|
82
|
-
f"/api/v1/dashboard/dns/zones/{zone_name}/records/{record_id}"
|
|
83
|
-
)
|
|
84
|
+
return self._client.delete(f"/v1/dns/zones/{zone_name}/records/{record_id}")
|
|
84
85
|
|
|
85
86
|
# ── DNSSEC ────────────────────────────────────────────────────
|
|
86
87
|
|
|
87
88
|
def dnssec_status(self, zone_name: str) -> dict:
|
|
88
89
|
"""Get DNSSEC status for a zone."""
|
|
89
|
-
return self._client.get(f"/
|
|
90
|
+
return self._client.get(f"/v1/dns/zones/{zone_name}/dnssec")
|
|
90
91
|
|
|
91
92
|
def dnssec_activate(self, zone_name: str) -> dict:
|
|
92
93
|
"""Activate DNSSEC for a zone."""
|
|
93
|
-
return self._client.post(f"/
|
|
94
|
+
return self._client.post(f"/v1/dns/zones/{zone_name}/dnssec/activate")
|
|
94
95
|
|
|
95
96
|
def dnssec_deactivate(self, zone_name: str) -> dict:
|
|
96
97
|
"""Deactivate DNSSEC for a zone."""
|
|
97
|
-
return self._client.
|
|
98
|
+
return self._client.delete(f"/v1/dns/zones/{zone_name}/dnssec")
|
|
@@ -6,21 +6,30 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class IoTService:
|
|
9
|
-
"""Manage IoT devices, groups, rules, and telemetry.
|
|
9
|
+
"""Manage IoT devices, groups, rules, and telemetry.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/iot (API key auth).
|
|
12
|
+
"""
|
|
10
13
|
|
|
11
14
|
def __init__(self, client):
|
|
12
15
|
self._client = client
|
|
13
16
|
|
|
14
17
|
# ── Devices ───────────────────────────────────────────────────
|
|
15
18
|
|
|
16
|
-
def
|
|
19
|
+
def list(self) -> list[dict]:
|
|
17
20
|
"""List all IoT devices."""
|
|
18
|
-
data = self._client.get("/
|
|
21
|
+
data = self._client.get("/v1/iot/devices")
|
|
19
22
|
return data.get("devices", data) if isinstance(data, dict) else data
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
# Alias for backward compatibility
|
|
25
|
+
devices = list
|
|
26
|
+
|
|
27
|
+
def get(self, device_id: str) -> dict:
|
|
22
28
|
"""Get details of a specific device."""
|
|
23
|
-
return self._client.get(f"/
|
|
29
|
+
return self._client.get(f"/v1/iot/devices/{device_id}")
|
|
30
|
+
|
|
31
|
+
# Alias
|
|
32
|
+
get_device = get
|
|
24
33
|
|
|
25
34
|
def create_device(
|
|
26
35
|
self,
|
|
@@ -28,56 +37,36 @@ class IoTService:
|
|
|
28
37
|
name: str,
|
|
29
38
|
device_type: Optional[str] = None,
|
|
30
39
|
) -> dict:
|
|
31
|
-
"""Register a new IoT device.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
device_id: Unique device identifier.
|
|
35
|
-
name: Human-readable device name.
|
|
36
|
-
device_type: Optional device type classification.
|
|
37
|
-
"""
|
|
40
|
+
"""Register a new IoT device."""
|
|
38
41
|
body: dict = {"device_id": device_id, "name": name}
|
|
39
42
|
if device_type:
|
|
40
43
|
body["device_type"] = device_type
|
|
41
|
-
return self._client.post("/
|
|
44
|
+
return self._client.post("/v1/iot/devices", json=body)
|
|
42
45
|
|
|
43
46
|
def update_device(self, device_id: str, **kwargs) -> dict:
|
|
44
47
|
"""Update a device. Pass name=, device_type=, and/or is_active= as kwargs."""
|
|
45
48
|
body = {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if "is_active" in kwargs:
|
|
51
|
-
body["is_active"] = kwargs["is_active"]
|
|
52
|
-
return self._client.patch(
|
|
53
|
-
f"/api/v1/dashboard/iot/devices/{device_id}",
|
|
54
|
-
json=body,
|
|
55
|
-
)
|
|
49
|
+
for key in ("name", "device_type", "is_active"):
|
|
50
|
+
if key in kwargs:
|
|
51
|
+
body[key] = kwargs[key]
|
|
52
|
+
return self._client.patch(f"/v1/iot/devices/{device_id}", json=body)
|
|
56
53
|
|
|
57
54
|
def delete_device(self, device_id: str) -> dict:
|
|
58
55
|
"""Delete an IoT device."""
|
|
59
|
-
return self._client.delete(f"/
|
|
56
|
+
return self._client.delete(f"/v1/iot/devices/{device_id}")
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Returns username, password, client_id, broker_url, and port.
|
|
65
|
-
"""
|
|
66
|
-
return self._client.get(
|
|
67
|
-
f"/api/v1/dashboard/iot/devices/{device_id}/credentials"
|
|
68
|
-
)
|
|
58
|
+
# device_credentials() removed — no public /v1/iot endpoint.
|
|
59
|
+
# MQTT credentials are returned in the create_device() response.
|
|
69
60
|
|
|
70
61
|
def device_telemetry(self, device_id: str) -> dict:
|
|
71
62
|
"""Get latest telemetry data for a device."""
|
|
72
|
-
return self._client.get(
|
|
73
|
-
f"/api/v1/dashboard/iot/devices/{device_id}/telemetry/latest"
|
|
74
|
-
)
|
|
63
|
+
return self._client.get(f"/v1/iot/devices/{device_id}/telemetry/latest")
|
|
75
64
|
|
|
76
65
|
# ── Groups ────────────────────────────────────────────────────
|
|
77
66
|
|
|
78
67
|
def groups(self) -> list[dict]:
|
|
79
68
|
"""List all device groups."""
|
|
80
|
-
data = self._client.get("/
|
|
69
|
+
data = self._client.get("/v1/iot/groups")
|
|
81
70
|
return data.get("groups", data) if isinstance(data, dict) else data
|
|
82
71
|
|
|
83
72
|
def create_group(self, name: str, description: Optional[str] = None) -> dict:
|
|
@@ -85,17 +74,17 @@ class IoTService:
|
|
|
85
74
|
body: dict = {"name": name}
|
|
86
75
|
if description:
|
|
87
76
|
body["description"] = description
|
|
88
|
-
return self._client.post("/
|
|
77
|
+
return self._client.post("/v1/iot/groups", json=body)
|
|
89
78
|
|
|
90
79
|
def delete_group(self, group_id: str) -> dict:
|
|
91
80
|
"""Delete a device group."""
|
|
92
|
-
return self._client.delete(f"/
|
|
81
|
+
return self._client.delete(f"/v1/iot/groups/{group_id}")
|
|
93
82
|
|
|
94
83
|
# ── Rules ─────────────────────────────────────────────────────
|
|
95
84
|
|
|
96
85
|
def rules(self) -> list[dict]:
|
|
97
86
|
"""List all IoT rules."""
|
|
98
|
-
data = self._client.get("/
|
|
87
|
+
data = self._client.get("/v1/iot/rules")
|
|
99
88
|
return data.get("rules", data) if isinstance(data, dict) else data
|
|
100
89
|
|
|
101
90
|
def create_rule(
|
|
@@ -104,28 +93,15 @@ class IoTService:
|
|
|
104
93
|
rule_type: str,
|
|
105
94
|
severity: str = "warning",
|
|
106
95
|
) -> dict:
|
|
107
|
-
"""Create an IoT rule.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
name: Rule name.
|
|
111
|
-
rule_type: Rule type.
|
|
112
|
-
severity: Severity level (default: warning).
|
|
113
|
-
"""
|
|
96
|
+
"""Create an IoT rule."""
|
|
114
97
|
return self._client.post(
|
|
115
|
-
"/
|
|
98
|
+
"/v1/iot/rules",
|
|
116
99
|
json={"name": name, "type": rule_type, "severity": severity},
|
|
117
100
|
)
|
|
118
101
|
|
|
119
102
|
def delete_rule(self, rule_id: str) -> dict:
|
|
120
103
|
"""Delete an IoT rule."""
|
|
121
|
-
return self._client.delete(f"/
|
|
122
|
-
|
|
123
|
-
# ── Overview & Subscription ───────────────────────────────────
|
|
124
|
-
|
|
125
|
-
def overview(self) -> dict:
|
|
126
|
-
"""Get IoT fleet overview."""
|
|
127
|
-
return self._client.get("/api/v1/dashboard/iot/overview")
|
|
104
|
+
return self._client.delete(f"/v1/iot/rules/{rule_id}")
|
|
128
105
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return self._client.get("/api/v1/dashboard/iot/subscription")
|
|
106
|
+
# overview() and subscription() removed — no public /v1/iot equivalent.
|
|
107
|
+
# These are dashboard-only features.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Redis service -- Redis-as-a-Service management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RedisService:
|
|
9
|
+
"""Manage Redis instances.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/redis (API key auth).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(self) -> list[dict]:
|
|
18
|
+
"""List all Redis instances."""
|
|
19
|
+
data = self._client.get("/v1/redis/instances")
|
|
20
|
+
return data if isinstance(data, list) else data.get("instances", [])
|
|
21
|
+
|
|
22
|
+
def get(self, instance_id: str) -> dict:
|
|
23
|
+
"""Get details of a specific Redis instance (includes connection info)."""
|
|
24
|
+
return self._client.get(f"/v1/redis/instances/{instance_id}")
|
|
25
|
+
|
|
26
|
+
def create(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
plan: str = "redis-starter",
|
|
30
|
+
region: str = "no",
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Create a new Redis instance."""
|
|
33
|
+
return self._client.post(
|
|
34
|
+
"/v1/redis/instances",
|
|
35
|
+
json={"name": name, "plan": plan, "region": region},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def delete(self, instance_id: str) -> dict:
|
|
39
|
+
"""Delete a Redis instance (permanent)."""
|
|
40
|
+
return self._client.delete(f"/v1/redis/instances/{instance_id}")
|
|
41
|
+
|
|
42
|
+
def restart(self, instance_id: str) -> dict:
|
|
43
|
+
"""Restart a Redis instance."""
|
|
44
|
+
return self._client.post(f"/v1/redis/instances/{instance_id}:restart")
|
|
45
|
+
|
|
46
|
+
def plans(self) -> list[dict]:
|
|
47
|
+
"""List available Redis plans."""
|
|
48
|
+
data = self._client.get("/v1/redis/plans")
|
|
49
|
+
return data if isinstance(data, list) else data.get("plans", [])
|
|
50
|
+
|
|
51
|
+
def regions(self) -> list[dict]:
|
|
52
|
+
"""List available Redis regions."""
|
|
53
|
+
data = self._client.get("/v1/redis/regions")
|
|
54
|
+
return data if isinstance(data, list) else data.get("regions", [])
|
|
55
|
+
|
|
56
|
+
# credentials() removed — GET /instances/{id} includes connection info.
|
|
57
|
+
# Use rotate_credentials() to get new password.
|
|
58
|
+
|
|
59
|
+
def rotate_credentials(self, instance_id: str) -> dict:
|
|
60
|
+
"""Rotate Redis credentials. Returns new password."""
|
|
61
|
+
return self._client.post(f"/v1/redis/instances/{instance_id}:rotate-credentials")
|
|
62
|
+
|
|
63
|
+
def firewall_rules(self, instance_id: str) -> list[dict]:
|
|
64
|
+
"""List firewall rules for a Redis instance."""
|
|
65
|
+
data = self._client.get(f"/v1/redis/instances/{instance_id}/firewall")
|
|
66
|
+
return data if isinstance(data, list) else data.get("rules", [])
|
|
67
|
+
|
|
68
|
+
def add_firewall_rule(self, instance_id: str, source: str, description: str = "") -> dict:
|
|
69
|
+
"""Add a firewall rule to allow access from a CIDR."""
|
|
70
|
+
return self._client.post(
|
|
71
|
+
f"/v1/redis/instances/{instance_id}/firewall",
|
|
72
|
+
json={"source": source, "description": description},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def remove_firewall_rule(self, instance_id: str, rule_id: str) -> dict:
|
|
76
|
+
"""Remove a firewall rule."""
|
|
77
|
+
return self._client.delete(f"/v1/redis/instances/{instance_id}/firewall/{rule_id}")
|