PyObservability 2.0.0__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyobservability/kuma.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import time
3
- from collections import defaultdict
4
- from typing import Any, Dict, List
3
+ from collections.abc import Generator
4
+ from typing import Any, Dict
5
5
  from urllib.parse import urlparse
6
6
 
7
7
  import socketio
@@ -81,18 +81,16 @@ class UptimeKumaClient:
81
81
  return self.monitors
82
82
 
83
83
 
84
- async def extract_monitors(payload: Dict[int, Dict[str, Any]]) -> List[Dict[str, Any]]:
85
- """Convert raw API payload into a list of dicts with name, url, tag_names, host.
84
+ def extract_monitors(payload: Dict[int, Dict[str, Any]]) -> Generator[Dict[str, Any]]:
85
+ """Convert raw API payload into a list of dicts with name, url, tags, host.
86
86
 
87
87
  Args:
88
88
  payload: Raw payload from Uptime Kuma server.
89
89
 
90
- Returns:
91
- List[Dict[str, Any]]:
92
- List of monitors with relevant fields.
90
+ Yields:
91
+ Dict[str, Any]:
92
+ Monitors with relevant fields.
93
93
  """
94
- monitors = []
95
-
96
94
  grouped = {}
97
95
  for monitor in payload.values():
98
96
  if children_ids := monitor.get("childrenIDs"):
@@ -104,23 +102,10 @@ async def extract_monitors(payload: Dict[int, Dict[str, Any]]) -> List[Dict[str,
104
102
  host = urlparse(url).hostname if url else None
105
103
  if not host:
106
104
  continue
107
- monitors.append(
108
- {
109
- "name": monitor.get("name"),
110
- "parent": grouped.get(monitor.get("id")),
111
- "url": url,
112
- "host": host,
113
- "tag_names": [tag.get("name") for tag in monitor.get("tags", []) if "name" in tag],
114
- }
115
- )
116
- return monitors
117
-
118
-
119
- async def group_by_host(monitors: List[Dict[str, Any]]) -> Dict[str, Any]:
120
- """Group monitors by host."""
121
- grouped = defaultdict(list)
122
-
123
- for monitor in monitors:
124
- grouped[monitor["host"]].append(monitor)
125
-
126
- return dict(grouped)
105
+ yield {
106
+ "name": monitor.get("name"),
107
+ "parent": grouped.get(monitor.get("id")),
108
+ "url": url,
109
+ "host": host,
110
+ "tags": [tag.get("name") for tag in monitor.get("tags", []) if "name" in tag],
111
+ }
pyobservability/main.py CHANGED
@@ -13,7 +13,7 @@ from fastapi.staticfiles import StaticFiles
13
13
  from fastapi.templating import Jinja2Templates
14
14
 
15
15
  from pyobservability.config import enums, settings
16
- from pyobservability.kuma import UptimeKumaClient, extract_monitors, group_by_host
16
+ from pyobservability.kuma import UptimeKumaClient, extract_monitors
17
17
  from pyobservability.transport import websocket_endpoint
18
18
  from pyobservability.version import __version__
19
19
 
@@ -41,8 +41,8 @@ async def index(request: Request):
41
41
  TemplateResponse:
42
42
  Rendered HTML template with targets and version.
43
43
  """
44
- kuma_data = {} if all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password)) else None
45
- args = dict(request=request, service_map=kuma_data, targets=settings.env.targets, version=__version__)
44
+ kuma_data = [{}] if all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password)) else None
45
+ args = dict(request=request, kuma_data=kuma_data, targets=settings.env.targets, version=__version__)
46
46
  if settings.env.username and settings.env.password:
47
47
  args["logout"] = uiauth.enums.APIEndpoints.fastapi_logout.value
48
48
  return templates.TemplateResponse("index.html", args)
@@ -52,20 +52,20 @@ async def kuma():
52
52
  """Kuma endpoint to retrieve monitors from Kuma server.
53
53
 
54
54
  Returns:
55
- Dict[str, Any]:
56
- Grouped monitors by host from Kuma server.
55
+ List[Dict[str, Any]]:
56
+ List of monitors from Kuma server after filtering the required fields.
57
57
  """
58
58
  try:
59
59
  kuma_data = UptimeKumaClient().get_monitors()
60
- LOGGER.info("Retrieved payload from kuma server.")
60
+ LOGGER.info("Retrieved monitors from kuma server - %d found.", len(kuma_data))
61
61
  except RuntimeError:
62
62
  raise HTTPException(
63
63
  status_code=HTTPStatus.SERVICE_UNAVAILABLE.real,
64
64
  detail="Unable to retrieve data from kuma server.",
65
65
  )
66
- json_monitors = await extract_monitors(kuma_data)
67
- LOGGER.info("Extracted JSON monitors from kuma payload.")
68
- return await group_by_host(json_monitors)
66
+ monitors = list(extract_monitors(kuma_data))
67
+ LOGGER.info("Processed [%d] monitors for UI payload.", len(monitors))
68
+ return monitors
69
69
 
70
70
 
71
71
  async def health() -> Dict[str, str]:
@@ -366,26 +366,24 @@
366
366
  });
367
367
  }
368
368
 
369
- function normalizeServiceMap(serviceMap) {
369
+ function normalizeKumaMap(monitors) {
370
370
  const rows = [];
371
- Object.entries(serviceMap || {}).forEach(([host, services]) => {
372
- services.forEach(svc => {
373
- rows.push({
374
- Node: host,
375
- Name: svc.name || "",
376
- Parent: svc.parent || "",
377
- Tags: (svc.tag_names || []).join(", "),
378
- URL: `<a href="${svc.url}" target="_blank">${svc.url}</a>`
379
- });
371
+ monitors.forEach(monitor => {
372
+ rows.push({
373
+ Node: monitor.host,
374
+ Name: monitor.name,
375
+ Parent: monitor.parent || "",
376
+ Tags: (monitor.tags || []).join(", "),
377
+ URL: `<a href="${monitor.url}" target="_blank">${monitor.url}</a>`
380
378
  });
381
379
  });
382
380
  return rows;
383
381
  }
384
382
 
385
383
  function renderEndpoints() {
386
- if (!window.SERVICE_MAP) return;
384
+ if (!window.KUMA_DATA) return;
387
385
 
388
- const rows = normalizeServiceMap(window.SERVICE_MAP);
386
+ const rows = normalizeKumaMap(window.KUMA_DATA);
389
387
  const columns = ["Node", "Name", "Parent", "Tags", "URL"];
390
388
 
391
389
  PAG_ENDPOINTS.setData(rows, columns);
@@ -827,6 +825,9 @@
827
825
  function resetUI() {
828
826
  firstMessage = true;
829
827
  hideSpinners();
828
+ // Disable Kuma tab during loading if present
829
+ if (kumaTab) { kumaTab.disabled = true; }
830
+
830
831
  const EMPTY_DATA = Array(MAX_POINTS).fill(null);
831
832
  const EMPTY_LABELS = Array(MAX_POINTS).fill("");
832
833
 
@@ -940,6 +941,8 @@
940
941
  if (firstMessage) {
941
942
  hideSpinners();
942
943
  firstMessage = false;
944
+ // Enable Kuma tab once initial data is loaded
945
+ if (kumaTab) { kumaTab.disabled = false; }
943
946
  }
944
947
 
945
948
  const now = new Date().toLocaleTimeString();
@@ -1197,12 +1200,12 @@
1197
1200
 
1198
1201
  try {
1199
1202
  const response = await fetch('/kuma');
1200
- if (!response.ok) throw new Error('Failed to fetch service map');
1203
+ if (!response.ok) throw new Error('Failed to fetch kuma map');
1201
1204
 
1202
1205
  kumaMapData = await response.json();
1203
1206
  kumaMapLoaded = true;
1204
1207
 
1205
- allKumaRows = normalizeServiceMap(kumaMapData);
1208
+ allKumaRows = normalizeKumaMap(kumaMapData);
1206
1209
  PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
1207
1210
  } catch (err) {
1208
1211
  console.error("Error loading Kuma map:", err);
@@ -1273,5 +1276,7 @@
1273
1276
 
1274
1277
  // Initialize nodes view by default
1275
1278
  document.body.classList.add('nodes-view');
1279
+ // Disable until first metrics load
1280
+ if (kumaTab) { kumaTab.disabled = true; }
1276
1281
  initWebSocket();
1277
1282
  })();
@@ -51,7 +51,7 @@ html, body {
51
51
  transition: all 0.2s;
52
52
  }
53
53
 
54
- .tab-btn:hover {
54
+ .tab-btn:hover:not(:disabled) {
55
55
  color: #e6eef8;
56
56
  }
57
57
 
@@ -60,6 +60,11 @@ html, body {
60
60
  border-bottom-color: var(--accent);
61
61
  }
62
62
 
63
+ .tab-btn:disabled {
64
+ opacity: 0.4;
65
+ cursor: not-allowed;
66
+ }
67
+
63
68
  .brand {
64
69
  font-weight: 700;
65
70
  font-size: 18px;
@@ -19,7 +19,7 @@
19
19
  <header class="topbar">
20
20
  <div class="brand">Node Monitor</div>
21
21
 
22
- {% if service_map is not none %}
22
+ {% if kuma_data is not none %}
23
23
  <div class="tab-navigation">
24
24
  <button id="nodes-tab" class="tab-btn active">Nodes</button>
25
25
  <button id="kuma-tab" class="tab-btn">Uptime Kuma</button>
@@ -293,9 +293,9 @@
293
293
  window.MONITOR_TARGETS = {{ targets | tojson }};
294
294
  </script>
295
295
 
296
- {% if service_map is not none %}
296
+ {% if kuma_data is not none %}
297
297
  <script>
298
- window.SERVICE_MAP = {{ service_map | tojson }};
298
+ window.KUMA_DATA = {{ kuma_data | tojson }};
299
299
  </script>
300
300
  {% endif %}
301
301
 
@@ -1 +1 @@
1
- __version__ = "2.0.0"
1
+ __version__ = "2.1.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 2.0.0
3
+ Version: 2.1.1
4
4
  Summary: Lightweight OS-agnostic observability UI for PyNinja
5
5
  Author-email: Vignesh Rao <svignesh1793@gmail.com>
6
6
  License: MIT License
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: aiohttp==3.13.*
45
45
  Requires-Dist: fastapi==0.128.*
46
- Requires-Dist: FastAPI-UI-Auth==0.2.1
46
+ Requires-Dist: FastAPI-UI-Auth==0.2.*
47
47
  Requires-Dist: Jinja2==3.1.*
48
48
  Requires-Dist: pydantic==2.12.*
49
49
  Requires-Dist: pydantic-settings==2.12.*
@@ -0,0 +1,17 @@
1
+ pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
2
+ pyobservability/kuma.py,sha256=4sB1M7-exzrObGGIh1yYQ22P40kHwsTfv4G1Xj9YYWw,3260
3
+ pyobservability/main.py,sha256=sMcQRh4mmivs8Xssu_PcP698oc1AzmMt8Tv19BSdmdo,6152
4
+ pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
5
+ pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
6
+ pyobservability/version.py,sha256=zPJIgPGcoSNiD0qme18OnYJYE3A9VVytlhO-V5DaAW0,22
7
+ pyobservability/config/enums.py,sha256=rQZh2Q4-9ItQTQgxsYjqw-jNePpWHhvQUrbrQJBG5CI,313
8
+ pyobservability/config/settings.py,sha256=aCz1tZEg8fUA5gHCCDN_m3fgrH1axz2fvdalj4bszGs,6121
9
+ pyobservability/static/app.js,sha256=rTj3WWTgkfHJR3qf19flGUJ-9OAeFtVX6Nm5vw51T0w,48862
10
+ pyobservability/static/styles.css,sha256=pSVrSpeeDHaNsqI-WI3PrtnaPRiuU5rIrpB8g6OlHBc,9486
11
+ pyobservability/templates/index.html,sha256=aqvHrxP_D2gSxYmsUkNMi1QH07pQNXh4VuXc51KBo5c,11826
12
+ pyobservability-2.1.1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
13
+ pyobservability-2.1.1.dist-info/METADATA,sha256=1iEH3chJwsf8T2FKyXLA3xn4x6coVytWSSr83o_9G1w,7026
14
+ pyobservability-2.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ pyobservability-2.1.1.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
16
+ pyobservability-2.1.1.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
17
+ pyobservability-2.1.1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
2
- pyobservability/kuma.py,sha256=RL6ZsK6ZE3vUufsUQ3r4gnH8uka0k8ARd41GP5YozbU,3634
3
- pyobservability/main.py,sha256=EbP6xkOvj2cV1cOEmmq6E6QsLOhbvKI_jdpfcWjSz-M,6124
4
- pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
5
- pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
6
- pyobservability/version.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
7
- pyobservability/config/enums.py,sha256=rQZh2Q4-9ItQTQgxsYjqw-jNePpWHhvQUrbrQJBG5CI,313
8
- pyobservability/config/settings.py,sha256=aCz1tZEg8fUA5gHCCDN_m3fgrH1axz2fvdalj4bszGs,6121
9
- pyobservability/static/app.js,sha256=lE-Oybq50FNr_0SrZJFiJihiXyIcyhw7-SNbGA81ySQ,48675
10
- pyobservability/static/styles.css,sha256=P6Xg-IAXO3WNeBLGH9Q5HAdNeDMRbFcM5P_cq60Jf00,9405
11
- pyobservability/templates/index.html,sha256=WQNOT_uHfAvVqOnYc1olYdyrjM11tDpeKwTkbPQ853Q,11834
12
- pyobservability-2.0.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
13
- pyobservability-2.0.0.dist-info/METADATA,sha256=hu_7k6rRxUXitpfLhkAB6GZxMYQNDeqGaQAHJDKa0Ss,7026
14
- pyobservability-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
- pyobservability-2.0.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
16
- pyobservability-2.0.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
17
- pyobservability-2.0.0.dist-info/RECORD,,