Skip to content

Commit b6b0659

Browse files
committed
Refactor Uptime Kuma metrics retrieval
1 parent 8c47551 commit b6b0659

4 files changed

Lines changed: 94 additions & 64 deletions

File tree

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@ import aiohttp
1616

1717
from pythonkuma import UptimeKuma
1818

19-
URL = ""
20-
USERNAME = ""
21-
PASSWORD = ""
19+
URL = "https://uptime.exampe.com"
20+
API_KEY = "api_key"
2221

2322

2423
async def main():
2524

2625
async with aiohttp.ClientSession() as session:
27-
uptime_kuma = UptimeKuma(session, URL, USERNAME, PASSWORD)
28-
response = await uptime_kuma.async_get_monitors()
29-
print(response.data)
26+
uptime_kuma = UptimeKuma(session, URL, API_KEY)
27+
response = await uptime_kuma.metrics()
28+
print(response)
3029

3130

3231
asyncio.run(main())

pythonkuma/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Python API wrapper for Uptime Kuma."""
22

3-
from .exceptions import UptimeKumaAuthenticationException, UptimeKumaConnectionException, UptimeKumaException
4-
from .models import MonitorStatus, MonitorType, UptimeKumaApiResponse, UptimeKumaMonitor
3+
from .exceptions import (
4+
UptimeKumaAuthenticationException,
5+
UptimeKumaConnectionException,
6+
UptimeKumaException,
7+
)
8+
from .models import MonitorStatus, MonitorType, UptimeKumaMonitor
59
from .uptimekuma import UptimeKuma
610

711
__version__ = "0.0.0rc0"
@@ -10,7 +14,6 @@
1014
"MonitorStatus",
1115
"MonitorType",
1216
"UptimeKuma",
13-
"UptimeKumaApiResponse",
1417
"UptimeKumaAuthenticationException",
1518
"UptimeKumaConnectionException",
1619
"UptimeKumaException",

pythonkuma/models.py

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
"""Uptime Kuma models"""
1+
"""Uptime Kuma models."""
22

33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
66
from enum import IntEnum, StrEnum
7-
from typing import Any
7+
from typing import Self
88

99
from mashumaro import DataClassDictMixin
10-
from prometheus_client.parser import text_string_to_metric_families as parser
1110

1211

1312
class MonitorStatus(IntEnum):
@@ -44,6 +43,12 @@ class MonitorType(StrEnum):
4443
RADIUS = "radius"
4544
REDIS = "redis"
4645
TAILSCALE_PING = "tailscale-ping"
46+
UNKNOWN = "unknown"
47+
48+
@classmethod
49+
def _missing_(cls, _: object) -> Self:
50+
"""Handle new and unknown monitor types."""
51+
return cls.UNKNOWN
4752

4853

4954
@dataclass
@@ -57,46 +62,16 @@ class UptimeKumaMonitor(UptimeKumaBaseModel):
5762

5863
monitor_cert_days_remaining: int
5964
monitor_cert_is_valid: bool
60-
monitor_hostname: str | None = field(metadata={"deserialize": lambda v: None if v == "null" else v})
65+
monitor_hostname: str | None = field(
66+
metadata={"deserialize": lambda v: None if v == "null" else v}
67+
)
6168
monitor_name: str
62-
monitor_port: str | None = field(metadata={"deserialize": lambda v: None if v == "null" else v})
69+
monitor_port: str | None = field(
70+
metadata={"deserialize": lambda v: None if v == "null" else v}
71+
)
6372
monitor_response_time: int = 0
6473
monitor_status: MonitorStatus
6574
monitor_type: MonitorType = MonitorType.HTTP
66-
monitor_url: str | None = field(metadata={"deserialize": lambda v: None if v == "null" else v})
67-
68-
69-
@dataclass
70-
class UptimeKumaApiResponse(UptimeKumaBaseModel):
71-
"""API response model for Uptime Kuma."""
72-
73-
_method: str | None = None
74-
_api_path: str | None = None
75-
data: list[UptimeKumaMonitor] | None = None
76-
77-
@staticmethod
78-
def from_prometheus(data: dict[str, Any]) -> UptimeKumaApiResponse:
79-
"""Generate object from json."""
80-
obj: dict[str, Any] = {}
81-
monitors = []
82-
83-
for key, value in data.items():
84-
if hasattr(UptimeKumaApiResponse, key):
85-
obj[key] = value
86-
87-
parsed = parser(data["monitors"])
88-
for family in parsed:
89-
for sample in family.samples:
90-
if sample.name.startswith("monitor"):
91-
existed = next(
92-
(i for i, x in enumerate(monitors) if x["monitor_name"] == sample.labels["monitor_name"]),
93-
None,
94-
)
95-
if existed is None:
96-
temp = {**sample.labels, sample.name: sample.value}
97-
monitors.append(temp)
98-
else:
99-
monitors[existed][sample.name] = sample.value
100-
obj["data"] = [UptimeKumaMonitor.from_dict(monitor) for monitor in monitors]
101-
102-
return UptimeKumaApiResponse(**obj)
75+
monitor_url: str | None = field(
76+
metadata={"deserialize": lambda v: None if v == "null" else v}
77+
)

pythonkuma/uptimekuma.py

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,76 @@
11
"""Uptime Kuma client."""
22

3-
from aiohttp import ClientSession
3+
from http import HTTPStatus
4+
from typing import Any
5+
6+
from aiohttp import (
7+
BasicAuth,
8+
ClientError,
9+
ClientResponseError,
10+
ClientSession,
11+
ClientTimeout,
12+
)
13+
from prometheus_client.parser import text_string_to_metric_families
414
from yarl import URL
515

6-
from .decorator import api_request
7-
from .models import UptimeKumaApiResponse
16+
from .exceptions import UptimeKumaAuthenticationException, UptimeKumaConnectionException
17+
from .models import UptimeKumaMonitor
818

919

1020
class UptimeKuma:
11-
"""This class is used to get information from Uptime Kuma."""
21+
"""Uptime Kuma client."""
1222

13-
def __init__(self, session: ClientSession, base_url: URL | str, username: str, password: str) -> None:
14-
"""Initialize"""
15-
self.monitors = []
23+
def __init__(
24+
self,
25+
session: ClientSession,
26+
base_url: URL | str,
27+
api_key: str | None = None,
28+
timeout: float | None = None,
29+
) -> None:
30+
"""Initialize the Uptime Kuma client."""
1631
self._base_url = base_url if isinstance(base_url, URL) else URL(base_url)
17-
self._username = username
18-
self._password = password
19-
self._session: ClientSession = session
2032

21-
@api_request("metrics")
22-
async def async_get_monitors(self, **kwargs) -> UptimeKumaApiResponse:
23-
"""Get monitors from API."""
33+
self._auth = BasicAuth("", api_key) if api_key else None
34+
35+
self._timeout = ClientTimeout(total=timeout or 10)
36+
self._session = session
37+
38+
async def metrics(self) -> list[UptimeKumaMonitor]:
39+
"""Retrieve metrics from Uptime Kuma."""
40+
url = self._base_url / "metrics"
41+
42+
try:
43+
request = await self._session.get(
44+
url, auth=self._auth, timeout=self._timeout
45+
)
46+
request.raise_for_status()
47+
except ClientResponseError as e:
48+
if e.status is HTTPStatus.UNAUTHORIZED:
49+
msg = "Authentication failed for %s"
50+
raise UptimeKumaAuthenticationException(msg, str(url)) from e
51+
msg = "Request for %s failed with status code %s"
52+
raise UptimeKumaConnectionException(msg, str(url), e.status) from e
53+
except TimeoutError as e:
54+
msg = "Request timeout for %s"
55+
raise UptimeKumaConnectionException(msg, str(url)) from e
56+
except ClientError as e:
57+
raise UptimeKumaConnectionException from e
58+
else:
59+
parsed = text_string_to_metric_families(await request.text())
60+
61+
monitors: dict[str, dict[str, Any]] = {}
62+
for metric in parsed:
63+
if not metric.name.startswith("monitor"):
64+
continue
65+
for sample in metric.samples:
66+
if not (monitor_name := sample.labels.get("monitor_name")):
67+
continue
68+
69+
monitors.setdefault(monitor_name, sample.labels).update(
70+
{sample.name: sample.value}
71+
)
72+
73+
return {
74+
key: UptimeKumaMonitor.from_dict(value)
75+
for key, value in monitors.items()
76+
}

0 commit comments

Comments
 (0)