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

1import enum 

2import json 

3import sys 

4import typing 

5from typing import Any, Optional 

6 

7import edwh 

8import requests 

9from edwh import check_env 

10from typing_extensions import NotRequired, Required 

11from yayarl import URL 

12 

13AnyDict: typing.TypeAlias = dict[str, Any] 

14 

15 

16class UptimeRobotException(Exception): 

17 status_code: int 

18 message: str 

19 response: requests.Response 

20 extra: Optional[Any] 

21 

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 

27 

28 

29class UptimeRobotErrorResponse(typing.TypedDict): 

30 type: str 

31 message: str 

32 

33 parameter_name: NotRequired[str] 

34 passed_value: NotRequired[str] 

35 

36 

37class UptimeRobotPagination(typing.TypedDict): 

38 offset: int 

39 limit: int 

40 total: int 

41 

42 

43class UptimeRobotAccount(typing.TypedDict, total=False): 

44 email: str 

45 user_id: int 

46 firstname: str 

47 # ... 

48 

49 

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] 

58 

59 

60class UptimeRobotMonitor(typing.TypedDict, total=False): 

61 id: Required[int] 

62 

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] 

78 

79 

80class MonitorType(enum.Enum): 

81 HTTP = 1 

82 KEYWORD = 2 

83 PING = 3 

84 PORT = 4 

85 HEARTBEAT = 5 

86 

87 

88class UptimeRobot: 

89 base = URL("https://api.uptimerobot.com/v2/") 

90 

91 _api_key: str = "" # cached version from .env 

92 _verbose: bool = False 

93 

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 ) 

102 

103 return self._api_key 

104 

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" 

108 

109 self._verbose = verbose 

110 

111 def _log(self, *args: Any) -> None: 

112 if not self._verbose: 

113 return 

114 

115 print(*args, file=sys.stderr) 

116 

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 

123 

124 self._log("POST", self.base / endpoint, input_data) 

125 

126 resp = (self.base / endpoint).post(json=input_data) 

127 

128 self._log("RESP", resp.__dict__) 

129 

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) 

132 

133 try: 

134 output_data = resp.json() # type: UptimeRobotResponse 

135 except json.JSONDecodeError as e: 

136 raise UptimeRobotException(resp, str(e)) from e 

137 

138 if output_data.get("stat") == "fail": 

139 raise UptimeRobotException(resp, output_data.get("error", output_data)) 

140 

141 return output_data 

142 

143 def get_account_details(self) -> Optional[UptimeRobotAccount]: 

144 resp = self._post("getAccountDetails") 

145 

146 return resp.get("account", {}) 

147 

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 [] 

155 

156 return result 

157 # return typing.cast(list[UptimeRobotMonitor], result) 

158 

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) 

166 

167 return response.get("monitor", {}).get("id") 

168 

169 def edit_monitor(self, monitor_id: int, new_data: AnyDict) -> bool: 

170 resp = self._post("editMonitor", id=monitor_id, **new_data) 

171 

172 return resp.get("monitor", {}).get("id") == monitor_id 

173 

174 def delete_monitor(self, monitor_id: int) -> bool: 

175 resp = self._post("deleteMonitor", id=monitor_id) 

176 

177 return resp.get("monitor", {}).get("id") == monitor_id 

178 

179 def reset_monitor(self, monitor_id: int) -> bool: 

180 resp = self._post("resetMonitor", id=monitor_id) 

181 

182 return resp.get("monitor", {}).get("id") == monitor_id 

183 

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}) 

219 

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}'!") 

229 

230 

231uptime_robot = UptimeRobot() 

232uptime_robot.set_verbosity()