hatui-kit 0.1.0__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.
- hatui/__init__.py +11 -0
- hatui/__main__.py +5 -0
- hatui/app.py +200 -0
- hatui/core/__init__.py +1 -0
- hatui/core/actions.py +46 -0
- hatui/core/context.py +33 -0
- hatui/core/input_manager.py +167 -0
- hatui/core/screen_buffer.py +150 -0
- hatui/core/style.py +381 -0
- hatui/core/terminal_env.py +67 -0
- hatui/core/util.py +0 -0
- hatui/core/widget.py +203 -0
- hatui/demo/__init__.py +1 -0
- hatui/demo/screens/dashboard/modals/debug.yaml +72 -0
- hatui/demo/screens/dashboard/modals/help.yaml +43 -0
- hatui/demo/screens/dashboard/providers/phase8.yaml +429 -0
- hatui/demo/screens/dashboard/providers.yaml +487 -0
- hatui/demo/screens/dashboard/screen.yaml +64 -0
- hatui/demo/screens/dashboard/tabs/analysis.yaml +65 -0
- hatui/demo/screens/dashboard/tabs/forensics.yaml +71 -0
- hatui/demo/screens/dashboard/tabs/hacker.yaml +80 -0
- hatui/demo/screens/dashboard/tabs/logs.yaml +39 -0
- hatui/demo/screens/dashboard/tabs/overview.yaml +180 -0
- hatui/demo/screens/dashboard/tabs/visuals.yaml +84 -0
- hatui/demo/screens/dashboard.yaml +166 -0
- hatui/demo_assets.py +16 -0
- hatui/providers/__init__.py +54 -0
- hatui/providers/base.py +61 -0
- hatui/providers/builtins.py +546 -0
- hatui/providers/helpers.py +169 -0
- hatui/runtime/__init__.py +24 -0
- hatui/runtime/action_registry.py +102 -0
- hatui/runtime/bindings.py +40 -0
- hatui/runtime/bootstrap.py +94 -0
- hatui/runtime/cli.py +66 -0
- hatui/runtime/defaults.py +219 -0
- hatui/runtime/engine.py +135 -0
- hatui/runtime/formatters.py +65 -0
- hatui/runtime/loader.py +140 -0
- hatui/runtime/provider_manager.py +138 -0
- hatui/runtime/registries.py +65 -0
- hatui/runtime/render_policy.py +144 -0
- hatui/runtime/router.py +74 -0
- hatui/runtime/store.py +29 -0
- hatui/runtime/validation.py +433 -0
- hatui/runtime/watcher.py +167 -0
- hatui/widgets/__init__.py +89 -0
- hatui/widgets/alert_stack_widget.py +64 -0
- hatui/widgets/alert_widget.py +83 -0
- hatui/widgets/banner_widget.py +76 -0
- hatui/widgets/border_widget.py +93 -0
- hatui/widgets/box_widget.py +81 -0
- hatui/widgets/center_widget.py +29 -0
- hatui/widgets/chart_widget.py +222 -0
- hatui/widgets/code_block_widget.py +91 -0
- hatui/widgets/column_widget.py +48 -0
- hatui/widgets/diff_viewer_widget.py +88 -0
- hatui/widgets/divider_widget.py +66 -0
- hatui/widgets/event_feed_widget.py +72 -0
- hatui/widgets/flow_widget.py +77 -0
- hatui/widgets/gauge_widget.py +109 -0
- hatui/widgets/heatmap_widget.py +153 -0
- hatui/widgets/hex_dump_widget.py +91 -0
- hatui/widgets/histogram_widget.py +96 -0
- hatui/widgets/inspector_widget.py +85 -0
- hatui/widgets/kv_inspector_widget.py +134 -0
- hatui/widgets/label_widget.py +99 -0
- hatui/widgets/list_widget.py +185 -0
- hatui/widgets/log_widget.py +106 -0
- hatui/widgets/menu_widget.py +31 -0
- hatui/widgets/metric_grid_widget.py +103 -0
- hatui/widgets/mini_chart_widget.py +79 -0
- hatui/widgets/modal_host_widget.py +85 -0
- hatui/widgets/modal_widget.py +112 -0
- hatui/widgets/paragraph_widget.py +94 -0
- hatui/widgets/progress_bar_widget.py +124 -0
- hatui/widgets/root_services.py +267 -0
- hatui/widgets/root_widget.py +130 -0
- hatui/widgets/row_widget.py +48 -0
- hatui/widgets/scroll_widget.py +224 -0
- hatui/widgets/selection.py +52 -0
- hatui/widgets/signal_strip_widget.py +79 -0
- hatui/widgets/sparkline_widget.py +84 -0
- hatui/widgets/stat_widget.py +107 -0
- hatui/widgets/status_matrix_widget.py +234 -0
- hatui/widgets/status_strip_widget.py +85 -0
- hatui/widgets/table_widget.py +239 -0
- hatui/widgets/tabs_widget.py +188 -0
- hatui/widgets/text_widget.py +64 -0
- hatui/widgets/timeline_widget.py +81 -0
- hatui/widgets/tree_widget.py +341 -0
- hatui/widgets/visualization.py +110 -0
- hatui_kit-0.1.0.dist-info/METADATA +154 -0
- hatui_kit-0.1.0.dist-info/RECORD +97 -0
- hatui_kit-0.1.0.dist-info/WHEEL +4 -0
- hatui_kit-0.1.0.dist-info/entry_points.txt +2 -0
- hatui_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import random
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from hatui.providers.base import Provider
|
|
16
|
+
from hatui.providers.helpers import render_template, resolve_items, resolve_mapping, resolve_value_spec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConstantProvider(Provider):
|
|
20
|
+
spec_schema = {"value": object}
|
|
21
|
+
|
|
22
|
+
def provide(self, delta_time: float, context):
|
|
23
|
+
return self.spec.get("value")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClockProvider(Provider):
|
|
27
|
+
def provide(self, delta_time: float, context):
|
|
28
|
+
now = time.time()
|
|
29
|
+
local = time.localtime(now)
|
|
30
|
+
utc = time.gmtime(now)
|
|
31
|
+
return {
|
|
32
|
+
"timestamp": now,
|
|
33
|
+
"iso": time.strftime("%Y-%m-%dT%H:%M:%S", local),
|
|
34
|
+
"time": time.strftime("%H:%M:%S", local),
|
|
35
|
+
"date": time.strftime("%Y-%m-%d", local),
|
|
36
|
+
"utc_time": time.strftime("%H:%M:%S", utc),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FrameProvider(Provider):
|
|
41
|
+
def provide(self, delta_time: float, context):
|
|
42
|
+
return {
|
|
43
|
+
"frame": context.frame,
|
|
44
|
+
"delta_time": context.delta_time,
|
|
45
|
+
"elapsed_time": context.elapsed_time,
|
|
46
|
+
"terminal_width": context.terminal_width,
|
|
47
|
+
"terminal_height": context.terminal_height,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RandomProvider(Provider):
|
|
52
|
+
spec_schema = {
|
|
53
|
+
"mode": str,
|
|
54
|
+
"min": float,
|
|
55
|
+
"max": float,
|
|
56
|
+
"choices": list,
|
|
57
|
+
"count": int,
|
|
58
|
+
"step": float,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def provide(self, delta_time: float, context):
|
|
62
|
+
mode = self.spec.get("mode", "float")
|
|
63
|
+
if mode == "int":
|
|
64
|
+
return random.randint(int(self.spec.get("min", 0)), int(self.spec.get("max", 100)))
|
|
65
|
+
if mode == "choice":
|
|
66
|
+
return random.choice(list(self.spec.get("choices", [])))
|
|
67
|
+
if mode == "series":
|
|
68
|
+
count = int(self.spec.get("count", 16))
|
|
69
|
+
minimum = float(self.spec.get("min", 0.0))
|
|
70
|
+
maximum = float(self.spec.get("max", 1.0))
|
|
71
|
+
return [random.uniform(minimum, maximum) for _ in range(count)]
|
|
72
|
+
minimum = float(self.spec.get("min", 0.0))
|
|
73
|
+
maximum = float(self.spec.get("max", 1.0))
|
|
74
|
+
return random.uniform(minimum, maximum)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class WaveformProvider(Provider):
|
|
78
|
+
spec_schema = {
|
|
79
|
+
"phase": float,
|
|
80
|
+
"frequency": float,
|
|
81
|
+
"amplitude": float,
|
|
82
|
+
"offset": float,
|
|
83
|
+
"waveform": str,
|
|
84
|
+
"count": int,
|
|
85
|
+
"step": float,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def provide(self, delta_time: float, context):
|
|
89
|
+
phase = float(self.spec.get("phase", 0.0))
|
|
90
|
+
frequency = float(self.spec.get("frequency", 1.0))
|
|
91
|
+
amplitude = float(self.spec.get("amplitude", 1.0))
|
|
92
|
+
offset = float(self.spec.get("offset", 0.0))
|
|
93
|
+
waveform = self.spec.get("waveform", "sine")
|
|
94
|
+
count = int(self.spec.get("count", 0))
|
|
95
|
+
|
|
96
|
+
def sample(value: float) -> float:
|
|
97
|
+
angle = (context.elapsed_time + phase + value) * frequency
|
|
98
|
+
if waveform == "triangle":
|
|
99
|
+
base = 2 * abs(2 * (angle - math.floor(angle + 0.5))) - 1
|
|
100
|
+
elif waveform == "saw":
|
|
101
|
+
base = 2 * (angle - math.floor(angle + 0.5))
|
|
102
|
+
else:
|
|
103
|
+
base = math.sin(angle)
|
|
104
|
+
return offset + amplitude * base
|
|
105
|
+
|
|
106
|
+
if count > 0:
|
|
107
|
+
step = float(self.spec.get("step", 0.25))
|
|
108
|
+
return [sample(index * step) for index in range(count)]
|
|
109
|
+
return sample(0.0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DemoLogsProvider(Provider):
|
|
113
|
+
spec_schema = {"messages": list, "max_lines": int}
|
|
114
|
+
|
|
115
|
+
def setup(self, context):
|
|
116
|
+
self.messages = self.spec.get(
|
|
117
|
+
"messages",
|
|
118
|
+
[
|
|
119
|
+
{"level": "info", "text": "Link state nominal; passive scan continuing."},
|
|
120
|
+
{"level": "warn", "text": "Signal jitter crossed advisory threshold."},
|
|
121
|
+
{"level": "success", "text": "Remote cache sync completed without drift."},
|
|
122
|
+
{"level": "error", "text": "Packet loss spike detected on relay-03."},
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
self.index = 0
|
|
126
|
+
self.lines: list[dict] = []
|
|
127
|
+
|
|
128
|
+
def provide(self, delta_time: float, context):
|
|
129
|
+
message = dict(self.messages[self.index % len(self.messages)])
|
|
130
|
+
message.setdefault("timestamp", time.strftime("%H:%M:%S", time.localtime()))
|
|
131
|
+
self.lines.append(message)
|
|
132
|
+
self.lines = self.lines[-int(self.spec.get("max_lines", 80)) :]
|
|
133
|
+
self.index += 1
|
|
134
|
+
return list(self.lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class EnvProvider(Provider):
|
|
138
|
+
spec_schema = {"key": str, "keys": list, "default": object}
|
|
139
|
+
|
|
140
|
+
def provide(self, delta_time: float, context):
|
|
141
|
+
key = self.spec.get("key")
|
|
142
|
+
keys = self.spec.get("keys")
|
|
143
|
+
if key is not None:
|
|
144
|
+
return os.environ.get(key, self.spec.get("default"))
|
|
145
|
+
if keys is not None:
|
|
146
|
+
return {item: os.environ.get(item) for item in keys}
|
|
147
|
+
return dict(os.environ)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class FileProvider(Provider):
|
|
151
|
+
spec_schema = {"path": str, "mode": str}
|
|
152
|
+
required_spec_keys = {"path"}
|
|
153
|
+
|
|
154
|
+
def provide(self, delta_time: float, context):
|
|
155
|
+
path = Path(self.spec["path"])
|
|
156
|
+
mode = self.spec.get("mode", "text")
|
|
157
|
+
if mode == "lines":
|
|
158
|
+
return path.read_text(encoding="utf-8").splitlines()
|
|
159
|
+
if mode == "json":
|
|
160
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
161
|
+
return path.read_text(encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class HostInfoProvider(Provider):
|
|
165
|
+
def provide(self, delta_time: float, context):
|
|
166
|
+
return {
|
|
167
|
+
"hostname": socket.gethostname(),
|
|
168
|
+
"platform": platform.platform(),
|
|
169
|
+
"python": platform.python_version(),
|
|
170
|
+
"cwd": os.getcwd(),
|
|
171
|
+
"pid": os.getpid(),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SystemStatsProvider(Provider):
|
|
176
|
+
spec_schema = {"path": str}
|
|
177
|
+
|
|
178
|
+
def provide(self, delta_time: float, context):
|
|
179
|
+
stats = {
|
|
180
|
+
"cpu_count": os.cpu_count(),
|
|
181
|
+
"loadavg": None,
|
|
182
|
+
"disk": {},
|
|
183
|
+
"memory": {},
|
|
184
|
+
}
|
|
185
|
+
if hasattr(os, "getloadavg"):
|
|
186
|
+
load = os.getloadavg()
|
|
187
|
+
stats["loadavg"] = {"1m": load[0], "5m": load[1], "15m": load[2]}
|
|
188
|
+
usage = shutil.disk_usage(self.spec.get("path", os.getcwd()))
|
|
189
|
+
stats["disk"] = {"total": usage.total, "used": usage.used, "free": usage.free}
|
|
190
|
+
try:
|
|
191
|
+
import resource
|
|
192
|
+
|
|
193
|
+
max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
194
|
+
stats["memory"] = {"rss_kb": max_rss}
|
|
195
|
+
except Exception:
|
|
196
|
+
stats["memory"] = {}
|
|
197
|
+
return stats
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CommandOutputProvider(Provider):
|
|
201
|
+
spec_schema = {"command": str, "mode": str}
|
|
202
|
+
required_spec_keys = {"command"}
|
|
203
|
+
|
|
204
|
+
def provide(self, delta_time: float, context):
|
|
205
|
+
result = subprocess.run(
|
|
206
|
+
self.spec["command"],
|
|
207
|
+
shell=True,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
check=False,
|
|
211
|
+
)
|
|
212
|
+
mode = self.spec.get("mode", "stdout")
|
|
213
|
+
if mode == "json":
|
|
214
|
+
return json.loads(result.stdout)
|
|
215
|
+
if mode == "lines":
|
|
216
|
+
return result.stdout.splitlines()
|
|
217
|
+
return {
|
|
218
|
+
"returncode": result.returncode,
|
|
219
|
+
"stdout": result.stdout.rstrip("\n"),
|
|
220
|
+
"stderr": result.stderr.rstrip("\n"),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class HttpJsonProvider(Provider):
|
|
225
|
+
spec_schema = {"url": str, "timeout": float}
|
|
226
|
+
required_spec_keys = {"url"}
|
|
227
|
+
|
|
228
|
+
def provide(self, delta_time: float, context):
|
|
229
|
+
with urllib.request.urlopen(self.spec["url"], timeout=float(self.spec.get("timeout", 5.0))) as response:
|
|
230
|
+
return json.loads(response.read().decode("utf-8"))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TransformProvider(Provider):
|
|
234
|
+
spec_schema = {
|
|
235
|
+
"items": list,
|
|
236
|
+
"mapping": dict,
|
|
237
|
+
"template": str,
|
|
238
|
+
"source": str,
|
|
239
|
+
"default": object,
|
|
240
|
+
"formatter": str,
|
|
241
|
+
"operations": list,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def provide(self, delta_time: float, context):
|
|
245
|
+
if "items" in self.spec:
|
|
246
|
+
return resolve_items(self.spec.get("items", []), context.data)
|
|
247
|
+
if "mapping" in self.spec and "template" in self.spec:
|
|
248
|
+
return render_template(self.spec.get("template", ""), self.spec.get("mapping", {}), context.data)
|
|
249
|
+
if "mapping" in self.spec:
|
|
250
|
+
return resolve_mapping(self.spec.get("mapping", {}), context.data)
|
|
251
|
+
return resolve_value_spec(
|
|
252
|
+
{
|
|
253
|
+
"source": self.spec.get("source"),
|
|
254
|
+
"default": self.spec.get("default"),
|
|
255
|
+
"formatter": self.spec.get("formatter"),
|
|
256
|
+
"template": self.spec.get("template"),
|
|
257
|
+
"operations": self.spec.get("operations", []),
|
|
258
|
+
},
|
|
259
|
+
context.data,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TemplateProvider(Provider):
|
|
264
|
+
spec_schema = {"template": str, "mapping": dict}
|
|
265
|
+
required_spec_keys = {"template"}
|
|
266
|
+
|
|
267
|
+
def provide(self, delta_time: float, context):
|
|
268
|
+
return render_template(self.spec.get("template", ""), self.spec.get("mapping", {}), context.data)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ComposeProvider(Provider):
|
|
272
|
+
spec_schema = {"mapping": dict}
|
|
273
|
+
required_spec_keys = {"mapping"}
|
|
274
|
+
|
|
275
|
+
def provide(self, delta_time: float, context):
|
|
276
|
+
return resolve_mapping(self.spec.get("mapping", {}), context.data)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class RecordsProvider(Provider):
|
|
280
|
+
spec_schema = {"items": list}
|
|
281
|
+
required_spec_keys = {"items"}
|
|
282
|
+
|
|
283
|
+
def provide(self, delta_time: float, context):
|
|
284
|
+
return resolve_items(self.spec.get("items", []), context.data)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class RollingWindowProvider(Provider):
|
|
288
|
+
spec_schema = {"source": str, "default": object, "size": int}
|
|
289
|
+
required_spec_keys = {"source"}
|
|
290
|
+
|
|
291
|
+
def setup(self, context):
|
|
292
|
+
self.values: list = []
|
|
293
|
+
|
|
294
|
+
def provide(self, delta_time: float, context):
|
|
295
|
+
source = resolve_value_spec(
|
|
296
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default")},
|
|
297
|
+
context.data,
|
|
298
|
+
)
|
|
299
|
+
self.values.append(source)
|
|
300
|
+
self.values = self.values[-int(self.spec.get("size", 32)) :]
|
|
301
|
+
return list(self.values)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ThresholdProvider(Provider):
|
|
305
|
+
spec_schema = {
|
|
306
|
+
"source": str,
|
|
307
|
+
"default": object,
|
|
308
|
+
"value": float,
|
|
309
|
+
"operator": str,
|
|
310
|
+
"true": object,
|
|
311
|
+
"false": object,
|
|
312
|
+
}
|
|
313
|
+
required_spec_keys = {"source"}
|
|
314
|
+
|
|
315
|
+
def provide(self, delta_time: float, context):
|
|
316
|
+
value = resolve_value_spec(
|
|
317
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default")},
|
|
318
|
+
context.data,
|
|
319
|
+
)
|
|
320
|
+
compare = float(self.spec.get("value", 0))
|
|
321
|
+
try:
|
|
322
|
+
actual = float(value)
|
|
323
|
+
except (TypeError, ValueError):
|
|
324
|
+
actual = 0.0
|
|
325
|
+
operator = self.spec.get("operator", "gt")
|
|
326
|
+
if operator == "gte":
|
|
327
|
+
matched = actual >= compare
|
|
328
|
+
elif operator == "lt":
|
|
329
|
+
matched = actual < compare
|
|
330
|
+
elif operator == "lte":
|
|
331
|
+
matched = actual <= compare
|
|
332
|
+
elif operator == "eq":
|
|
333
|
+
matched = actual == compare
|
|
334
|
+
elif operator == "ne":
|
|
335
|
+
matched = actual != compare
|
|
336
|
+
else:
|
|
337
|
+
matched = actual > compare
|
|
338
|
+
true_value = self.spec.get("true", self.spec.get(True))
|
|
339
|
+
false_value = self.spec.get("false", self.spec.get(False))
|
|
340
|
+
return true_value if matched else false_value
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class NormalizeStateProvider(Provider):
|
|
344
|
+
spec_schema = {
|
|
345
|
+
"source": str,
|
|
346
|
+
"default": object,
|
|
347
|
+
"state_key": str,
|
|
348
|
+
"severity_key": str,
|
|
349
|
+
"state_map": dict,
|
|
350
|
+
"severity_map": dict,
|
|
351
|
+
"severity_by_state": dict,
|
|
352
|
+
"default_state": str,
|
|
353
|
+
"default_severity": str,
|
|
354
|
+
}
|
|
355
|
+
required_spec_keys = {"source"}
|
|
356
|
+
|
|
357
|
+
def provide(self, delta_time: float, context):
|
|
358
|
+
source = resolve_value_spec(
|
|
359
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default", [])},
|
|
360
|
+
context.data,
|
|
361
|
+
)
|
|
362
|
+
state_key = self.spec.get("state_key", "state")
|
|
363
|
+
severity_key = self.spec.get("severity_key", "severity")
|
|
364
|
+
state_map = dict(self.spec.get("state_map", {}) or {})
|
|
365
|
+
severity_map = dict(self.spec.get("severity_map", {}) or {})
|
|
366
|
+
severity_by_state = dict(self.spec.get("severity_by_state", {}) or {})
|
|
367
|
+
default_state = self.spec.get("default_state", "unknown")
|
|
368
|
+
default_severity = self.spec.get("default_severity", "info")
|
|
369
|
+
if isinstance(source, list):
|
|
370
|
+
return [
|
|
371
|
+
self._normalize_item(
|
|
372
|
+
item,
|
|
373
|
+
state_key=state_key,
|
|
374
|
+
severity_key=severity_key,
|
|
375
|
+
state_map=state_map,
|
|
376
|
+
severity_map=severity_map,
|
|
377
|
+
severity_by_state=severity_by_state,
|
|
378
|
+
default_state=default_state,
|
|
379
|
+
default_severity=default_severity,
|
|
380
|
+
)
|
|
381
|
+
for item in source
|
|
382
|
+
]
|
|
383
|
+
if isinstance(source, dict):
|
|
384
|
+
return self._normalize_item(
|
|
385
|
+
source,
|
|
386
|
+
state_key=state_key,
|
|
387
|
+
severity_key=severity_key,
|
|
388
|
+
state_map=state_map,
|
|
389
|
+
severity_map=severity_map,
|
|
390
|
+
severity_by_state=severity_by_state,
|
|
391
|
+
default_state=default_state,
|
|
392
|
+
default_severity=default_severity,
|
|
393
|
+
)
|
|
394
|
+
return source
|
|
395
|
+
|
|
396
|
+
def _normalize_item(
|
|
397
|
+
self,
|
|
398
|
+
item,
|
|
399
|
+
*,
|
|
400
|
+
state_key: str,
|
|
401
|
+
severity_key: str,
|
|
402
|
+
state_map: dict,
|
|
403
|
+
severity_map: dict,
|
|
404
|
+
severity_by_state: dict,
|
|
405
|
+
default_state: str,
|
|
406
|
+
default_severity: str,
|
|
407
|
+
):
|
|
408
|
+
if not isinstance(item, dict):
|
|
409
|
+
return item
|
|
410
|
+
normalized = dict(item)
|
|
411
|
+
raw_state = str(item.get(state_key, default_state)).lower()
|
|
412
|
+
raw_severity = str(item.get(severity_key, "")).lower()
|
|
413
|
+
state = state_map.get(raw_state, raw_state or default_state)
|
|
414
|
+
severity = severity_map.get(raw_severity, raw_severity) if raw_severity else severity_by_state.get(state, default_severity)
|
|
415
|
+
if not severity:
|
|
416
|
+
severity = severity_by_state.get(state, default_severity)
|
|
417
|
+
normalized[state_key] = state
|
|
418
|
+
normalized[severity_key] = severity
|
|
419
|
+
return normalized
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class BucketProvider(Provider):
|
|
423
|
+
spec_schema = {
|
|
424
|
+
"source": str,
|
|
425
|
+
"default": object,
|
|
426
|
+
"value_key": str,
|
|
427
|
+
"bucket_count": int,
|
|
428
|
+
"min": float,
|
|
429
|
+
"max": float,
|
|
430
|
+
"precision": int,
|
|
431
|
+
}
|
|
432
|
+
required_spec_keys = {"source"}
|
|
433
|
+
|
|
434
|
+
def provide(self, delta_time: float, context):
|
|
435
|
+
source = resolve_value_spec(
|
|
436
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default", [])},
|
|
437
|
+
context.data,
|
|
438
|
+
)
|
|
439
|
+
values = self._values(source)
|
|
440
|
+
if not values:
|
|
441
|
+
return []
|
|
442
|
+
bucket_count = max(int(self.spec.get("bucket_count", 8)), 1)
|
|
443
|
+
minimum = float(self.spec.get("min", min(values)))
|
|
444
|
+
maximum = float(self.spec.get("max", max(values)))
|
|
445
|
+
if maximum <= minimum:
|
|
446
|
+
maximum = minimum + 1.0
|
|
447
|
+
width = (maximum - minimum) / bucket_count
|
|
448
|
+
counts = [0 for _ in range(bucket_count)]
|
|
449
|
+
for value in values:
|
|
450
|
+
if value <= minimum:
|
|
451
|
+
index = 0
|
|
452
|
+
elif value >= maximum:
|
|
453
|
+
index = bucket_count - 1
|
|
454
|
+
else:
|
|
455
|
+
index = min(bucket_count - 1, int((value - minimum) / width))
|
|
456
|
+
counts[index] += 1
|
|
457
|
+
result = []
|
|
458
|
+
precision = int(self.spec.get("precision", 2))
|
|
459
|
+
for index, count in enumerate(counts):
|
|
460
|
+
start = minimum + index * width
|
|
461
|
+
end = start + width
|
|
462
|
+
result.append(
|
|
463
|
+
{
|
|
464
|
+
"label": f"{start:.{precision}f}-{end:.{precision}f}",
|
|
465
|
+
"count": count,
|
|
466
|
+
"value": count,
|
|
467
|
+
"start": round(start, precision),
|
|
468
|
+
"end": round(end, precision),
|
|
469
|
+
"display": str(count),
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
def _values(self, source) -> list[float]:
|
|
475
|
+
if isinstance(source, list):
|
|
476
|
+
result = []
|
|
477
|
+
value_path = self.spec.get("value_key")
|
|
478
|
+
for item in source:
|
|
479
|
+
if isinstance(item, dict) and value_path:
|
|
480
|
+
try:
|
|
481
|
+
result.append(float(item.get(value_path, 0.0)))
|
|
482
|
+
except (TypeError, ValueError):
|
|
483
|
+
continue
|
|
484
|
+
else:
|
|
485
|
+
try:
|
|
486
|
+
result.append(float(item))
|
|
487
|
+
except (TypeError, ValueError):
|
|
488
|
+
continue
|
|
489
|
+
return result
|
|
490
|
+
try:
|
|
491
|
+
return [float(source)]
|
|
492
|
+
except (TypeError, ValueError):
|
|
493
|
+
return []
|
|
494
|
+
|
|
495
|
+
class EventStreamProvider(Provider):
|
|
496
|
+
spec_schema = {"item": dict, "source": str, "default": object, "max_items": int}
|
|
497
|
+
|
|
498
|
+
def setup(self, context):
|
|
499
|
+
self.events: list[dict] = []
|
|
500
|
+
|
|
501
|
+
def provide(self, delta_time: float, context):
|
|
502
|
+
payload = self.spec.get("item")
|
|
503
|
+
if payload is not None:
|
|
504
|
+
value = resolve_mapping(payload, context.data, string_mode="literal_fallback")
|
|
505
|
+
items = [value]
|
|
506
|
+
else:
|
|
507
|
+
source = resolve_value_spec(
|
|
508
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default")},
|
|
509
|
+
context.data,
|
|
510
|
+
)
|
|
511
|
+
if isinstance(source, list):
|
|
512
|
+
items = [item for item in source if isinstance(item, dict)]
|
|
513
|
+
elif isinstance(source, dict):
|
|
514
|
+
items = [source]
|
|
515
|
+
else:
|
|
516
|
+
items = []
|
|
517
|
+
|
|
518
|
+
for item in items:
|
|
519
|
+
event = dict(item)
|
|
520
|
+
event.setdefault("timestamp", time.strftime("%H:%M:%S", time.localtime()))
|
|
521
|
+
self.events.append(event)
|
|
522
|
+
self.events = self.events[-int(self.spec.get("max_items", 80)) :]
|
|
523
|
+
return list(self.events)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class GridHistoryProvider(Provider):
|
|
527
|
+
spec_schema = {"source": str, "default": object, "size": int}
|
|
528
|
+
required_spec_keys = {"source"}
|
|
529
|
+
|
|
530
|
+
def setup(self, context):
|
|
531
|
+
self.rows: list[list[float]] = []
|
|
532
|
+
|
|
533
|
+
def provide(self, delta_time: float, context):
|
|
534
|
+
row = resolve_value_spec(
|
|
535
|
+
{"source": self.spec.get("source"), "default": self.spec.get("default", [])},
|
|
536
|
+
context.data,
|
|
537
|
+
)
|
|
538
|
+
if isinstance(row, dict):
|
|
539
|
+
values = list(row.values())
|
|
540
|
+
elif isinstance(row, list):
|
|
541
|
+
values = row
|
|
542
|
+
else:
|
|
543
|
+
values = [row]
|
|
544
|
+
self.rows.append(list(values))
|
|
545
|
+
self.rows = self.rows[-int(self.spec.get("size", 16)) :]
|
|
546
|
+
return list(self.rows)
|