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.
Files changed (97) hide show
  1. hatui/__init__.py +11 -0
  2. hatui/__main__.py +5 -0
  3. hatui/app.py +200 -0
  4. hatui/core/__init__.py +1 -0
  5. hatui/core/actions.py +46 -0
  6. hatui/core/context.py +33 -0
  7. hatui/core/input_manager.py +167 -0
  8. hatui/core/screen_buffer.py +150 -0
  9. hatui/core/style.py +381 -0
  10. hatui/core/terminal_env.py +67 -0
  11. hatui/core/util.py +0 -0
  12. hatui/core/widget.py +203 -0
  13. hatui/demo/__init__.py +1 -0
  14. hatui/demo/screens/dashboard/modals/debug.yaml +72 -0
  15. hatui/demo/screens/dashboard/modals/help.yaml +43 -0
  16. hatui/demo/screens/dashboard/providers/phase8.yaml +429 -0
  17. hatui/demo/screens/dashboard/providers.yaml +487 -0
  18. hatui/demo/screens/dashboard/screen.yaml +64 -0
  19. hatui/demo/screens/dashboard/tabs/analysis.yaml +65 -0
  20. hatui/demo/screens/dashboard/tabs/forensics.yaml +71 -0
  21. hatui/demo/screens/dashboard/tabs/hacker.yaml +80 -0
  22. hatui/demo/screens/dashboard/tabs/logs.yaml +39 -0
  23. hatui/demo/screens/dashboard/tabs/overview.yaml +180 -0
  24. hatui/demo/screens/dashboard/tabs/visuals.yaml +84 -0
  25. hatui/demo/screens/dashboard.yaml +166 -0
  26. hatui/demo_assets.py +16 -0
  27. hatui/providers/__init__.py +54 -0
  28. hatui/providers/base.py +61 -0
  29. hatui/providers/builtins.py +546 -0
  30. hatui/providers/helpers.py +169 -0
  31. hatui/runtime/__init__.py +24 -0
  32. hatui/runtime/action_registry.py +102 -0
  33. hatui/runtime/bindings.py +40 -0
  34. hatui/runtime/bootstrap.py +94 -0
  35. hatui/runtime/cli.py +66 -0
  36. hatui/runtime/defaults.py +219 -0
  37. hatui/runtime/engine.py +135 -0
  38. hatui/runtime/formatters.py +65 -0
  39. hatui/runtime/loader.py +140 -0
  40. hatui/runtime/provider_manager.py +138 -0
  41. hatui/runtime/registries.py +65 -0
  42. hatui/runtime/render_policy.py +144 -0
  43. hatui/runtime/router.py +74 -0
  44. hatui/runtime/store.py +29 -0
  45. hatui/runtime/validation.py +433 -0
  46. hatui/runtime/watcher.py +167 -0
  47. hatui/widgets/__init__.py +89 -0
  48. hatui/widgets/alert_stack_widget.py +64 -0
  49. hatui/widgets/alert_widget.py +83 -0
  50. hatui/widgets/banner_widget.py +76 -0
  51. hatui/widgets/border_widget.py +93 -0
  52. hatui/widgets/box_widget.py +81 -0
  53. hatui/widgets/center_widget.py +29 -0
  54. hatui/widgets/chart_widget.py +222 -0
  55. hatui/widgets/code_block_widget.py +91 -0
  56. hatui/widgets/column_widget.py +48 -0
  57. hatui/widgets/diff_viewer_widget.py +88 -0
  58. hatui/widgets/divider_widget.py +66 -0
  59. hatui/widgets/event_feed_widget.py +72 -0
  60. hatui/widgets/flow_widget.py +77 -0
  61. hatui/widgets/gauge_widget.py +109 -0
  62. hatui/widgets/heatmap_widget.py +153 -0
  63. hatui/widgets/hex_dump_widget.py +91 -0
  64. hatui/widgets/histogram_widget.py +96 -0
  65. hatui/widgets/inspector_widget.py +85 -0
  66. hatui/widgets/kv_inspector_widget.py +134 -0
  67. hatui/widgets/label_widget.py +99 -0
  68. hatui/widgets/list_widget.py +185 -0
  69. hatui/widgets/log_widget.py +106 -0
  70. hatui/widgets/menu_widget.py +31 -0
  71. hatui/widgets/metric_grid_widget.py +103 -0
  72. hatui/widgets/mini_chart_widget.py +79 -0
  73. hatui/widgets/modal_host_widget.py +85 -0
  74. hatui/widgets/modal_widget.py +112 -0
  75. hatui/widgets/paragraph_widget.py +94 -0
  76. hatui/widgets/progress_bar_widget.py +124 -0
  77. hatui/widgets/root_services.py +267 -0
  78. hatui/widgets/root_widget.py +130 -0
  79. hatui/widgets/row_widget.py +48 -0
  80. hatui/widgets/scroll_widget.py +224 -0
  81. hatui/widgets/selection.py +52 -0
  82. hatui/widgets/signal_strip_widget.py +79 -0
  83. hatui/widgets/sparkline_widget.py +84 -0
  84. hatui/widgets/stat_widget.py +107 -0
  85. hatui/widgets/status_matrix_widget.py +234 -0
  86. hatui/widgets/status_strip_widget.py +85 -0
  87. hatui/widgets/table_widget.py +239 -0
  88. hatui/widgets/tabs_widget.py +188 -0
  89. hatui/widgets/text_widget.py +64 -0
  90. hatui/widgets/timeline_widget.py +81 -0
  91. hatui/widgets/tree_widget.py +341 -0
  92. hatui/widgets/visualization.py +110 -0
  93. hatui_kit-0.1.0.dist-info/METADATA +154 -0
  94. hatui_kit-0.1.0.dist-info/RECORD +97 -0
  95. hatui_kit-0.1.0.dist-info/WHEEL +4 -0
  96. hatui_kit-0.1.0.dist-info/entry_points.txt +2 -0
  97. 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)