From 7ef61f8ce178fb96f8dc58a4dd241decba3ce775 Mon Sep 17 00:00:00 2001 From: lucasheld Date: Tue, 2 May 2023 17:57:32 +0200 Subject: [PATCH 1/7] feat: drop python 3.6 support BREAKING CHANGE: Python 3.7+ required --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c0347dc..5a0d95a 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( author_email="lucasheld@hotmail.de", license=info["__license__"], packages=["uptime_kuma_api"], - python_requires=">=3.6, <4", + python_requires=">=3.7, <4", install_requires=[ "python-socketio[client]>=5.0.0", "packaging" @@ -43,7 +43,6 @@ setup( "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From b87eed2597914aca4e00fe0daf47983e5bc3fe47 Mon Sep 17 00:00:00 2001 From: lucasheld Date: Tue, 2 May 2023 20:34:26 +0200 Subject: [PATCH 2/7] fix: adjust monitor `status` type to allow all used values BREAKING CHANGE: monitor `status` type changed from `bool` to `MonitorStatus` --- uptime_kuma_api/__init__.py | 1 + uptime_kuma_api/api.py | 42 +++++++++++++++++++++++-------- uptime_kuma_api/monitor_status.py | 17 +++++++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 uptime_kuma_api/monitor_status.py diff --git a/uptime_kuma_api/__init__.py b/uptime_kuma_api/__init__.py index b069d5b..a62c17b 100644 --- a/uptime_kuma_api/__init__.py +++ b/uptime_kuma_api/__init__.py @@ -1,5 +1,6 @@ from .__version__ import __title__, __version__, __author__, __copyright__ from .auth_method import AuthMethod +from .monitor_status import MonitorStatus from .monitor_type import MonitorType from .notification_providers import NotificationType, notification_provider_options, notification_provider_conditions from .proxy_protocol import ProxyProtocol diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index 36fe904..4355de4 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -13,11 +13,12 @@ import requests import socketio from packaging.version import parse as parse_version -from . import (AuthMethod, +from . import (AuthMethod, DockerType, Event, IncidentStyle, MaintenanceStrategy, + MonitorStatus, MonitorType, NotificationType, ProxyProtocol, @@ -33,6 +34,7 @@ from .docstrings import (append_docstring, proxy_docstring, tag_docstring) + def int_to_bool(data, keys) -> None: if isinstance(data, list): for d in data: @@ -43,6 +45,20 @@ def int_to_bool(data, keys) -> None: data[key] = True if data[key] == 1 else False +def parse_value(data, func) -> None: + if isinstance(data, list): + for d in data: + parse_value(d, func) + else: + func(data) + + +def parse_monitor_status(data) -> None: + def parse(x): + x["status"] = MonitorStatus(x["status"]) + parse_value(data, parse) + + def gen_secret(length: int) -> str: chars = string.ascii_uppercase + string.ascii_lowercase + string.digits return ''.join(random.choice(chars) for _ in range(length)) @@ -1123,7 +1139,7 @@ class UptimeKumaApi(object): 'monitor_id': 1, 'msg': '200 - OK', 'ping': 201, - 'status': True, + 'status': , 'time': '2022-12-15 12:38:42.661' }, { @@ -1134,14 +1150,15 @@ class UptimeKumaApi(object): 'monitor_id': 1, 'msg': '200 - OK', 'ping': 193, - 'status': True, + 'status': , 'time': '2022-12-15 12:39:42.878' }, ... ] """ r = self._call('getMonitorBeats', (id_, hours))["data"] - int_to_bool(r, ["important", "status"]) + int_to_bool(r, ["important"]) + parse_monitor_status(r) return r def get_game_list(self) -> list[dict]: @@ -1945,7 +1962,7 @@ class UptimeKumaApi(object): 'monitor_id': 1, 'msg': 'connect ECONNREFUSED 127.0.0.1:80', 'ping': None, - 'status': False, + 'status': , 'time': '2022-12-15 16:51:41.782' }, { @@ -1956,7 +1973,7 @@ class UptimeKumaApi(object): 'monitor_id': 1, 'msg': 'connect ECONNREFUSED 127.0.0.1:80', 'ping': None, - 'status': False, + 'status': , 'time': '2022-12-15 16:52:41.799' }, ... @@ -1967,7 +1984,8 @@ class UptimeKumaApi(object): """ r = self._get_event_data(Event.HEARTBEAT_LIST) for i in r: - int_to_bool(i["data"], ["important", "status"]) + int_to_bool(i["data"], ["important"]) + parse_monitor_status(i["data"]) return r def get_important_heartbeats(self) -> list[dict]: @@ -1990,7 +2008,7 @@ class UptimeKumaApi(object): 'monitorID': 1, 'msg': 'connect ECONNREFUSED 127.0.0.1:80', 'ping': None, - 'status': False, + 'status': , 'time': '2022-12-15 16:51:41.782' } ], @@ -2000,7 +2018,8 @@ class UptimeKumaApi(object): """ r = self._get_event_data(Event.IMPORTANT_HEARTBEAT_LIST) for i in r: - int_to_bool(i["data"], ["important", "status"]) + int_to_bool(i["data"], ["important"]) + parse_monitor_status(i["data"]) return r def get_heartbeat(self) -> list[dict]: @@ -2019,13 +2038,14 @@ class UptimeKumaApi(object): 'important': False, 'monitorID': 1, 'msg': 'connect ECONNREFUSED 127.0.0.1:80', - 'status': False, + 'status': , 'time': '2022-12-15 17:17:42.099' } ] """ r = self._get_event_data(Event.HEARTBEAT) - int_to_bool(r, ["important", "status"]) + int_to_bool(r, ["important"]) + parse_monitor_status(r) return r # avg ping diff --git a/uptime_kuma_api/monitor_status.py b/uptime_kuma_api/monitor_status.py new file mode 100644 index 0000000..abf3d23 --- /dev/null +++ b/uptime_kuma_api/monitor_status.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class MonitorStatus(int, Enum): + """Enumerate monitor statuses.""" + + DOWN = 0 + """DOWN""" + + UP = 1 + """UP""" + + PENDING = 2 + """PENDING""" + + MAINTENANCE = 3 + """MAINTENANCE""" From a9f2b6d894503fdcbb402353fb5aa76ec5eeb0df Mon Sep 17 00:00:00 2001 From: lucasheld Date: Tue, 2 May 2023 20:36:49 +0200 Subject: [PATCH 3/7] feat: implement `get_monitor_status` helper method --- uptime_kuma_api/api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index 4355de4..e35d739 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -3560,3 +3560,13 @@ class UptimeKumaApi(object): """ with self.wait_for_event(Event.API_KEY_LIST): return self._call('deleteAPIKey', id_) + + # helper methods + + def get_monitor_status(self, monitor_id: int) -> MonitorStatus: + heartbeats = self.get_heartbeats() + for heartbeat in heartbeats: + if int(heartbeat["id"]) == monitor_id: + status = heartbeat["data"][-1]["status"] + return MonitorStatus(status) + raise UptimeKumaException("monitor does not exist") From a576ed9f3a96c6e4d240a352c210c4c78ac48c7f Mon Sep 17 00:00:00 2001 From: Lucas Held Date: Tue, 2 May 2023 20:43:52 +0200 Subject: [PATCH 4/7] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44328ff..4836bf6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Documentation --- The API Reference is available on [Read the Docs](https://uptime-kuma-api.readthedocs.io). -Examples +Example --- Once you have installed the python package, you can use it to communicate with an Uptime Kuma instance. @@ -49,3 +49,17 @@ At the end, the connection to the API must be disconnected so that the program d ```python >>> api.disconnect() ``` + +With a context manager, the disconnect method is called automatically: + +```python +from uptime_kuma_api import UptimeKumaApi + +with UptimeKumaApi('INSERT_URL') as api: + api.login('INSERT_USERNAME', 'INSERT_PASSWORD') + api.add_monitor( + type=MonitorType.HTTP, + name="Google", + url="https://google.com" + ) +``` From d2cfc6652d1296ad545b2008a93c192094d23037 Mon Sep 17 00:00:00 2001 From: lucasheld Date: Sat, 6 May 2023 13:36:26 +0200 Subject: [PATCH 5/7] refactor: use square brackets for tuple type hint --- uptime_kuma_api/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index 36fe904..cf86537 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -13,7 +13,7 @@ import requests import socketio from packaging.version import parse as parse_version -from . import (AuthMethod, +from . import (AuthMethod, DockerType, Event, IncidentStyle, @@ -33,6 +33,7 @@ from .docstrings import (append_docstring, proxy_docstring, tag_docstring) + def int_to_bool(data, keys) -> None: if isinstance(data, list): for d in data: @@ -147,7 +148,7 @@ def _build_status_page_data( icon: str = "/icon.svg", publicGroupList: list = None -) -> tuple(str, dict, str, list): +) -> tuple[str, dict, str, list]: if theme not in ["light", "dark"]: raise ValueError if not domainNameList: From 8e841cd32449d46ac724a3a55140e1097d4e95ed Mon Sep 17 00:00:00 2001 From: lucasheld Date: Fri, 19 May 2023 13:49:36 +0200 Subject: [PATCH 6/7] feat: add support for uptime kuma 1.21.3 BREAKING CHANGE: maintenance parameter `timezone` renamed to `timezoneOption` --- README.md | 7 ++++++- run_tests.sh | 2 +- tests/test_maintenance.py | 4 ++-- uptime_kuma_api/api.py | 18 +++++++++--------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4836bf6..a553684 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,12 @@ This package was developed to configure Uptime Kuma with Ansible. The Ansible co Python version 3.7+ is required. -Supported Uptime Kuma versions: 1.17.0 - 1.21.2 +Supported Uptime Kuma versions: + +| uptime-kuma-api | Uptime Kuma | +|-----------------|-----------------| +| 1.0.0 | 1.21.3 | +| 0.1.0 - 0.13.0 | 1.17.0 - 1.21.2 | Installation --- diff --git a/run_tests.sh b/run_tests.sh index af7ff36..ff942c7 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -5,7 +5,7 @@ if [ $version ] then versions=("$version") else - versions=(1.21.2 1.21.1 1.20.2 1.19.6 1.18.5 1.17.1) + versions=(1.21.3) fi for version in ${versions[*]} diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index e5cf40e..cb443e3 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -30,7 +30,7 @@ class TestMaintenance(UptimeKumaTestCase): if parse_version(self.api.version) >= parse_version("1.21.2"): expected_maintenance.update({ - "timezone": "Europe/Berlin" + "timezoneOption": "Europe/Berlin" }) # add maintenance @@ -242,7 +242,7 @@ class TestMaintenance(UptimeKumaTestCase): "daysOfMonth": [], "cron": "50 5 * * *", "durationMinutes": 120, - "timezone": "Europe/Berlin" + "timezoneOption": "Europe/Berlin" } self.do_test_maintenance_strategy(expected_maintenance) diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index cf86537..d8c0bf4 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -848,7 +848,7 @@ class UptimeKumaApi(object): timeRange: list = None, cron: str = "30 3 * * *", durationMinutes: int = 60, - timezone: str = None + timezoneOption: str = None ) -> dict: if not dateRange: dateRange = [ @@ -883,7 +883,7 @@ class UptimeKumaApi(object): data.update({ "cron": cron, "durationMinutes": durationMinutes, - "timezone": timezone, + "timezoneOption": timezoneOption, }) return data @@ -2915,7 +2915,7 @@ class UptimeKumaApi(object): ], "cron": "", "durationMinutes": null, - "timezone": "Europe/Berlin", + "timezoneOption": "Europe/Berlin", "timezoneOffset": "+02:00", "status": "ended" } @@ -2967,7 +2967,7 @@ class UptimeKumaApi(object): "cron": null, "duration": null, "durationMinutes": 0, - "timezone": "Europe/Berlin", + "timezoneOption": "Europe/Berlin", "timezoneOffset": "+02:00", "status": "ended" } @@ -3016,7 +3016,7 @@ class UptimeKumaApi(object): ... ], ... weekdays=[], ... daysOfMonth=[], - ... timezone="Europe/Berlin" + ... timezoneOption="Europe/Berlin" ... ) { "msg": "Added Successfully.", @@ -3049,7 +3049,7 @@ class UptimeKumaApi(object): ... ], ... weekdays=[], ... daysOfMonth=[], - ... timezone="Europe/Berlin" + ... timezoneOption="Europe/Berlin" ... ) { "msg": "Added Successfully.", @@ -3087,7 +3087,7 @@ class UptimeKumaApi(object): ... 0 ... ], ... daysOfMonth=[], - ... timezone="Europe/Berlin" + ... timezoneOption="Europe/Berlin" ... ) { "msg": "Added Successfully.", @@ -3126,7 +3126,7 @@ class UptimeKumaApi(object): ... 30, ... "lastDay1" ... ], - ... timezone="Europe/Berlin" + ... timezoneOption="Europe/Berlin" ... ) { "msg": "Added Successfully.", @@ -3149,7 +3149,7 @@ class UptimeKumaApi(object): ... daysOfMonth=[], ... cron="50 5 * * *", ... durationMinutes=120, - ... timezone="Europe/Berlin" + ... timezoneOption="Europe/Berlin" ... ) { "msg": "Added Successfully.", From 9728cfdb3445c030a1a50722639dd056a2b2a802 Mon Sep 17 00:00:00 2001 From: Lucas Held Date: Fri, 19 May 2023 13:50:39 +0200 Subject: [PATCH 7/7] feat: implement timeouts for all methods (#34) BREAKING CHANGE: Removed the `wait_timeout` parameter. Use the new `timeout` parameter instead. The `timeout` parameter specifies how many seconds the client should wait for the connection, an expected event or a server response. --- docs/api.rst | 2 ++ uptime_kuma_api/__init__.py | 2 +- uptime_kuma_api/api.py | 34 +++++++++++++++++++--------------- uptime_kuma_api/exceptions.py | 7 ++++++- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2ab209c..0ebf9eb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -39,3 +39,5 @@ Exceptions ---------- .. autoexception:: UptimeKumaException + +.. autoexception:: Timeout diff --git a/uptime_kuma_api/__init__.py b/uptime_kuma_api/__init__.py index b069d5b..e050022 100644 --- a/uptime_kuma_api/__init__.py +++ b/uptime_kuma_api/__init__.py @@ -6,6 +6,6 @@ from .proxy_protocol import ProxyProtocol from .incident_style import IncidentStyle from .docker_type import DockerType from .maintenance_strategy import MaintenanceStrategy -from .exceptions import UptimeKumaException +from .exceptions import UptimeKumaException, Timeout from .event import Event from .api import UptimeKumaApi diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index d8c0bf4..07aade0 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -21,6 +21,7 @@ from . import (AuthMethod, MonitorType, NotificationType, ProxyProtocol, + Timeout, UptimeKumaException, notification_provider_conditions, notification_provider_options) @@ -382,7 +383,8 @@ class UptimeKumaApi(object): ) :param str url: The url to the Uptime Kuma instance. For example ``http://127.0.0.1:3001`` - :param float wait_timeout: How many seconds the client should wait for the connection., defaults to 1 + :param float timeout: How many seconds the client should wait for the connection, an expected event or a server + response. Default is ``10``. :param dict headers: Headers that are passed to the socketio connection, defaults to None :param bool ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -396,13 +398,13 @@ class UptimeKumaApi(object): def __init__( self, url: str, - wait_timeout: float = 1, + timeout: float = 10, headers: dict = None, ssl_verify: bool = True, wait_events: float = 0.2 ) -> None: self.url = url - self.wait_timeout = wait_timeout + self.timeout = timeout self.headers = headers self.wait_events = wait_events self.sio = socketio.Client(ssl_verify=ssl_verify) @@ -454,26 +456,25 @@ class UptimeKumaApi(object): @contextmanager def wait_for_event(self, event: Event) -> None: - # 200 * 0.05 seconds = 10 seconds - retries = 200 - sleep = 0.05 + # waits for the first event of the given type to arrive try: yield except: raise else: - counter = 0 + timestamp = time.time() while self._event_data[event] is None: - time.sleep(sleep) - counter += 1 - if counter >= retries: - print(f"wait_for_event {event} timeout") - break + if time.time() - timestamp > self.timeout: + raise Timeout(f"Timed out while waiting for event {event}") + time.sleep(0.01) def _get_event_data(self, event) -> Any: monitor_events = [Event.AVG_PING, Event.UPTIME, Event.HEARTBEAT_LIST, Event.IMPORTANT_HEARTBEAT_LIST, Event.CERT_INFO, Event.HEARTBEAT] + timestamp = time.time() while self._event_data[event] is None: + if time.time() - timestamp > self.timeout: + raise Timeout(f"Timed out while waiting for event {event}") # do not wait for events that are not sent if self._event_data[Event.MONITOR_LIST] == {} and event in monitor_events: return [] @@ -482,7 +483,7 @@ class UptimeKumaApi(object): return deepcopy(self._event_data[event]) def _call(self, event, data=None) -> Any: - r = self.sio.call(event, data) + r = self.sio.call(event, data, timeout=self.timeout) if isinstance(r, dict) and "ok" in r: if not r["ok"]: raise UptimeKumaException(r.get("msg")) @@ -587,7 +588,7 @@ class UptimeKumaApi(object): """ url = self.url.rstrip("/") try: - self.sio.connect(f'{url}/socket.io/', wait_timeout=self.wait_timeout, headers=self.headers) + self.sio.connect(f'{url}/socket.io/', wait_timeout=self.timeout, headers=self.headers) except: raise UptimeKumaException("unable to connect") @@ -1727,7 +1728,10 @@ class UptimeKumaApi(object): } """ r1 = self._call('getStatusPage', slug) - r2 = requests.get(f"{self.url}/api/status-page/{slug}").json() + try: + r2 = requests.get(f"{self.url}/api/status-page/{slug}", timeout=self.timeout).json() + except requests.exceptions.Timeout as e: + raise Timeout(e) config = r1["config"] config.update(r2["config"]) diff --git a/uptime_kuma_api/exceptions.py b/uptime_kuma_api/exceptions.py index e9bb9fc..116db50 100644 --- a/uptime_kuma_api/exceptions.py +++ b/uptime_kuma_api/exceptions.py @@ -2,4 +2,9 @@ class UptimeKumaException(Exception): """ There was an exception that occurred while communicating with Uptime Kuma. """ - pass + + +class Timeout(UptimeKumaException): + """ + A timeout has occurred while communicating with Uptime Kuma. + """