PyObservability 0.0.0a0__py3-none-any.whl → 0.0.0a2__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.
@@ -0,0 +1,68 @@
1
+ """Module for packaging."""
2
+
3
+ import sys
4
+
5
+ from pyobservability.main import start # noqa: F401
6
+ from pyobservability.version import __version__
7
+
8
+
9
+ def _cli() -> None:
10
+ """Starter function to invoke the observability UI via CLI commands.
11
+
12
+ **Flags**
13
+ - ``--version | -V``: Prints the version.
14
+ - ``--help | -H``: Prints the help section.
15
+ - ``--env | -E <path>``: Filepath to load environment variables.
16
+
17
+ **Commands**
18
+ ``start``: Initiates the PyObservability as a regular script.
19
+ """
20
+ assert sys.argv[0].endswith("pyobservability"), "Invalid commandline trigger!!"
21
+ options = {
22
+ "--version | -V": "Prints the version.",
23
+ "--help | -H": "Prints the help section.",
24
+ "--env | -E <path>": "Filepath to load environment variables.",
25
+ "start": "Initiates the PyObservability as a regular script.",
26
+ }
27
+ # weird way to increase spacing to keep all values monotonic
28
+ _longest_key = len(max(options.keys()))
29
+ _pretext = "\n\t* "
30
+ choices = _pretext + _pretext.join(
31
+ f"{k} {'·' * (_longest_key - len(k) + 8)}→ {v}".expandtabs() for k, v in options.items()
32
+ )
33
+ args = [arg.lower() for arg in sys.argv[1:]]
34
+ try:
35
+ assert len(args) > 1
36
+ except (IndexError, AttributeError, AssertionError):
37
+ print(f"Cannot proceed without a valid arbitrary command. Please choose from {choices}")
38
+ exit(1)
39
+ env_file = None
40
+ if any(arg in args for arg in ["version", "--version", "-v"]):
41
+ print(f"PyObservability: {__version__}")
42
+ exit(0)
43
+ elif any(arg in args for arg in ["help", "--help", "-h"]):
44
+ print(f"Usage: pyobservability [arbitrary-command]\nOptions (and corresponding behavior):{choices}")
45
+ exit(0)
46
+ elif any(arg in args for arg in ["env", "--env", "E", "-e"]):
47
+ extra_index = next(
48
+ (index for index, arg in enumerate(args) if arg in ["env", "--env", "E", "-e"]),
49
+ None,
50
+ )
51
+ try:
52
+ env_file = sys.argv[extra_index + 2]
53
+ except (IndexError, TypeError):
54
+ print("Cannot proceed without a valid extra environment file path.")
55
+ exit(1)
56
+ elif any(arg in args for arg in ("start",)):
57
+ pass
58
+ else:
59
+ print(f"Unknown Option: {sys.argv[1]}\nArbitrary commands must be one of {choices}")
60
+ exit(1)
61
+ if any(arg in args for arg in ("start",)):
62
+ start(env_file=env_file)
63
+ else:
64
+ print(
65
+ "Insufficient Arguments:\n\tNo command received to initiate the PyObservability. "
66
+ f"Please choose from {choices}"
67
+ )
68
+ exit(1)
@@ -0,0 +1,108 @@
1
+ import json
2
+ import pathlib
3
+ import socket
4
+ from typing import List
5
+
6
+ import yaml
7
+ from pydantic import BaseModel, HttpUrl, PositiveInt
8
+ from pydantic_settings import BaseSettings
9
+
10
+
11
+ class PydanticEnvConfig(BaseSettings):
12
+ """Pydantic BaseSettings with custom order for loading environment variables.
13
+
14
+ >>> PydanticEnvConfig
15
+
16
+ """
17
+
18
+ @classmethod
19
+ def settings_customise_sources(
20
+ cls,
21
+ settings_cls,
22
+ init_settings,
23
+ env_settings,
24
+ dotenv_settings,
25
+ file_secret_settings,
26
+ ):
27
+ """Order: dotenv, env, init, secrets files."""
28
+ return dotenv_settings, env_settings, init_settings, file_secret_settings
29
+
30
+
31
+ class MonitorTarget(BaseModel):
32
+ name: str
33
+ base_url: HttpUrl
34
+ apikey: str
35
+
36
+
37
+ class EnvConfig(PydanticEnvConfig):
38
+ """Configuration settings for the server.
39
+
40
+ >>> EnvConfig
41
+
42
+ """
43
+
44
+ host: str = socket.gethostbyname("localhost") or "0.0.0.0"
45
+ port: PositiveInt = 8080
46
+
47
+ monitor_targets: List[MonitorTarget]
48
+ poll_interval: PositiveInt = 3
49
+
50
+ class Config:
51
+ """Environment variables configuration."""
52
+
53
+ env_prefix = ""
54
+ extra = "forbid"
55
+
56
+ @classmethod
57
+ def from_env_file(cls, filename: pathlib.Path) -> "EnvConfig":
58
+ """Create an instance of EnvConfig from environment file.
59
+
60
+ Args:
61
+ filename: Name of the env file.
62
+
63
+ Returns:
64
+ EnvConfig:
65
+ Loads the ``EnvConfig`` model.
66
+ """
67
+ # noinspection PyArgumentList
68
+ return cls(_env_file=filename)
69
+
70
+
71
+ def env_loader(**kwargs) -> EnvConfig:
72
+ """Loads environment variables based on filetypes or kwargs.
73
+
74
+ Returns:
75
+ EnvConfig:
76
+ Returns a reference to the ``EnvConfig`` object.
77
+ """
78
+ # Default to .env if no kwargs were passed
79
+ if not kwargs:
80
+ return EnvConfig.from_env_file(".env")
81
+ # Look for the kwarg env_file and process accordingly
82
+ if env_file := kwargs.get("env_file"):
83
+ env_file = pathlib.Path(env_file)
84
+ assert env_file.is_file(), f"\n\tenv_file: [{env_file.resolve()!r}] does not exist"
85
+ if env_file.suffix.lower() == ".json":
86
+ with env_file.open() as stream:
87
+ env_data = json.load(stream)
88
+ return EnvConfig(**{k.lower(): v for k, v in env_data.items()})
89
+ elif env_file.suffix.lower() in (".yaml", ".yml"):
90
+ with env_file.open() as stream:
91
+ env_data = yaml.load(stream, yaml.FullLoader)
92
+ return EnvConfig(**{k.lower(): v for k, v in env_data.items()})
93
+ elif not env_file.suffix or env_file.suffix.lower() in (
94
+ ".text",
95
+ ".txt",
96
+ ".env",
97
+ "",
98
+ ):
99
+ return EnvConfig.from_env_file(env_file)
100
+ else:
101
+ raise ValueError(
102
+ f"\n\tUnsupported format for {env_file!r}, " "can be one of (.json, .yaml, .yml, .txt, .text, .env)"
103
+ )
104
+ # Load env config with regular kwargs
105
+ return EnvConfig(**kwargs)
106
+
107
+
108
+ env: EnvConfig
pyobservability/main.py CHANGED
@@ -1,30 +1,45 @@
1
- # app/main.py
2
- import asyncio
1
+ import logging
3
2
  import pathlib
4
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
3
+
4
+ import uvicorn
5
+ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
6
+ from fastapi.routing import APIRoute, APIWebSocketRoute
5
7
  from fastapi.staticfiles import StaticFiles
6
8
  from fastapi.templating import Jinja2Templates
9
+
10
+ from pyobservability.config import settings
7
11
  from pyobservability.monitor import Monitor
8
- import os
9
- from dotenv import load_dotenv
10
- load_dotenv()
11
12
 
12
- app = FastAPI(title="Monitor UI")
13
+ LOGGER = logging.getLogger("uvicorn.default")
14
+
15
+ PyObservability = FastAPI(title="PyObservability")
16
+ PyObservability.__name__ = "PyObservability"
17
+ PyObservability.description = "Observability page for nodes running PyNinja"
18
+
13
19
  root = pathlib.Path(__file__).parent
14
20
  templates_dir = root / "templates"
15
- static_dir = root / "static"
16
21
  templates = Jinja2Templates(directory=templates_dir)
17
- app.mount("/static", StaticFiles(directory=static_dir), name="static")
18
22
 
19
- monitor = Monitor(poll_interval=float(os.getenv("POLL_INTERVAL", 2)))
23
+ static_dir = root / "static"
24
+ PyObservability.mount("/static", StaticFiles(directory=static_dir), name="static")
25
+
20
26
 
21
- @app.get("/")
22
27
  async def index(request: Request):
23
- # pass configured targets to the template so frontend can prebuild UI
24
- return templates.TemplateResponse("index.html", {"request": request, "targets": monitor.targets})
28
+ """Pass configured targets to the template so frontend can prebuild UI.
29
+
30
+ Args:
31
+ request: FastAPI request object.
32
+ """
33
+ return templates.TemplateResponse("index.html", {"request": request, "targets": settings.env.monitor_targets})
34
+
25
35
 
26
- @app.websocket("/ws")
27
36
  async def websocket_endpoint(websocket: WebSocket):
37
+ """Websocket endpoint to render the metrics.
38
+
39
+ Args:
40
+ websocket: FastAPI websocket object.
41
+ """
42
+ monitor = Monitor(targets=settings.env.monitor_targets, poll_interval=settings.env.poll_interval)
28
43
  await websocket.accept()
29
44
  await monitor.start()
30
45
  q = monitor.subscribe()
@@ -39,6 +54,35 @@ async def websocket_endpoint(websocket: WebSocket):
39
54
  monitor.unsubscribe(q)
40
55
  try:
41
56
  await websocket.close()
42
- except:
43
- pass
57
+ except Exception as err:
58
+ LOGGER.warning(err)
44
59
  await monitor.stop()
60
+
61
+
62
+ PyObservability.routes.append(
63
+ APIRoute(
64
+ path="/", # enums.APIEndpoints.root,
65
+ endpoint=index,
66
+ methods=["GET"],
67
+ include_in_schema=False,
68
+ ),
69
+ )
70
+ PyObservability.routes.append(
71
+ APIWebSocketRoute(
72
+ path="/ws",
73
+ endpoint=websocket_endpoint,
74
+ )
75
+ )
76
+
77
+
78
+ def start(**kwargs):
79
+ settings.env = settings.env_loader(**kwargs)
80
+ settings.env.monitor_targets = [
81
+ {k: str(v) for k, v in target.model_dump().items()} for target in settings.env.monitor_targets
82
+ ]
83
+ uvicorn_args = dict(
84
+ host=settings.env.host,
85
+ port=settings.env.port,
86
+ app=PyObservability,
87
+ )
88
+ uvicorn.run(**uvicorn_args)
@@ -1,12 +1,14 @@
1
1
  # app/monitor.py
2
2
 
3
3
  import asyncio
4
- import os
5
- import json
4
+ import logging
5
+ from asyncio import CancelledError
6
6
  from typing import Any, Dict, List
7
+ from urllib.parse import urlparse
8
+
7
9
  import aiohttp
8
- from asyncio import CancelledError
9
10
 
11
+ LOGGER = logging.getLogger("uvicorn.default")
10
12
 
11
13
  ###############################################################################
12
14
  # ENDPOINT DEFINITIONS (PyNinja Correct)
@@ -52,46 +54,20 @@ ENDPOINTS = {
52
54
  "certificates": {
53
55
  "path": "/get-certificates",
54
56
  "params": {},
55
- }
57
+ },
56
58
  }
57
59
 
58
60
 
59
- ###############################################################################
60
- # LOAD TARGETS FROM ENV
61
- ###############################################################################
62
-
63
- def load_targets_from_env() -> List[Dict[str, Any]]:
64
- raw = os.getenv("MONITOR_TARGETS", "[]")
65
-
66
- try:
67
- data = json.loads(raw)
68
- except Exception:
69
- data = [raw] if raw else []
70
-
71
- parsed = []
72
-
73
- for entry in data:
74
- if isinstance(entry, str):
75
- parsed.append({"name": entry, "base_url": entry, "apikey": None})
76
- elif isinstance(entry, dict):
77
- parsed.append({
78
- "name": entry.get("name") or entry["base_url"],
79
- "base_url": entry["base_url"],
80
- "apikey": entry.get("apikey")
81
- })
82
-
83
- return parsed
84
-
85
-
86
61
  ###############################################################################
87
62
  # MONITOR CLASS
88
63
  ###############################################################################
89
64
 
65
+
90
66
  class Monitor:
91
67
 
92
- def __init__(self, poll_interval: float = 2.0):
93
- self.targets = load_targets_from_env()
94
- self.poll_interval = float(os.getenv("POLL_INTERVAL", poll_interval))
68
+ def __init__(self, targets: List[Dict[str, str]], poll_interval: float):
69
+ self.targets = targets
70
+ self.poll_interval = poll_interval
95
71
  self.sessions: Dict[str, aiohttp.ClientSession] = {}
96
72
  self._ws_subscribers: List[asyncio.Queue] = []
97
73
  self._task = None
@@ -101,8 +77,8 @@ class Monitor:
101
77
  # LIFECYCLE
102
78
  ############################################################################
103
79
  async def start(self):
104
- for t in self.targets:
105
- self.sessions[t["base_url"]] = aiohttp.ClientSession()
80
+ for target in self.targets:
81
+ self.sessions[target["base_url"]] = aiohttp.ClientSession()
106
82
  self._task = asyncio.create_task(self._run_loop())
107
83
 
108
84
  async def stop(self):
@@ -134,49 +110,40 @@ class Monitor:
134
110
  ############################################################################
135
111
  # FETCH WRAPPER
136
112
  ############################################################################
137
- async def _fetch(self, session, base_url, ep, apikey=None, params=None):
113
+ async def _fetch(self, session, base_url, ep, headers: Dict[str, str], params=None):
138
114
  url = base_url.rstrip("/") + ep
139
- headers = {"accept": "application/json"}
140
- if apikey:
141
- headers["Authorization"] = f"Bearer {apikey}"
142
-
143
115
  try:
144
116
  async with session.get(url, headers=headers, params=params, timeout=10) as resp:
145
117
  if resp.status == 200:
146
118
  try:
147
119
  return await resp.json()
148
- except Exception:
149
- return {"detail": await resp.text()}
150
- return {"error": f"HTTP {resp.status}"}
151
- except Exception as e:
152
- return {"error": str(e)}
120
+ except Exception as err:
121
+ LOGGER.debug(err)
122
+ return "NO DATA"
123
+ parsed = urlparse(url)
124
+ LOGGER.debug("Exception on '%s' - [%d]: %s", parsed.path, resp.status, await resp.text())
125
+ return "NO DATA"
126
+ except Exception as err:
127
+ LOGGER.debug(err)
128
+ return "NO DATA"
153
129
 
154
130
  ############################################################################
155
131
  # PER-TARGET POLLING
156
132
  ############################################################################
157
133
  async def _poll_target(self, target: Dict[str, Any]) -> Dict[str, Any]:
158
134
  base = target["base_url"]
159
- apikey = target.get("apikey")
135
+ apikey = target["apikey"]
160
136
  session = self.sessions[base]
137
+ headers = {"Accept": "application/json", "Authorization": f"Bearer {apikey}"}
161
138
 
162
- result = {
163
- "name": target["name"],
164
- "base_url": base,
165
- "metrics": {}
166
- }
139
+ result = {"name": target["name"], "base_url": base, "metrics": {}}
167
140
 
168
141
  # Fire ALL requests concurrently
169
142
  tasks = {}
170
143
 
171
144
  for key, cfg in ENDPOINTS.items():
172
145
  tasks[key] = asyncio.create_task(
173
- self._fetch(
174
- session,
175
- base,
176
- cfg["path"],
177
- apikey=apikey,
178
- params=cfg["params"]
179
- )
146
+ self._fetch(session, base, cfg["path"], headers=headers, params=cfg["params"])
180
147
  )
181
148
 
182
149
  # Wait for all endpoints
@@ -184,7 +151,7 @@ class Monitor:
184
151
 
185
152
  for (key, _), resp in zip(tasks.items(), raw_results):
186
153
  if isinstance(resp, Exception):
187
- result["metrics"][key] = {"error": str(resp)}
154
+ result["metrics"][key] = "NO DATA"
188
155
  continue
189
156
  if isinstance(resp, dict):
190
157
  result["metrics"][key] = resp.get("detail", resp)
@@ -198,11 +165,12 @@ class Monitor:
198
165
  # POLL ALL HOSTS
199
166
  ############################################################################
200
167
  async def _poll_all(self) -> List[Dict[str, Any]]:
201
- tasks = [self._poll_target(t) for t in self.targets]
168
+ tasks = [self._poll_target(target) for target in self.targets]
202
169
  results = await asyncio.gather(*tasks, return_exceptions=True)
203
170
  out = []
204
171
  for r in results:
205
172
  if isinstance(r, Exception):
173
+ LOGGER.error("%s", r)
206
174
  out.append({"error": str(r)})
207
175
  else:
208
176
  out.append(r)
@@ -215,11 +183,7 @@ class Monitor:
215
183
  while not self._stop.is_set():
216
184
  metrics = await self._poll_all()
217
185
 
218
- payload = {
219
- "type": "metrics",
220
- "ts": asyncio.get_event_loop().time(),
221
- "data": metrics
222
- }
186
+ payload = {"type": "metrics", "ts": asyncio.get_event_loop().time(), "data": metrics}
223
187
 
224
188
  for q in list(self._ws_subscribers):
225
189
  try:
@@ -27,8 +27,9 @@
27
27
  const servicesTableBody = document.querySelector("#services-table tbody");
28
28
  const svcFilter = document.getElementById("svc-filter");
29
29
 
30
- const dockerStatsEl = document.getElementById("docker-stats");
31
- const containersList = document.getElementById("containers-list");
30
+ const dockerTable = document.getElementById("docker-table");
31
+ const dockerTableHead = dockerTable.querySelector("thead");
32
+ const dockerTableBody = dockerTable.querySelector("tbody");
32
33
 
33
34
  const disksTableBody = document.querySelector("#disks-table tbody");
34
35
  const certsEl = document.getElementById("certificates");
@@ -182,8 +183,8 @@
182
183
  loadEl.textContent = "—";
183
184
 
184
185
  servicesTableBody.innerHTML = "";
185
- dockerStatsEl.textContent = "";
186
- containersList.innerHTML = "";
186
+ dockerTableHead.innerHTML = "";
187
+ dockerTableBody.innerHTML = "";
187
188
  disksTableBody.innerHTML = "";
188
189
  certsEl.textContent = "—";
189
190
  }
@@ -209,6 +210,19 @@
209
210
  return Number.isFinite(n) ? n : null;
210
211
  }
211
212
 
213
+ function formatStringOrObject(x) {
214
+ if (x == null) return "—";
215
+ if (typeof x === "string" || typeof x === "number") return x;
216
+ return Object.entries(x)
217
+ .map(([k,v]) => `${k}: ${v}`)
218
+ .join("\n");
219
+ }
220
+
221
+ function round2(x) {
222
+ const n = Number(x);
223
+ return Number.isFinite(n) ? n.toFixed(2) : "—";
224
+ }
225
+
212
226
  // ------------------------------------------------------------
213
227
  // METRICS HANDLER
214
228
  // ------------------------------------------------------------
@@ -221,14 +235,22 @@
221
235
 
222
236
  // ------------------- BASIC INFO -------------------
223
237
  ipEl.textContent = m.ip?.ip || m.ip || "—";
224
- gpuEl.textContent = JSON.stringify(m.gpu ?? "—", null, 2);
225
- diskEl.textContent = m.disk ? JSON.stringify(m.disk, null, 0) : "—";
238
+ gpuEl.textContent = formatStringOrObject(m.gpu ?? m.cpu ?? "—");
239
+ if (m.disk) {
240
+ const total = m.disk.total ?? "—";
241
+ const used = m.disk.used ?? "—";
242
+ const free = m.disk.free ?? "—";
243
+ diskEl.textContent = `Total: ${total}\nUsed: ${used}\nFree: ${free}`;
244
+ } else {
245
+ diskEl.textContent = "—";
246
+ }
226
247
 
227
248
  // ------------------- MEMORY -------------------
228
249
  if (m.memory) {
229
250
  const used = m.memory.ram_used || m.memory.used || "";
230
251
  const percent = m.memory.ram_usage ?? m.memory.usage ?? m.memory.percent ?? "—";
231
- memEl.textContent = `used: ${used} (${percent}%)`;
252
+ const totalMem = m.memory.ram_total ?? m.memory.total ?? "—";
253
+ memEl.textContent = `Total: ${totalMem}\nUsed: ${used}\nPercent: ${percent}%`;
232
254
  pushPoint(memChart, num(percent));
233
255
  }
234
256
 
@@ -278,7 +300,7 @@
278
300
  const m5 = load.m5 ?? load[1];
279
301
  const m15 = load.m15 ?? load[2];
280
302
 
281
- loadEl.textContent = `${m1} / ${m5} / ${m15}`;
303
+ loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
282
304
  pushPoint(loadChart, num(m1) ?? 0);
283
305
  } else {
284
306
  loadEl.textContent = load;
@@ -309,16 +331,28 @@
309
331
  }
310
332
 
311
333
  // ------------------- DOCKER -------------------
312
- if (m.docker_stats)
313
- dockerStatsEl.textContent = JSON.stringify(m.docker_stats, null, 2);
314
-
315
- if (Array.isArray(m.containers)) {
316
- containersList.innerHTML = "";
317
- for (const c of m.containers) {
318
- const li = document.createElement("li");
319
- li.textContent = c["Container Name"] || c.name || JSON.stringify(c);
320
- containersList.appendChild(li);
334
+ const dockerList = m.docker_stats;
335
+
336
+ if (Array.isArray(dockerList) && dockerTableHead && dockerTableBody) {
337
+ dockerTableHead.innerHTML = "";
338
+ dockerTableBody.innerHTML = "";
339
+
340
+ if (dockerList.length > 0) {
341
+ // Table header
342
+ const columns = Object.keys(dockerList[0]);
343
+ dockerTableHead.innerHTML =
344
+ "<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
345
+
346
+ // Table rows
347
+ dockerList.forEach(c => {
348
+ const row = "<tr>" +
349
+ columns.map(col => `<td>${c[col] ?? ""}</td>`).join("") +
350
+ "</tr>";
351
+ dockerTableBody.insertAdjacentHTML("beforeend", row);
352
+ });
321
353
  }
354
+ } else {
355
+ console.warn("Cannot render Docker table:", dockerList, dockerTableHead, dockerTableBody);
322
356
  }
323
357
 
324
358
  // ------------------- DISKS -------------------
@@ -44,7 +44,7 @@ html,body { height:100%; margin:0; font-family: Inter, system-ui, -apple-system,
44
44
  .core-mini .label { color:var(--muted); font-size:11px; }
45
45
  .core-mini .value { font-weight:700; margin-top:6px; }
46
46
 
47
- .tables-row { display:grid; grid-template-columns: repeat(2, 1fr); gap:12px; }
47
+ .tables-row { display:grid; grid-template-columns:1fr; grid-auto-rows:auto; gap:12px; }
48
48
  .table { width:100%; border-collapse:collapse; font-size:13px; }
49
49
  .table th, .table td { padding:8px; border-bottom:1px solid rgba(255,255,255,0.03); text-align:left; }
50
50
  .pre { background:rgba(255,255,255,0.02); padding:8px; border-radius:6px; overflow:auto; white-space:pre-wrap; font-family:monospace; font-size:13px; }
@@ -91,9 +91,10 @@
91
91
  <div class="panel">
92
92
  <div class="panel-header"><h3>Docker / Containers</h3></div>
93
93
  <div class="panel-body">
94
- <pre id="docker-stats" class="pre"></pre>
95
- <h4>Containers</h4>
96
- <ul id="containers-list" class="list"></ul>
94
+ <table class="table" id="docker-table">
95
+ <thead></thead>
96
+ <tbody></tbody>
97
+ </table>
97
98
  </div>
98
99
  </div>
99
100
 
@@ -1 +1 @@
1
- __version__ = "0.0.0a0"
1
+ __version__ = "0.0.0a2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 0.0.0a0
3
+ Version: 0.0.0a2
4
4
  Summary: Lightweight OS-agnostic observability UI for PyNinja
5
5
  Author-email: Vignesh Rao <svignesh1793@gmail.com>
6
6
  License: MIT License
@@ -41,11 +41,13 @@ Classifier: Topic :: System :: Monitoring
41
41
  Requires-Python: >=3.11
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
- Requires-Dist: fastapi
45
- Requires-Dist: uvicorn[standard]
46
- Requires-Dist: aiohttp
47
- Requires-Dist: jinja2
48
- Requires-Dist: python-dotenv
44
+ Requires-Dist: aiohttp==3.13.*
45
+ Requires-Dist: fastapi==0.122.*
46
+ Requires-Dist: Jinja2==3.1.*
47
+ Requires-Dist: pydantic==2.12.*
48
+ Requires-Dist: pydantic-settings==2.12.*
49
+ Requires-Dist: python-dotenv==1.2.*
50
+ Requires-Dist: uvicorn[standard]==0.38.*
49
51
  Provides-Extra: dev
50
52
  Requires-Dist: sphinx==5.1.1; extra == "dev"
51
53
  Requires-Dist: pre-commit; extra == "dev"
@@ -0,0 +1,14 @@
1
+ pyobservability/__init__.py,sha256=rr4udGMbbNPl3yo7l8R3FUUVVahBtYVaW6vSWWgXlv0,2617
2
+ pyobservability/main.py,sha256=m3jNBQ7B495d1Pk_Fcdy3AQbNts2K8iFwDQDKV1pB0M,2527
3
+ pyobservability/monitor.py,sha256=s2sVp97sLjkkdtL6be82bX5ydu_gBdMSoWxDlmUtpgE,6613
4
+ pyobservability/version.py,sha256=za-UuO_D1PzxYCprpyh75AnwaFhntPuAZpHRqS1fIxc,24
5
+ pyobservability/config/settings.py,sha256=53dYdfO5SbmHQ4cLzPM2JQvrU2Lw70vBghlhiLy28ZI,3013
6
+ pyobservability/static/app.js,sha256=poc7eReoiRUbyI5JKnPwxSqmSNCuOge4aZKCITFy7eo,13494
7
+ pyobservability/static/styles.css,sha256=t6r1C0ueBanipgRRjdu18nmq6RbSGLK5bhpf0BdMOpQ,3245
8
+ pyobservability/templates/index.html,sha256=PsN3aq-7Q6RzGeshoNH5v37G3sHoI2saJq6mfuA6JYs,3977
9
+ pyobservability-0.0.0a2.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
10
+ pyobservability-0.0.0a2.dist-info/METADATA,sha256=y74dxaDkt1pTwgqFmkNj7bemJqOPqKUjfHBHPwWh0UA,2775
11
+ pyobservability-0.0.0a2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ pyobservability-0.0.0a2.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
13
+ pyobservability-0.0.0a2.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
14
+ pyobservability-0.0.0a2.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyobservability = pyobservability:_cli
@@ -1,12 +0,0 @@
1
- pyobservability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pyobservability/main.py,sha256=yegGcq_OTHGIeJOiJecxnNSBYQ_Hf2MRjJs4KgNZsrQ,1385
3
- pyobservability/monitor.py,sha256=H_8-c5_S0QNWlGnMktEm1Z64T8HK2nXQ803V1scr6Ok,7393
4
- pyobservability/version.py,sha256=MVtVCT-b01z6f8XUlOsWWZynQ9Wv001Mk-_erJHty6Y,24
5
- pyobservability/static/app.js,sha256=dLxyB25ttKaSE1RLpCaqeLXRRwcWyre0icMDZUV6CvU,12351
6
- pyobservability/static/styles.css,sha256=lUQ74-frlBuNcf6u_8KHUKcNsdMmb0vSxBBaDLPwGEI,3236
7
- pyobservability/templates/index.html,sha256=6x_fCqTcgcA7eInq49VMsoAGW-Ks80k0by8LeP1rkvI,3988
8
- pyobservability-0.0.0a0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
9
- pyobservability-0.0.0a0.dist-info/METADATA,sha256=E_g2OFzx8C103vvcVThDjaVvJjpBsj86Y9caIH4EZ6E,2663
10
- pyobservability-0.0.0a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- pyobservability-0.0.0a0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
12
- pyobservability-0.0.0a0.dist-info/RECORD,,