wayscloud 0.2.3__tar.gz → 0.3.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.
- {wayscloud-0.2.3/wayscloud.egg-info → wayscloud-0.3.0}/PKG-INFO +1 -1
- {wayscloud-0.2.3 → wayscloud-0.3.0}/pyproject.toml +1 -1
- {wayscloud-0.2.3 → wayscloud-0.3.0}/setup.cfg +1 -1
- wayscloud-0.3.0/wayscloud/_version.py +1 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/database.py +19 -6
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/dns.py +6 -0
- wayscloud-0.3.0/wayscloud/services/iot.py +376 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/redis.py +9 -3
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/storage.py +15 -3
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/vps.py +30 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0/wayscloud.egg-info}/PKG-INFO +1 -1
- wayscloud-0.2.3/wayscloud/_version.py +0 -1
- wayscloud-0.2.3/wayscloud/services/iot.py +0 -107
- {wayscloud-0.2.3 → wayscloud-0.3.0}/LICENSE +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/README.md +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/tests/test_smoke.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/__init__.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/client.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/exceptions.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/py.typed +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/__init__.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/account.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/apps.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud/services/sms.py +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud.egg-info/SOURCES.txt +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud.egg-info/dependency_links.txt +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud.egg-info/requires.txt +0 -0
- {wayscloud-0.2.3 → wayscloud-0.3.0}/wayscloud.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -59,17 +59,30 @@ class DatabaseService:
|
|
|
59
59
|
data = self._client.get(f"/v1/databases/{db_type}/{name}/firewall")
|
|
60
60
|
return data if isinstance(data, list) else data.get("rules", [])
|
|
61
61
|
|
|
62
|
-
def add_firewall_rule(self, db_type: str, name: str,
|
|
63
|
-
"""Add a firewall rule to
|
|
62
|
+
def add_firewall_rule(self, db_type: str, name: str, ip_address: str, description: str = "") -> dict:
|
|
63
|
+
"""Add a firewall rule to whitelist an IP address.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
db_type: Database type (postgresql or mariadb).
|
|
67
|
+
name: Database name.
|
|
68
|
+
ip_address: IPv4 address to whitelist (e.g., '203.0.113.50').
|
|
69
|
+
description: Optional description.
|
|
70
|
+
"""
|
|
71
|
+
body: dict = {"ip_address": ip_address}
|
|
72
|
+
if description:
|
|
73
|
+
body["description"] = description
|
|
64
74
|
return self._client.post(
|
|
65
75
|
f"/v1/databases/{db_type}/{name}/firewall",
|
|
66
|
-
json=
|
|
76
|
+
json=body,
|
|
67
77
|
)
|
|
68
78
|
|
|
69
79
|
def remove_firewall_rule(self, db_type: str, name: str, rule_id: str) -> dict:
|
|
70
80
|
"""Remove a firewall rule."""
|
|
71
81
|
return self._client.delete(f"/v1/databases/{db_type}/{name}/firewall/{rule_id}")
|
|
72
82
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
# ── Tiers ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def tiers(self) -> list[dict]:
|
|
86
|
+
"""List available database tiers (standard, encrypted)."""
|
|
87
|
+
data = self._client.get("/v1/databases/tiers")
|
|
88
|
+
return data if isinstance(data, list) else data.get("tiers", [])
|
|
@@ -96,3 +96,9 @@ class DNSService:
|
|
|
96
96
|
def dnssec_deactivate(self, zone_name: str) -> dict:
|
|
97
97
|
"""Deactivate DNSSEC for a zone."""
|
|
98
98
|
return self._client.delete(f"/v1/dns/zones/{zone_name}/dnssec")
|
|
99
|
+
|
|
100
|
+
# ── Statistics ────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def statistics(self, zone_name: str) -> dict:
|
|
103
|
+
"""Get zone statistics (query counts, record breakdown)."""
|
|
104
|
+
return self._client.get(f"/v1/dns/zones/{zone_name}/statistics")
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""IoT service -- device, group, rule, alarm, telemetry, and notification management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IoTService:
|
|
9
|
+
"""Manage IoT devices, groups, rules, alarms, telemetry, and notifications.
|
|
10
|
+
|
|
11
|
+
All methods use the public API at /v1/iot (API key auth).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
# ── Devices ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
def list(self) -> list[dict]:
|
|
20
|
+
"""List all IoT devices."""
|
|
21
|
+
data = self._client.get("/v1/iot/devices")
|
|
22
|
+
return data.get("devices", data) if isinstance(data, dict) else data
|
|
23
|
+
|
|
24
|
+
# Alias
|
|
25
|
+
devices = list
|
|
26
|
+
|
|
27
|
+
def get(self, device_id: str) -> dict:
|
|
28
|
+
"""Get details of a specific device."""
|
|
29
|
+
return self._client.get(f"/v1/iot/devices/{device_id}")
|
|
30
|
+
|
|
31
|
+
# Alias
|
|
32
|
+
get_device = get
|
|
33
|
+
|
|
34
|
+
def create_device(
|
|
35
|
+
self,
|
|
36
|
+
device_id: str,
|
|
37
|
+
name: str,
|
|
38
|
+
device_type: Optional[str] = None,
|
|
39
|
+
description: Optional[str] = None,
|
|
40
|
+
metadata: Optional[dict] = None,
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""Register a new IoT device."""
|
|
43
|
+
body: dict = {"device_id": device_id, "name": name}
|
|
44
|
+
if device_type:
|
|
45
|
+
body["device_type"] = device_type
|
|
46
|
+
if description:
|
|
47
|
+
body["description"] = description
|
|
48
|
+
if metadata:
|
|
49
|
+
body["metadata"] = metadata
|
|
50
|
+
return self._client.post("/v1/iot/devices", json=body)
|
|
51
|
+
|
|
52
|
+
def update_device(self, device_id: str, **kwargs) -> dict:
|
|
53
|
+
"""Update a device. Pass name=, description=, device_type=, metadata=, is_active=."""
|
|
54
|
+
body = {}
|
|
55
|
+
for key in ("name", "description", "device_type", "metadata", "is_active"):
|
|
56
|
+
if key in kwargs:
|
|
57
|
+
body[key] = kwargs[key]
|
|
58
|
+
return self._client.patch(f"/v1/iot/devices/{device_id}", json=body)
|
|
59
|
+
|
|
60
|
+
def delete_device(self, device_id: str) -> dict:
|
|
61
|
+
"""Delete an IoT device."""
|
|
62
|
+
return self._client.delete(f"/v1/iot/devices/{device_id}")
|
|
63
|
+
|
|
64
|
+
def device_health(self, device_id: str) -> dict:
|
|
65
|
+
"""Get health score (0-100) with breakdown for a device."""
|
|
66
|
+
return self._client.get(f"/v1/iot/devices/{device_id}/health")
|
|
67
|
+
|
|
68
|
+
# ── Device Tags ──────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
def device_tags(self, device_id: str) -> list[str]:
|
|
71
|
+
"""List tags for a device."""
|
|
72
|
+
data = self._client.get(f"/v1/iot/devices/{device_id}/tags")
|
|
73
|
+
return data.get("tags", data) if isinstance(data, dict) else data
|
|
74
|
+
|
|
75
|
+
def add_device_tags(self, device_id: str, tags: list[str]) -> dict:
|
|
76
|
+
"""Add tags to a device."""
|
|
77
|
+
return self._client.post(
|
|
78
|
+
f"/v1/iot/devices/{device_id}/tags", json={"tags": tags}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def remove_device_tag(self, device_id: str, tag: str) -> dict:
|
|
82
|
+
"""Remove a tag from a device."""
|
|
83
|
+
return self._client.delete(f"/v1/iot/devices/{device_id}/tags/{tag}")
|
|
84
|
+
|
|
85
|
+
# ── Telemetry ────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def device_telemetry(self, device_id: str) -> dict:
|
|
88
|
+
"""Get latest telemetry data for a device."""
|
|
89
|
+
return self._client.get(f"/v1/iot/devices/{device_id}/telemetry/latest")
|
|
90
|
+
|
|
91
|
+
def device_telemetry_history(
|
|
92
|
+
self, device_id: str, datapoint_id: Optional[str] = None, hours: int = 24
|
|
93
|
+
) -> list[dict]:
|
|
94
|
+
"""Get historical telemetry for a device."""
|
|
95
|
+
params: dict = {"hours": hours}
|
|
96
|
+
if datapoint_id:
|
|
97
|
+
params["datapoint_id"] = datapoint_id
|
|
98
|
+
data = self._client.get(
|
|
99
|
+
f"/v1/iot/devices/{device_id}/telemetry", params=params
|
|
100
|
+
)
|
|
101
|
+
return data if isinstance(data, list) else data.get("data", [])
|
|
102
|
+
|
|
103
|
+
# ── Groups ────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def groups(self) -> list[dict]:
|
|
106
|
+
"""List all device groups."""
|
|
107
|
+
data = self._client.get("/v1/iot/groups")
|
|
108
|
+
return data.get("groups", data) if isinstance(data, dict) else data
|
|
109
|
+
|
|
110
|
+
def get_group(self, group_id: str) -> dict:
|
|
111
|
+
"""Get group details."""
|
|
112
|
+
return self._client.get(f"/v1/iot/groups/{group_id}")
|
|
113
|
+
|
|
114
|
+
def create_group(self, name: str, description: Optional[str] = None) -> dict:
|
|
115
|
+
"""Create a device group."""
|
|
116
|
+
body: dict = {"name": name}
|
|
117
|
+
if description:
|
|
118
|
+
body["description"] = description
|
|
119
|
+
return self._client.post("/v1/iot/groups", json=body)
|
|
120
|
+
|
|
121
|
+
def update_group(self, group_id: str, name: Optional[str] = None, description: Optional[str] = None) -> dict:
|
|
122
|
+
"""Update a device group."""
|
|
123
|
+
body: dict = {}
|
|
124
|
+
if name is not None:
|
|
125
|
+
body["name"] = name
|
|
126
|
+
if description is not None:
|
|
127
|
+
body["description"] = description
|
|
128
|
+
return self._client.put(f"/v1/iot/groups/{group_id}", json=body)
|
|
129
|
+
|
|
130
|
+
def delete_group(self, group_id: str) -> dict:
|
|
131
|
+
"""Delete a device group."""
|
|
132
|
+
return self._client.delete(f"/v1/iot/groups/{group_id}")
|
|
133
|
+
|
|
134
|
+
def add_devices_to_group(self, group_id: str, device_ids: list[str]) -> dict:
|
|
135
|
+
"""Add devices to a group."""
|
|
136
|
+
return self._client.post(
|
|
137
|
+
f"/v1/iot/groups/{group_id}/devices", json={"device_ids": device_ids}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def remove_device_from_group(self, group_id: str, device_id: str) -> dict:
|
|
141
|
+
"""Remove a device from a group."""
|
|
142
|
+
return self._client.delete(f"/v1/iot/groups/{group_id}/devices/{device_id}")
|
|
143
|
+
|
|
144
|
+
# ── Device Profiles ──────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def profiles(self) -> list[dict]:
|
|
147
|
+
"""List device profiles."""
|
|
148
|
+
data = self._client.get("/v1/iot/profiles")
|
|
149
|
+
return data if isinstance(data, list) else data.get("profiles", [])
|
|
150
|
+
|
|
151
|
+
def get_profile(self, profile_id: str) -> dict:
|
|
152
|
+
"""Get profile details."""
|
|
153
|
+
return self._client.get(f"/v1/iot/profiles/{profile_id}")
|
|
154
|
+
|
|
155
|
+
def create_profile(self, name: str, **kwargs) -> dict:
|
|
156
|
+
"""Create a device profile. Optional kwargs: description, expected_topics, reporting_interval_seconds, metadata."""
|
|
157
|
+
body: dict = {"name": name}
|
|
158
|
+
for key in ("description", "expected_topics", "reporting_interval_seconds", "metadata"):
|
|
159
|
+
if key in kwargs and kwargs[key] is not None:
|
|
160
|
+
body[key] = kwargs[key]
|
|
161
|
+
return self._client.post("/v1/iot/profiles", json=body)
|
|
162
|
+
|
|
163
|
+
def update_profile(self, profile_id: str, **kwargs) -> dict:
|
|
164
|
+
"""Update a device profile."""
|
|
165
|
+
body: dict = {}
|
|
166
|
+
for key in ("name", "description", "expected_topics", "reporting_interval_seconds", "metadata"):
|
|
167
|
+
if key in kwargs and kwargs[key] is not None:
|
|
168
|
+
body[key] = kwargs[key]
|
|
169
|
+
return self._client.put(f"/v1/iot/profiles/{profile_id}", json=body)
|
|
170
|
+
|
|
171
|
+
def delete_profile(self, profile_id: str) -> dict:
|
|
172
|
+
"""Delete a device profile."""
|
|
173
|
+
return self._client.delete(f"/v1/iot/profiles/{profile_id}")
|
|
174
|
+
|
|
175
|
+
def apply_profile(self, profile_id: str, device_id: str) -> dict:
|
|
176
|
+
"""Apply a profile to a device."""
|
|
177
|
+
return self._client.post(f"/v1/iot/profiles/{profile_id}/apply/{device_id}")
|
|
178
|
+
|
|
179
|
+
# ── Rules ─────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
def rules(self, scope_type: Optional[str] = None) -> list[dict]:
|
|
182
|
+
"""List alarm rules. Optional scope_type filter."""
|
|
183
|
+
params = {"scope_type": scope_type} if scope_type else None
|
|
184
|
+
data = self._client.get("/v1/iot/rules", params=params)
|
|
185
|
+
return data.get("rules", data) if isinstance(data, dict) else data
|
|
186
|
+
|
|
187
|
+
def get_rule(self, rule_id: str) -> dict:
|
|
188
|
+
"""Get rule details."""
|
|
189
|
+
return self._client.get(f"/v1/iot/rules/{rule_id}")
|
|
190
|
+
|
|
191
|
+
def create_rule(self, name: str, rule_type: str, severity: str = "warning", **kwargs) -> dict:
|
|
192
|
+
"""Create an alarm rule. Optional kwargs: description, scope_type, scope_device_id, scope_group_id, config, actions, cooldown_seconds, is_enabled, auto_resolve."""
|
|
193
|
+
body: dict = {"name": name, "rule_type": rule_type, "severity": severity}
|
|
194
|
+
for key in ("description", "scope_type", "scope_device_id", "scope_group_id",
|
|
195
|
+
"scope_profile_id", "config", "actions", "cooldown_seconds",
|
|
196
|
+
"is_enabled", "auto_resolve"):
|
|
197
|
+
if key in kwargs and kwargs[key] is not None:
|
|
198
|
+
body[key] = kwargs[key]
|
|
199
|
+
return self._client.post("/v1/iot/rules", json=body)
|
|
200
|
+
|
|
201
|
+
def update_rule(self, rule_id: str, **kwargs) -> dict:
|
|
202
|
+
"""Update an alarm rule."""
|
|
203
|
+
body: dict = {}
|
|
204
|
+
for key in ("name", "description", "config", "severity", "actions",
|
|
205
|
+
"cooldown_seconds", "is_enabled", "auto_resolve"):
|
|
206
|
+
if key in kwargs and kwargs[key] is not None:
|
|
207
|
+
body[key] = kwargs[key]
|
|
208
|
+
return self._client.put(f"/v1/iot/rules/{rule_id}", json=body)
|
|
209
|
+
|
|
210
|
+
def delete_rule(self, rule_id: str) -> dict:
|
|
211
|
+
"""Delete an alarm rule."""
|
|
212
|
+
return self._client.delete(f"/v1/iot/rules/{rule_id}")
|
|
213
|
+
|
|
214
|
+
def enable_rule(self, rule_id: str) -> dict:
|
|
215
|
+
"""Enable an alarm rule."""
|
|
216
|
+
return self._client.post(f"/v1/iot/rules/{rule_id}/enable")
|
|
217
|
+
|
|
218
|
+
def disable_rule(self, rule_id: str) -> dict:
|
|
219
|
+
"""Disable an alarm rule."""
|
|
220
|
+
return self._client.post(f"/v1/iot/rules/{rule_id}/disable")
|
|
221
|
+
|
|
222
|
+
# ── Alarms ────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def alarms(self, **kwargs) -> list[dict]:
|
|
225
|
+
"""List alarms. Optional kwargs: status, severity, device_id, rule_id, page, page_size."""
|
|
226
|
+
params = {k: v for k, v in kwargs.items() if v is not None}
|
|
227
|
+
data = self._client.get("/v1/iot/alarms", params=params or None)
|
|
228
|
+
return data.get("alarms", data) if isinstance(data, dict) else data
|
|
229
|
+
|
|
230
|
+
def alarm_summary(self) -> dict:
|
|
231
|
+
"""Get alarm counts by status and severity."""
|
|
232
|
+
return self._client.get("/v1/iot/alarms/summary")
|
|
233
|
+
|
|
234
|
+
def get_alarm(self, alarm_id: str) -> dict:
|
|
235
|
+
"""Get alarm details."""
|
|
236
|
+
return self._client.get(f"/v1/iot/alarms/{alarm_id}")
|
|
237
|
+
|
|
238
|
+
def alarm_timeline(self, alarm_id: str) -> list[dict]:
|
|
239
|
+
"""Get alarm event timeline."""
|
|
240
|
+
data = self._client.get(f"/v1/iot/alarms/{alarm_id}/timeline")
|
|
241
|
+
return data if isinstance(data, list) else data.get("events", [])
|
|
242
|
+
|
|
243
|
+
def acknowledge_alarm(self, alarm_id: str, note: Optional[str] = None) -> dict:
|
|
244
|
+
"""Acknowledge an alarm."""
|
|
245
|
+
body = {"note": note} if note else {}
|
|
246
|
+
return self._client.post(f"/v1/iot/alarms/{alarm_id}/acknowledge", json=body)
|
|
247
|
+
|
|
248
|
+
def resolve_alarm(self, alarm_id: str, note: Optional[str] = None) -> dict:
|
|
249
|
+
"""Resolve an alarm."""
|
|
250
|
+
body = {"note": note} if note else {}
|
|
251
|
+
return self._client.post(f"/v1/iot/alarms/{alarm_id}/resolve", json=body)
|
|
252
|
+
|
|
253
|
+
def reopen_alarm(self, alarm_id: str, note: Optional[str] = None) -> dict:
|
|
254
|
+
"""Reopen a resolved alarm."""
|
|
255
|
+
body = {"note": note} if note else {}
|
|
256
|
+
return self._client.post(f"/v1/iot/alarms/{alarm_id}/reopen", json=body)
|
|
257
|
+
|
|
258
|
+
# ── Datapoints ────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def datapoints(self) -> list[dict]:
|
|
261
|
+
"""List datapoint definitions."""
|
|
262
|
+
data = self._client.get("/v1/iot/datapoints")
|
|
263
|
+
return data if isinstance(data, list) else data.get("datapoints", [])
|
|
264
|
+
|
|
265
|
+
def get_datapoint(self, datapoint_id: str) -> dict:
|
|
266
|
+
"""Get datapoint details."""
|
|
267
|
+
return self._client.get(f"/v1/iot/datapoints/{datapoint_id}")
|
|
268
|
+
|
|
269
|
+
def create_datapoint(self, key: str, label: str, unit: str = "", data_type: str = "number") -> dict:
|
|
270
|
+
"""Create a datapoint definition."""
|
|
271
|
+
return self._client.post(
|
|
272
|
+
"/v1/iot/datapoints",
|
|
273
|
+
json={"key": key, "label": label, "unit": unit, "data_type": data_type},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def update_datapoint(self, datapoint_id: str, label: Optional[str] = None, unit: Optional[str] = None) -> dict:
|
|
277
|
+
"""Update a datapoint definition."""
|
|
278
|
+
body: dict = {}
|
|
279
|
+
if label is not None:
|
|
280
|
+
body["label"] = label
|
|
281
|
+
if unit is not None:
|
|
282
|
+
body["unit"] = unit
|
|
283
|
+
return self._client.put(f"/v1/iot/datapoints/{datapoint_id}", json=body)
|
|
284
|
+
|
|
285
|
+
def delete_datapoint(self, datapoint_id: str) -> dict:
|
|
286
|
+
"""Delete a datapoint definition."""
|
|
287
|
+
return self._client.delete(f"/v1/iot/datapoints/{datapoint_id}")
|
|
288
|
+
|
|
289
|
+
# ── Notification Channels ────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def notification_channels(self) -> list[dict]:
|
|
292
|
+
"""List notification channels."""
|
|
293
|
+
data = self._client.get("/v1/iot/notifications/channels")
|
|
294
|
+
return data if isinstance(data, list) else data.get("channels", [])
|
|
295
|
+
|
|
296
|
+
def get_notification_channel(self, channel_id: str) -> dict:
|
|
297
|
+
"""Get notification channel details."""
|
|
298
|
+
return self._client.get(f"/v1/iot/notifications/channels/{channel_id}")
|
|
299
|
+
|
|
300
|
+
def create_notification_channel(self, name: str, channel_type: str, config: dict, is_enabled: bool = True) -> dict:
|
|
301
|
+
"""Create a notification channel (email, webhook, slack, teams, sms)."""
|
|
302
|
+
return self._client.post(
|
|
303
|
+
"/v1/iot/notifications/channels",
|
|
304
|
+
json={"name": name, "channel_type": channel_type, "config": config, "is_enabled": is_enabled},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def update_notification_channel(self, channel_id: str, **kwargs) -> dict:
|
|
308
|
+
"""Update a notification channel."""
|
|
309
|
+
body: dict = {}
|
|
310
|
+
for key in ("name", "config", "is_enabled"):
|
|
311
|
+
if key in kwargs and kwargs[key] is not None:
|
|
312
|
+
body[key] = kwargs[key]
|
|
313
|
+
return self._client.put(f"/v1/iot/notifications/channels/{channel_id}", json=body)
|
|
314
|
+
|
|
315
|
+
def delete_notification_channel(self, channel_id: str) -> dict:
|
|
316
|
+
"""Delete a notification channel."""
|
|
317
|
+
return self._client.delete(f"/v1/iot/notifications/channels/{channel_id}")
|
|
318
|
+
|
|
319
|
+
def test_notification_channel(self, channel_id: str) -> dict:
|
|
320
|
+
"""Send a test notification through a channel."""
|
|
321
|
+
return self._client.post(f"/v1/iot/notifications/channels/{channel_id}/test")
|
|
322
|
+
|
|
323
|
+
# ── Notification Policies ────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
def notification_policies(self) -> list[dict]:
|
|
326
|
+
"""List notification policies."""
|
|
327
|
+
data = self._client.get("/v1/iot/notifications/policies")
|
|
328
|
+
return data if isinstance(data, list) else data.get("policies", [])
|
|
329
|
+
|
|
330
|
+
def get_notification_policy(self, policy_id: str) -> dict:
|
|
331
|
+
"""Get notification policy details."""
|
|
332
|
+
return self._client.get(f"/v1/iot/notifications/policies/{policy_id}")
|
|
333
|
+
|
|
334
|
+
def create_notification_policy(self, name: str, channel_ids: list[str], **kwargs) -> dict:
|
|
335
|
+
"""Create a notification policy. Optional: description, severity_filter, rule_ids, scope_type, scope_ids, event_types, cooldown_seconds, is_enabled."""
|
|
336
|
+
body: dict = {"name": name, "channel_ids": channel_ids}
|
|
337
|
+
for key in ("description", "severity_filter", "rule_ids", "scope_type",
|
|
338
|
+
"scope_ids", "event_types", "cooldown_seconds", "is_enabled"):
|
|
339
|
+
if key in kwargs and kwargs[key] is not None:
|
|
340
|
+
body[key] = kwargs[key]
|
|
341
|
+
return self._client.post("/v1/iot/notifications/policies", json=body)
|
|
342
|
+
|
|
343
|
+
def update_notification_policy(self, policy_id: str, **kwargs) -> dict:
|
|
344
|
+
"""Update a notification policy."""
|
|
345
|
+
body: dict = {}
|
|
346
|
+
for key in ("name", "description", "severity_filter", "event_types",
|
|
347
|
+
"channel_ids", "cooldown_seconds", "is_enabled"):
|
|
348
|
+
if key in kwargs and kwargs[key] is not None:
|
|
349
|
+
body[key] = kwargs[key]
|
|
350
|
+
return self._client.put(f"/v1/iot/notifications/policies/{policy_id}", json=body)
|
|
351
|
+
|
|
352
|
+
def delete_notification_policy(self, policy_id: str) -> dict:
|
|
353
|
+
"""Delete a notification policy."""
|
|
354
|
+
return self._client.delete(f"/v1/iot/notifications/policies/{policy_id}")
|
|
355
|
+
|
|
356
|
+
# ── Notification Deliveries ──────────────────────────────────
|
|
357
|
+
|
|
358
|
+
def notification_deliveries(self, **kwargs) -> list[dict]:
|
|
359
|
+
"""List notification delivery history. Optional: channel_type, status, created_after, limit, offset."""
|
|
360
|
+
params = {k: v for k, v in kwargs.items() if v is not None}
|
|
361
|
+
data = self._client.get("/v1/iot/notifications/deliveries", params=params or None)
|
|
362
|
+
return data if isinstance(data, list) else data.get("deliveries", [])
|
|
363
|
+
|
|
364
|
+
# ── MQTT & Subscription ──────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
def mqtt_credentials(self) -> dict:
|
|
367
|
+
"""Get MQTT broker connection details."""
|
|
368
|
+
return self._client.get("/v1/iot/credentials")
|
|
369
|
+
|
|
370
|
+
def subscription(self) -> dict:
|
|
371
|
+
"""Get current IoT plan, device limit, and usage."""
|
|
372
|
+
return self._client.get("/v1/iot/subscription")
|
|
373
|
+
|
|
374
|
+
def usage(self) -> dict:
|
|
375
|
+
"""Get message counts, data usage, and device activity."""
|
|
376
|
+
return self._client.get("/v1/iot/usage")
|
|
@@ -65,11 +65,17 @@ class RedisService:
|
|
|
65
65
|
data = self._client.get(f"/v1/redis/instances/{instance_id}/firewall")
|
|
66
66
|
return data if isinstance(data, list) else data.get("rules", [])
|
|
67
67
|
|
|
68
|
-
def add_firewall_rule(self, instance_id: str,
|
|
69
|
-
"""Add a firewall rule to allow access from a CIDR.
|
|
68
|
+
def add_firewall_rule(self, instance_id: str, ip_address: str, description: str = "") -> dict:
|
|
69
|
+
"""Add a firewall rule to allow access from a CIDR.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
instance_id: Redis instance ID.
|
|
73
|
+
ip_address: IP address or CIDR (e.g., '192.0.2.100/32').
|
|
74
|
+
description: Optional description.
|
|
75
|
+
"""
|
|
70
76
|
return self._client.post(
|
|
71
77
|
f"/v1/redis/instances/{instance_id}/firewall",
|
|
72
|
-
json={"
|
|
78
|
+
json={"ip_address": ip_address, "description": description},
|
|
73
79
|
)
|
|
74
80
|
|
|
75
81
|
def remove_firewall_rule(self, instance_id: str, rule_id: str) -> dict:
|
|
@@ -58,6 +58,18 @@ class StorageService:
|
|
|
58
58
|
f"/v1/storage/buckets/{bucket_name}/keys/{key_id}"
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
# ── Visibility ────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def set_visibility(self, bucket_name: str, is_public: bool) -> dict:
|
|
64
|
+
"""Toggle bucket public/private access."""
|
|
65
|
+
return self._client.patch(
|
|
66
|
+
f"/v1/storage/buckets/{bucket_name}/visibility",
|
|
67
|
+
json={"is_public": is_public},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# ── Tiers ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def tiers(self) -> list[dict]:
|
|
73
|
+
"""List available storage tiers."""
|
|
74
|
+
data = self._client.get("/v1/storage/tiers")
|
|
75
|
+
return data if isinstance(data, list) else data.get("tiers", [])
|
|
@@ -69,6 +69,30 @@ class VPSService:
|
|
|
69
69
|
"""Get real-time VPS status (power, CPU, memory)."""
|
|
70
70
|
return self._client.get(f"/v1/vps/{vps_id}/status")
|
|
71
71
|
|
|
72
|
+
def update(
|
|
73
|
+
self,
|
|
74
|
+
vps_id: str,
|
|
75
|
+
display_name: Optional[str] = None,
|
|
76
|
+
tags: Optional[list[str]] = None,
|
|
77
|
+
labels: Optional[dict[str, str]] = None,
|
|
78
|
+
) -> dict:
|
|
79
|
+
"""Update VPS metadata (display name, tags, labels)."""
|
|
80
|
+
body: dict = {}
|
|
81
|
+
if display_name is not None:
|
|
82
|
+
body["display_name"] = display_name
|
|
83
|
+
if tags is not None:
|
|
84
|
+
body["tags"] = tags
|
|
85
|
+
if labels is not None:
|
|
86
|
+
body["labels"] = labels
|
|
87
|
+
return self._client.patch(f"/v1/vps/{vps_id}", json=body)
|
|
88
|
+
|
|
89
|
+
def upgrade(self, vps_id: str, new_plan_code: str) -> dict:
|
|
90
|
+
"""Upgrade VPS to a higher plan. Norway/Proxmox only."""
|
|
91
|
+
return self._client.post(
|
|
92
|
+
f"/v1/vps/{vps_id}/upgrade",
|
|
93
|
+
json={"new_plan_code": new_plan_code},
|
|
94
|
+
)
|
|
95
|
+
|
|
72
96
|
# ── Plans & Discovery ────────────────────────────────────────
|
|
73
97
|
|
|
74
98
|
def plans(self, region: Optional[str] = None) -> list[dict]:
|
|
@@ -89,6 +113,12 @@ class VPSService:
|
|
|
89
113
|
data = self._client.get("/v1/vps/regions")
|
|
90
114
|
return data if isinstance(data, list) else data.get("regions", [])
|
|
91
115
|
|
|
116
|
+
def addons(self, region: Optional[str] = None) -> list[dict]:
|
|
117
|
+
"""List available VPS addons (extra disk, vCPU)."""
|
|
118
|
+
params = {"region": region} if region else None
|
|
119
|
+
data = self._client.get("/v1/vps/addons/", params=params)
|
|
120
|
+
return data if isinstance(data, list) else data.get("addons", [])
|
|
121
|
+
|
|
92
122
|
# ── Snapshots ────────────────────────────────────────────────
|
|
93
123
|
|
|
94
124
|
def snapshots(self, vps_id: str) -> list[dict]:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.3"
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""IoT service -- device, group, and rule management."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class IoTService:
|
|
9
|
-
"""Manage IoT devices, groups, rules, and telemetry.
|
|
10
|
-
|
|
11
|
-
All methods use the public API at /v1/iot (API key auth).
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self, client):
|
|
15
|
-
self._client = client
|
|
16
|
-
|
|
17
|
-
# ── Devices ───────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
def list(self) -> list[dict]:
|
|
20
|
-
"""List all IoT devices."""
|
|
21
|
-
data = self._client.get("/v1/iot/devices")
|
|
22
|
-
return data.get("devices", data) if isinstance(data, dict) else data
|
|
23
|
-
|
|
24
|
-
# Alias for backward compatibility
|
|
25
|
-
devices = list
|
|
26
|
-
|
|
27
|
-
def get(self, device_id: str) -> dict:
|
|
28
|
-
"""Get details of a specific device."""
|
|
29
|
-
return self._client.get(f"/v1/iot/devices/{device_id}")
|
|
30
|
-
|
|
31
|
-
# Alias
|
|
32
|
-
get_device = get
|
|
33
|
-
|
|
34
|
-
def create_device(
|
|
35
|
-
self,
|
|
36
|
-
device_id: str,
|
|
37
|
-
name: str,
|
|
38
|
-
device_type: Optional[str] = None,
|
|
39
|
-
) -> dict:
|
|
40
|
-
"""Register a new IoT device."""
|
|
41
|
-
body: dict = {"device_id": device_id, "name": name}
|
|
42
|
-
if device_type:
|
|
43
|
-
body["device_type"] = device_type
|
|
44
|
-
return self._client.post("/v1/iot/devices", json=body)
|
|
45
|
-
|
|
46
|
-
def update_device(self, device_id: str, **kwargs) -> dict:
|
|
47
|
-
"""Update a device. Pass name=, device_type=, and/or is_active= as kwargs."""
|
|
48
|
-
body = {}
|
|
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)
|
|
53
|
-
|
|
54
|
-
def delete_device(self, device_id: str) -> dict:
|
|
55
|
-
"""Delete an IoT device."""
|
|
56
|
-
return self._client.delete(f"/v1/iot/devices/{device_id}")
|
|
57
|
-
|
|
58
|
-
# device_credentials() removed — no public /v1/iot endpoint.
|
|
59
|
-
# MQTT credentials are returned in the create_device() response.
|
|
60
|
-
|
|
61
|
-
def device_telemetry(self, device_id: str) -> dict:
|
|
62
|
-
"""Get latest telemetry data for a device."""
|
|
63
|
-
return self._client.get(f"/v1/iot/devices/{device_id}/telemetry/latest")
|
|
64
|
-
|
|
65
|
-
# ── Groups ────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
def groups(self) -> list[dict]:
|
|
68
|
-
"""List all device groups."""
|
|
69
|
-
data = self._client.get("/v1/iot/groups")
|
|
70
|
-
return data.get("groups", data) if isinstance(data, dict) else data
|
|
71
|
-
|
|
72
|
-
def create_group(self, name: str, description: Optional[str] = None) -> dict:
|
|
73
|
-
"""Create a device group."""
|
|
74
|
-
body: dict = {"name": name}
|
|
75
|
-
if description:
|
|
76
|
-
body["description"] = description
|
|
77
|
-
return self._client.post("/v1/iot/groups", json=body)
|
|
78
|
-
|
|
79
|
-
def delete_group(self, group_id: str) -> dict:
|
|
80
|
-
"""Delete a device group."""
|
|
81
|
-
return self._client.delete(f"/v1/iot/groups/{group_id}")
|
|
82
|
-
|
|
83
|
-
# ── Rules ─────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
def rules(self) -> list[dict]:
|
|
86
|
-
"""List all IoT rules."""
|
|
87
|
-
data = self._client.get("/v1/iot/rules")
|
|
88
|
-
return data.get("rules", data) if isinstance(data, dict) else data
|
|
89
|
-
|
|
90
|
-
def create_rule(
|
|
91
|
-
self,
|
|
92
|
-
name: str,
|
|
93
|
-
rule_type: str,
|
|
94
|
-
severity: str = "warning",
|
|
95
|
-
) -> dict:
|
|
96
|
-
"""Create an IoT rule."""
|
|
97
|
-
return self._client.post(
|
|
98
|
-
"/v1/iot/rules",
|
|
99
|
-
json={"name": name, "type": rule_type, "severity": severity},
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
def delete_rule(self, rule_id: str) -> dict:
|
|
103
|
-
"""Delete an IoT rule."""
|
|
104
|
-
return self._client.delete(f"/v1/iot/rules/{rule_id}")
|
|
105
|
-
|
|
106
|
-
# overview() and subscription() removed — no public /v1/iot equivalent.
|
|
107
|
-
# These are dashboard-only features.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|