diff --git a/pyproject.toml b/pyproject.toml index 557f27a..e37bbfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ include = [ "/pythonkuma", ] +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.14", "3.13", "3.12"] + [tool.hatch.envs.default] dependencies = [ "ruff==0.15.0", @@ -45,6 +48,10 @@ dependencies = [ "mashumaro==3.20", "mkdocs-material==9.7.1", "mkdocstrings[python]==1.0.3", + "pytest-asyncio==1.3.0", + "pytest==9.0.2", + "pytest-cov==7.0.0", + "syrupy==5.1.0", ] [tool.hatch.envs.hatch-static-analysis] @@ -63,6 +70,8 @@ pythonpath = ["pythonkuma"] [tool.hatch.envs.hatch-test] extra-dependencies = [ "pytest-cov==7.0.0", + "pytest-asyncio==1.3.0", + "syrupy==5.1.0", ] [tool.hatch.envs.default.scripts] @@ -83,6 +92,9 @@ indent-style = "space" select = ["ALL"] ignore = ["TRY003", "COM812", "N818", "C901"] +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101", "TC002", "TC003"] + [lint.per-file-ignores] "**/scripts/*" = [ "INP001", diff --git a/pythonkuma/uptimekuma.py b/pythonkuma/uptimekuma.py index f15b2e6..0aa9e9d 100644 --- a/pythonkuma/uptimekuma.py +++ b/pythonkuma/uptimekuma.py @@ -95,7 +95,7 @@ async def metrics(self) -> dict[str | int, UptimeKumaMonitor]: raise UptimeKumaConnectionException from e try: - metrics = set(text_string_to_metric_families(await request.text())) + metrics = list(text_string_to_metric_families(await request.text())) except ValueError as e: raise UptimeKumaParseException from e diff --git a/tests/__snapshots__/test_metrics.ambr b/tests/__snapshots__/test_metrics.ambr new file mode 100644 index 0000000..dd1531e --- /dev/null +++ b/tests/__snapshots__/test_metrics.ambr @@ -0,0 +1,77 @@ +# serializer version: 1 +# name: test_metrics + dict({ + 1: dict({ + 'monitor_cert_days_remaining': 80, + 'monitor_cert_is_valid': True, + 'monitor_hostname': None, + 'monitor_id': 1, + 'monitor_name': 'Home Assistant', + 'monitor_port': None, + 'monitor_response_time': 85, + 'monitor_response_time_seconds_1d': 0.10396079958463136, + 'monitor_response_time_seconds_30d': 0.10284582478851578, + 'monitor_response_time_seconds_365d': 0.10957428212662089, + 'monitor_status': 1, + 'monitor_type': 'http', + 'monitor_uptime_ratio_1d': 1.0, + 'monitor_uptime_ratio_30d': 0.999247554552295, + 'monitor_uptime_ratio_365d': 0.9944324016912971, + 'monitor_url': 'https://home.example.com:8123', + }), + 2: dict({ + 'monitor_cert_days_remaining': 80, + 'monitor_cert_is_valid': True, + 'monitor_hostname': None, + 'monitor_id': 2, + 'monitor_name': 'FritzBox', + 'monitor_port': None, + 'monitor_response_time': 2725, + 'monitor_response_time_seconds_1d': 2.339521038961039, + 'monitor_response_time_seconds_30d': 2.3636583723629956, + 'monitor_response_time_seconds_365d': 2.3783335690116663, + 'monitor_status': 1, + 'monitor_type': 'http', + 'monitor_uptime_ratio_1d': 0.9992213859330392, + 'monitor_uptime_ratio_30d': 0.9998319004850872, + 'monitor_uptime_ratio_365d': 0.9947252084798553, + 'monitor_url': 'https://home.example.com', + }), + 3: dict({ + 'monitor_cert_days_remaining': 46, + 'monitor_cert_is_valid': True, + 'monitor_hostname': None, + 'monitor_id': 3, + 'monitor_name': 'Jellyfin', + 'monitor_port': None, + 'monitor_response_time': 85, + 'monitor_response_time_seconds_1d': 0.10102960288808664, + 'monitor_response_time_seconds_30d': 0.09908259629443207, + 'monitor_response_time_seconds_365d': 0.10429958790526786, + 'monitor_status': 1, + 'monitor_type': 'keyword', + 'monitor_uptime_ratio_1d': 1.0, + 'monitor_uptime_ratio_30d': 0.9993293252532994, + 'monitor_uptime_ratio_365d': 0.9941631600380073, + 'monitor_url': 'https://home.example.com:8920/health', + }), + 8: dict({ + 'monitor_cert_days_remaining': 81, + 'monitor_cert_is_valid': True, + 'monitor_hostname': None, + 'monitor_id': 8, + 'monitor_name': 'Nextcloud', + 'monitor_port': None, + 'monitor_response_time': 150, + 'monitor_response_time_seconds_1d': 0.16155477855477854, + 'monitor_response_time_seconds_30d': 0.3391915450984161, + 'monitor_response_time_seconds_365d': 0.34379255863250385, + 'monitor_status': 1, + 'monitor_type': 'json-query', + 'monitor_uptime_ratio_1d': 1.0, + 'monitor_uptime_ratio_30d': 0.9991115593334294, + 'monitor_uptime_ratio_365d': 0.9994703389830508, + 'monitor_url': 'https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json', + }), + }) +# --- diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d3aa1dd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +"""Fixtures for pythonkuma.""" + +from collections.abc import Generator +from functools import lru_cache +import pathlib +from unittest.mock import AsyncMock + +from aiohttp import ClientResponse +import pytest + + +@lru_cache +def load_fixture(filename: str) -> str: + """Load a fixture.""" + return ( + pathlib.Path(__file__) + .parent.joinpath("fixtures", filename) + .read_text(encoding="utf-8") + ) + + +@pytest.fixture +def mock_session() -> Generator[AsyncMock]: + """Mock aiohttp ClientSession.""" + mock_session = AsyncMock() + mock_response = AsyncMock(spec=ClientResponse, status=200) + mock_response.text.return_value = load_fixture("metrics.txt") + + mock_session.get.return_value = mock_response + + return mock_session diff --git a/tests/fixtures/metrics.txt b/tests/fixtures/metrics.txt new file mode 100644 index 0000000..366d058 --- /dev/null +++ b/tests/fixtures/metrics.txt @@ -0,0 +1,61 @@ +# HELP monitor_cert_days_remaining The number of days remaining until the certificate expires +# TYPE monitor_cert_days_remaining gauge +monitor_cert_days_remaining{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null"} 80 +monitor_cert_days_remaining{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null"} 80 +monitor_cert_days_remaining{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null"} 46 +monitor_cert_days_remaining{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null"} 81 + +# HELP monitor_cert_is_valid Is the certificate still valid? (1 = Yes, 0= No) +# TYPE monitor_cert_is_valid gauge +monitor_cert_is_valid{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null"} 1 +monitor_cert_is_valid{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null"} 1 +monitor_cert_is_valid{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null"} 1 +monitor_cert_is_valid{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null"} 1 + +# HELP monitor_uptime_ratio Uptime ratio calculated over sliding window specified by the 'window' label. (0.0 - 1.0) +# TYPE monitor_uptime_ratio gauge +monitor_uptime_ratio{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="1d"} 1 +monitor_uptime_ratio{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="30d"} 0.999247554552295 +monitor_uptime_ratio{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="365d"} 0.9944324016912971 +monitor_uptime_ratio{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="1d"} 0.9992213859330392 +monitor_uptime_ratio{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="30d"} 0.9998319004850872 +monitor_uptime_ratio{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="365d"} 0.9947252084798553 +monitor_uptime_ratio{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="1d"} 1 +monitor_uptime_ratio{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="30d"} 0.9993293252532994 +monitor_uptime_ratio{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="365d"} 0.9941631600380073 +monitor_uptime_ratio{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="1d"} 1 +monitor_uptime_ratio{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="30d"} 0.9991115593334294 +monitor_uptime_ratio{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="365d"} 0.9994703389830508 + +# HELP monitor_response_time_seconds Average response time in seconds calculated over sliding window specified by the 'window' label +# TYPE monitor_response_time_seconds gauge +monitor_response_time_seconds{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="1d"} 0.10396079958463136 +monitor_response_time_seconds{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="30d"} 0.10284582478851578 +monitor_response_time_seconds{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null",window="365d"} 0.10957428212662089 +monitor_response_time_seconds{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="1d"} 2.339521038961039 +monitor_response_time_seconds{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="30d"} 2.3636583723629956 +monitor_response_time_seconds{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null",window="365d"} 2.3783335690116663 +monitor_response_time_seconds{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="1d"} 0.10102960288808664 +monitor_response_time_seconds{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="30d"} 0.09908259629443207 +monitor_response_time_seconds{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null",window="365d"} 0.10429958790526786 +monitor_response_time_seconds{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="1d"} 0.16155477855477854 +monitor_response_time_seconds{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="30d"} 0.3391915450984161 +monitor_response_time_seconds{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null",window="365d"} 0.34379255863250385 + +# HELP monitor_response_time Monitor Response Time (ms) +# TYPE monitor_response_time gauge +monitor_response_time{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null"} 85 +monitor_response_time{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null"} 2725 +monitor_response_time{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null"} 85 +monitor_response_time{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null"} 150 + +# HELP monitor_status Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE) +# TYPE monitor_status gauge +monitor_status{monitor_id="1",monitor_name="Home Assistant",monitor_type="http",monitor_url="https://home.example.com:8123",monitor_hostname="null",monitor_port="null"} 1 +monitor_status{monitor_id="2",monitor_name="FritzBox",monitor_type="http",monitor_url="https://home.example.com",monitor_hostname="null",monitor_port="null"} 1 +monitor_status{Test="",Zuhause="",monitor_id="3",monitor_name="Jellyfin",monitor_type="keyword",monitor_url="https://home.example.com:8920/health",monitor_hostname="null",monitor_port="null"} 1 +monitor_status{monitor_id="8",monitor_name="Nextcloud",monitor_type="json-query",monitor_url="https://cloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json",monitor_hostname="null",monitor_port="null"} 1 + +# HELP app_version The service version by package.json +# TYPE app_version gauge +app_version{version="2.1.0",major="2",minor="1",patch="0"} 1 diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..9efcd33 --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,16 @@ +"""Tests for pythonkuma.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from pythonkuma import UptimeKuma + + +async def test_metrics(mock_session: AsyncMock, snapshot: SnapshotAssertion) -> None: + """Test metrics.""" + uptime_kuma = UptimeKuma(mock_session, "http://uptime.example.com", "test-apikey") + + response = await uptime_kuma.metrics() + + assert {k: v.to_dict() for k, v in response.items()} == snapshot