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.
Files changed (32) hide show
  1. {wayscloud-0.1.0/wayscloud.egg-info → wayscloud-0.2.3}/PKG-INFO +4 -2
  2. {wayscloud-0.1.0 → wayscloud-0.2.3}/pyproject.toml +4 -2
  3. {wayscloud-0.1.0 → wayscloud-0.2.3}/setup.cfg +1 -1
  4. wayscloud-0.2.3/wayscloud/__init__.py +25 -0
  5. wayscloud-0.2.3/wayscloud/_version.py +1 -0
  6. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/client.py +11 -2
  7. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/exceptions.py +16 -1
  8. wayscloud-0.2.3/wayscloud/services/apps.py +104 -0
  9. wayscloud-0.2.3/wayscloud/services/database.py +75 -0
  10. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/dns.py +23 -22
  11. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/iot.py +33 -57
  12. wayscloud-0.2.3/wayscloud/services/redis.py +77 -0
  13. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/sms.py +17 -8
  14. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/storage.py +26 -21
  15. wayscloud-0.2.3/wayscloud/services/vps.py +192 -0
  16. {wayscloud-0.1.0 → wayscloud-0.2.3/wayscloud.egg-info}/PKG-INFO +4 -2
  17. wayscloud-0.1.0/wayscloud/__init__.py +0 -5
  18. wayscloud-0.1.0/wayscloud/_version.py +0 -1
  19. wayscloud-0.1.0/wayscloud/services/apps.py +0 -132
  20. wayscloud-0.1.0/wayscloud/services/database.py +0 -69
  21. wayscloud-0.1.0/wayscloud/services/redis.py +0 -67
  22. wayscloud-0.1.0/wayscloud/services/vps.py +0 -127
  23. {wayscloud-0.1.0 → wayscloud-0.2.3}/LICENSE +0 -0
  24. {wayscloud-0.1.0 → wayscloud-0.2.3}/README.md +0 -0
  25. {wayscloud-0.1.0 → wayscloud-0.2.3}/tests/test_smoke.py +0 -0
  26. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/py.typed +0 -0
  27. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/__init__.py +0 -0
  28. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud/services/account.py +0 -0
  29. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/SOURCES.txt +0 -0
  30. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/dependency_links.txt +0 -0
  31. {wayscloud-0.1.0 → wayscloud-0.2.3}/wayscloud.egg-info/requires.txt +0 -0
  32. {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.1.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.1.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*"]
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = wayscloud
3
- version = 0.1.0
3
+ version = 0.2.3
4
4
  description = Official Python SDK for the WAYSCloud API
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -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 == 422:
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 failed."""
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 zones(self) -> list[dict]:
19
+ def list(self) -> list[dict]:
17
20
  """List all DNS zones."""
18
- data = self._client.get("/api/v1/dashboard/dns/zones")
21
+ data = self._client.get("/v1/dns/zones")
19
22
  return data.get("zones", data) if isinstance(data, dict) else data
20
23
 
21
- def get_zone(self, zone_name: str) -> dict:
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"/api/v1/dashboard/dns/zones/{zone_name}")
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
- "/api/v1/dashboard/dns/zones",
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"/api/v1/dashboard/dns/zones/{zone_name}")
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"/api/v1/dashboard/dns/zones/{zone_name}/records")
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.patch(
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"/api/v1/dashboard/dns/zones/{zone_name}/dnssec")
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"/api/v1/dashboard/dns/zones/{zone_name}/dnssec/activate")
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.post(f"/api/v1/dashboard/dns/zones/{zone_name}/dnssec/deactivate")
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 devices(self) -> list[dict]:
19
+ def list(self) -> list[dict]:
17
20
  """List all IoT devices."""
18
- data = self._client.get("/api/v1/dashboard/iot/devices")
21
+ data = self._client.get("/v1/iot/devices")
19
22
  return data.get("devices", data) if isinstance(data, dict) else data
20
23
 
21
- def get_device(self, device_id: str) -> dict:
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"/api/v1/dashboard/iot/devices/{device_id}")
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("/api/v1/dashboard/iot/devices", json=body)
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
- if "name" in kwargs:
47
- body["name"] = kwargs["name"]
48
- if "device_type" in kwargs:
49
- body["device_type"] = kwargs["device_type"]
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"/api/v1/dashboard/iot/devices/{device_id}")
56
+ return self._client.delete(f"/v1/iot/devices/{device_id}")
60
57
 
61
- def device_credentials(self, device_id: str) -> dict:
62
- """Get MQTT credentials for a device.
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("/api/v1/dashboard/iot/groups")
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("/api/v1/dashboard/iot/groups", json=body)
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"/api/v1/dashboard/iot/groups/{group_id}")
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("/api/v1/dashboard/iot/rules")
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
- "/api/v1/dashboard/iot/rules",
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"/api/v1/dashboard/iot/rules/{rule_id}")
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
- def subscription(self) -> dict:
130
- """Get IoT subscription info."""
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}")