Coverage for src/edwh_uptime_plugin/uptimerobot.py: 81%
124 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-18 16:57 +0100
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-18 16:57 +0100
1import enum
2import json
3import sys
4import typing
5from typing import Any, Optional
7import edwh
8import requests
9from edwh import check_env
10from typing_extensions import NotRequired, Required
11from yayarl import URL
13AnyDict: typing.TypeAlias = dict[str, Any]
16class UptimeRobotException(Exception):
17 status_code: int
18 message: str
19 response: requests.Response
20 extra: Optional[Any]
22 def __init__(self, response: requests.Response, extra: Any = None):
23 self.response = response
24 self.status_code = response.status_code
25 self.message = response.text
26 self.extra = extra
29class UptimeRobotErrorResponse(typing.TypedDict):
30 type: str
31 message: str
33 parameter_name: NotRequired[str]
34 passed_value: NotRequired[str]
37class UptimeRobotPagination(typing.TypedDict):
38 offset: int
39 limit: int
40 total: int
43class UptimeRobotAccount(typing.TypedDict, total=False):
44 email: str
45 user_id: int
46 firstname: str
47 # ...
50class UptimeRobotResponse(typing.TypedDict, total=False):
51 stat: typing.Literal["ok", "fail"]
52 error: NotRequired[UptimeRobotErrorResponse]
53 pagination: NotRequired[UptimeRobotPagination]
54 # actual data differs per endpoint:
55 monitor: NotRequired["UptimeRobotMonitor"]
56 monitors: NotRequired[list["UptimeRobotMonitor"]]
57 account: NotRequired[UptimeRobotAccount]
60class UptimeRobotMonitor(typing.TypedDict, total=False):
61 id: Required[int]
63 # may or may not be there:
64 friendly_name: NotRequired[str]
65 url: NotRequired[str]
66 type: NotRequired[int]
67 sub_type: NotRequired[str]
68 keyword_type: NotRequired[Optional[str]]
69 keyword_case_type: NotRequired[Optional[str]]
70 keyword_value: NotRequired[str]
71 http_username: NotRequired[str]
72 http_password: NotRequired[str]
73 port: NotRequired[str]
74 interval: NotRequired[int]
75 timeout: NotRequired[int]
76 status: NotRequired[int]
77 create_datetime: NotRequired[int]
80class MonitorType(enum.Enum):
81 HTTP = 1
82 KEYWORD = 2
83 PING = 3
84 PORT = 4
85 HEARTBEAT = 5
88class UptimeRobot:
89 base = URL("https://api.uptimerobot.com/v2/")
91 _api_key: str = "" # cached version from .env
92 _verbose: bool = False
94 @property
95 def api_key(self) -> str:
96 if not self._api_key:
97 self._api_key = check_env(
98 "UPTIMEROBOT_APIKEY",
99 default=None,
100 comment="The API key used to manage UptimeRobot monitors.",
101 )
103 return self._api_key
105 def set_verbosity(self, verbose: bool = None) -> None:
106 if verbose is None: 106 ↛ 109line 106 didn't jump to line 109, because the condition on line 106 was never false
107 verbose = edwh.get_env_value("IS_DEBUG", "0") == "1"
109 self._verbose = verbose
111 def _log(self, *args: Any) -> None:
112 if not self._verbose:
113 return
115 print(*args, file=sys.stderr)
117 def _post(self, endpoint: str, **input_data: Any) -> UptimeRobotResponse:
118 """
119 :raise UptimeRobotError: if the request returns an error status code
120 """
121 input_data.setdefault("format", "json")
122 input_data["api_key"] = self.api_key
124 self._log("POST", self.base / endpoint, input_data)
126 resp = (self.base / endpoint).post(json=input_data)
128 self._log("RESP", resp.__dict__)
130 if not resp.ok: 130 ↛ 131line 130 didn't jump to line 131, because the condition on line 130 was never true
131 raise UptimeRobotException(resp)
133 try:
134 output_data = resp.json() # type: UptimeRobotResponse
135 except json.JSONDecodeError as e:
136 raise UptimeRobotException(resp, str(e)) from e
138 if output_data.get("stat") == "fail":
139 raise UptimeRobotException(resp, output_data.get("error", output_data))
141 return output_data
143 def get_account_details(self) -> Optional[UptimeRobotAccount]:
144 resp = self._post("getAccountDetails")
146 return resp.get("account", {})
148 def get_monitors(self, search: str = "") -> list[UptimeRobotMonitor]:
149 data = {}
150 if search:
151 data["search"] = search
152 result = self._post("getMonitors", **data).get("monitors")
153 if result is None:
154 return []
156 return result
157 # return typing.cast(list[UptimeRobotMonitor], result)
159 def new_monitor(self, friendly_name: str, url: str, monitor_type: MonitorType = MonitorType.HTTP) -> Optional[int]:
160 data = {
161 "friendly_name": friendly_name,
162 "url": url,
163 "type": monitor_type.value,
164 }
165 response = self._post("newMonitor", **data)
167 return response.get("monitor", {}).get("id")
169 def edit_monitor(self, monitor_id: int, new_data: AnyDict) -> bool:
170 resp = self._post("editMonitor", id=monitor_id, **new_data)
172 return resp.get("monitor", {}).get("id") == monitor_id
174 def delete_monitor(self, monitor_id: int) -> bool:
175 resp = self._post("deleteMonitor", id=monitor_id)
177 return resp.get("monitor", {}).get("id") == monitor_id
179 def reset_monitor(self, monitor_id: int) -> bool:
180 resp = self._post("resetMonitor", id=monitor_id)
182 return resp.get("monitor", {}).get("id") == monitor_id
184 # def get_alert_contacts(self):
185 # return self._post("getAlertContacts")
186 #
187 # def new_alert_contact(self, contact_data):
188 # return self._post("newAlertContact", input_data=contact_data)
189 #
190 # def edit_alert_contact(self, contact_id, new_data):
191 # return self._post("editAlertContact", input_data={"contact_id": contact_id, "new_data": new_data})
192 #
193 # def delete_alert_contact(self, contact_id):
194 # return self._post("deleteAlertContact", input_data={"contact_id": contact_id})
195 #
196 # def get_m_windows(self):
197 # return self._post("getMWindows")
198 #
199 # def new_m_window(self, window_data):
200 # return self._post("newMWindow", input_data=window_data)
201 #
202 # def edit_m_window(self, window_id, new_data):
203 # return self._post("editMWindow", input_data={"window_id": window_id, "new_data": new_data})
204 #
205 # def delete_m_window(self, window_id):
206 # return self._post("deleteMWindow", input_data={"window_id": window_id})
207 #
208 # def get_psps(self):
209 # return self._post("getPSPs")
210 #
211 # def new_psp(self, psp_data):
212 # return self._post("newPSP", input_data=psp_data)
213 #
214 # def edit_psp(self, psp_id, new_data):
215 # return self._post("editPSP", input_data={"psp_id": psp_id, "new_data": new_data})
216 #
217 # def delete_psp(self, psp_id):
218 # return self._post("deletePSP", input_data={"psp_id": psp_id})
220 @staticmethod
221 def format_status(status_code: int) -> str:
222 return {
223 0: "paused",
224 1: "not checked yet",
225 2: "up",
226 8: "seems down",
227 9: "down",
228 }.get(status_code, f"Unknown status '{status_code}'!")
231uptime_robot = UptimeRobot()
232uptime_robot.set_verbosity()