benchlab-pytools 3.0.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 (184) hide show
  1. benchlab/__init__.py +14 -0
  2. benchlab/__main__.py +21 -0
  3. benchlab/config/README.md +325 -0
  4. benchlab/config/__init__.py +9 -0
  5. benchlab/config/config_client.py +631 -0
  6. benchlab/config/config_manager.py +495 -0
  7. benchlab/config/config_tool.py +358 -0
  8. benchlab/config/diff.py +163 -0
  9. benchlab/config/examples/complete_config.json +56 -0
  10. benchlab/config/examples/rgb_lighting.json +24 -0
  11. benchlab/config/examples/simple_fan.json +32 -0
  12. benchlab/config/schema.py +138 -0
  13. benchlab/core/README.md +154 -0
  14. benchlab/core/__init__.py +78 -0
  15. benchlab/core/config.py +145 -0
  16. benchlab/core/datasource.py +1209 -0
  17. benchlab/core/datasource_manager.py +418 -0
  18. benchlab/core/device_registry.py +206 -0
  19. benchlab/core/discovery.py +92 -0
  20. benchlab/core/infrastructure.py +309 -0
  21. benchlab/core/process_manager.py +356 -0
  22. benchlab/core/retry.py +85 -0
  23. benchlab/core/shared_serial.py +235 -0
  24. benchlab/core/statistics.py +224 -0
  25. benchlab/csv_log/README.md +206 -0
  26. benchlab/csv_log/__init__.py +5 -0
  27. benchlab/csv_log/csv_logger_enhanced.py +416 -0
  28. benchlab/csv_log/message_batcher.py +361 -0
  29. benchlab/csv_log/requirements.txt +3 -0
  30. benchlab/csv_log/smart_retry.py +466 -0
  31. benchlab/graph/README.md +117 -0
  32. benchlab/graph/__init__.py +5 -0
  33. benchlab/graph/app.py +353 -0
  34. benchlab/graph/requirements.txt +2 -0
  35. benchlab/graph/runner.py +42 -0
  36. benchlab/graph/ui.py +288 -0
  37. benchlab/hwinfo/README.md +135 -0
  38. benchlab/hwinfo/__init__.py +5 -0
  39. benchlab/hwinfo/hwinfo_export.py +386 -0
  40. benchlab/hwinfo/requirements.txt +1 -0
  41. benchlab/launcher.py +387 -0
  42. benchlab/link/.env.EXAMPLE +10 -0
  43. benchlab/link/README.md +172 -0
  44. benchlab/link/__init__.py +1 -0
  45. benchlab/link/link_main.py +479 -0
  46. benchlab/main.py +462 -0
  47. benchlab/menu.py +729 -0
  48. benchlab/menu_classic.py +410 -0
  49. benchlab/mqtt/README.md +201 -0
  50. benchlab/mqtt/__init__.py +5 -0
  51. benchlab/mqtt/mqtt.config_template +2 -0
  52. benchlab/mqtt/mqtt_publisher.py +540 -0
  53. benchlab/mqtt/requirements.txt +3 -0
  54. benchlab/prefs.py +67 -0
  55. benchlab/restapi/.env.example +17 -0
  56. benchlab/restapi/__init__.py +5 -0
  57. benchlab/restapi/favicon.ico +0 -0
  58. benchlab/restapi/readme.md +202 -0
  59. benchlab/restapi/requirements.txt +5 -0
  60. benchlab/restapi/telemetry_api.py +808 -0
  61. benchlab/sources.py +515 -0
  62. benchlab/tools.py +147 -0
  63. benchlab/tui/README.md +266 -0
  64. benchlab/tui/__init__.py +5 -0
  65. benchlab/tui/config.py +477 -0
  66. benchlab/tui/requirements.txt +2 -0
  67. benchlab/tui/tui_core.py +1298 -0
  68. benchlab/tui/tui_main.py +418 -0
  69. benchlab/vu/README.md +64 -0
  70. benchlab/vu/VU-Server/NOTICE.md +13 -0
  71. benchlab/vu/VU-Server/README.md +80 -0
  72. benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
  73. benchlab/vu/VU-Server/__init__.py +0 -0
  74. benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  75. benchlab/vu/VU-Server/community_applications.md +13 -0
  76. benchlab/vu/VU-Server/config.yaml +9 -0
  77. benchlab/vu/VU-Server/database.py +256 -0
  78. benchlab/vu/VU-Server/dial_driver.py +534 -0
  79. benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
  80. benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
  81. benchlab/vu/VU-Server/dials/__init__.py +0 -0
  82. benchlab/vu/VU-Server/dials/base_logger.py +100 -0
  83. benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  84. benchlab/vu/VU-Server/installer/install.nsi +148 -0
  85. benchlab/vu/VU-Server/make_version.py +40 -0
  86. benchlab/vu/VU-Server/pylintrc +628 -0
  87. benchlab/vu/VU-Server/requirements.txt +9 -0
  88. benchlab/vu/VU-Server/serial_driver.py +244 -0
  89. benchlab/vu/VU-Server/server.py +670 -0
  90. benchlab/vu/VU-Server/server_config.py +255 -0
  91. benchlab/vu/VU-Server/server_dial_handler.py +334 -0
  92. benchlab/vu/VU-Server/vu_notifications.py +24 -0
  93. benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  94. benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  95. benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  96. benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
  97. benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
  98. benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
  99. benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
  100. benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
  101. benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
  102. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
  103. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
  104. benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
  105. benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
  106. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
  107. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
  108. benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
  109. benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
  110. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
  111. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
  112. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
  113. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
  114. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
  115. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
  116. benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
  117. benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
  118. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
  119. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
  120. benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
  121. benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  122. benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  123. benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
  124. benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
  125. benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
  126. benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
  127. benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
  128. benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
  129. benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
  130. benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
  131. benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
  132. benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
  133. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
  134. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
  135. benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
  136. benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
  137. benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
  138. benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  139. benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  140. benchlab/vu/VU-Server/www/favicon.ico +0 -0
  141. benchlab/vu/VU-Server/www/favicon.png +0 -0
  142. benchlab/vu/VU-Server/www/index.html +47 -0
  143. benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
  144. benchlab/vu/VU-Server/www/views/dial.html +286 -0
  145. benchlab/vu/VU-Server/www/views/footer.html +27 -0
  146. benchlab/vu/VU-Server/www/views/header.html +86 -0
  147. benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
  148. benchlab/vu/VU-Server/www/views/start.html +124 -0
  149. benchlab/vu/__init__.py +5 -0
  150. benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  151. benchlab/vu/assets/bl_dial_144x200.png +0 -0
  152. benchlab/vu/assets/bl_logo_144x200.png +0 -0
  153. benchlab/vu/devices.py +166 -0
  154. benchlab/vu/requirements.txt +24 -0
  155. benchlab/vu/sensors.py +18 -0
  156. benchlab/vu/vu_dial.config_template +90 -0
  157. benchlab/vu/vu_logo_gen.py +97 -0
  158. benchlab/vu/vu_server.config_template +5 -0
  159. benchlab/vu/vu_server_config.py +40 -0
  160. benchlab/vu/vu_server_manager.py +192 -0
  161. benchlab/vu/vu_tui.py +919 -0
  162. benchlab/vu/vu_updater.py +540 -0
  163. benchlab/wigidash/README.md +182 -0
  164. benchlab/wigidash/__init__.py +5 -0
  165. benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  166. benchlab/wigidash/assets/Inter.ttf +0 -0
  167. benchlab/wigidash/assets/benchlab.png +0 -0
  168. benchlab/wigidash/benchlab_fleet.py +150 -0
  169. benchlab/wigidash/benchlab_graph.py +517 -0
  170. benchlab/wigidash/benchlab_overview.py +426 -0
  171. benchlab/wigidash/benchlab_telemetry.py +105 -0
  172. benchlab/wigidash/benchlab_ui.py +190 -0
  173. benchlab/wigidash/benchlab_utils.py +182 -0
  174. benchlab/wigidash/requirements.txt +9 -0
  175. benchlab/wigidash/wigidash_device.py +541 -0
  176. benchlab/wigidash/wigidash_manager.py +342 -0
  177. benchlab/wigidash/wigidash_session.py +355 -0
  178. benchlab/wigidash/wigidash_usb.py +247 -0
  179. benchlab/wigidash/wigidash_widget.py +88 -0
  180. benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
  181. benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
  182. benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
  183. benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
  184. benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,361 @@
1
+ """
2
+ Message Batching System for BENCHLAB CSV Logger
3
+ Provides efficient buffering and batch processing for improved performance
4
+ """
5
+
6
+ import time
7
+ import json
8
+ import csv
9
+ import threading
10
+ from typing import List, Dict, Any, Optional, Callable
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+ import logging
15
+
16
+
17
+ @dataclass
18
+ class BatchConfig:
19
+ """Configuration for message batching"""
20
+ batch_size: int = 100
21
+ flush_interval: float = 30.0 # seconds between automatic flushes
22
+ # maximum messages in buffer before dropping
23
+ max_buffer_size: int = 10000
24
+ flush_on_shutdown: bool = True
25
+ enable_metrics: bool = True
26
+
27
+
28
+ @dataclass
29
+ class BatchMetrics:
30
+ """Metrics for batch processing performance"""
31
+ total_batches: int = 0
32
+ total_messages: int = 0
33
+ total_flushes: int = 0
34
+ avg_batch_size: float = 0.0
35
+ avg_flush_time: float = 0.0
36
+ last_flush_time: Optional[float] = None
37
+ buffer_utilization: float = 0.0
38
+
39
+
40
+ class MessageBatcher:
41
+ """Efficient message batching system with configurable strategies."""
42
+
43
+ def __init__(self, config: BatchConfig = None):
44
+ self.config = config or BatchConfig()
45
+ self.buffer: List[Dict[str, Any]] = []
46
+ self.buffer_lock = threading.Lock()
47
+ # Single lock — no separate flush_lock to avoid deadlocks between
48
+ # the background flush thread and manual flush() calls.
49
+ self.metrics = BatchMetrics()
50
+ self.logger = logging.getLogger(__name__)
51
+
52
+ self.flush_callback: Optional[Callable] = None
53
+ self.metrics_callback: Optional[Callable] = None
54
+
55
+ self.shutdown_event = threading.Event()
56
+ self.flush_thread = threading.Thread(
57
+ target=self._flush_worker, daemon=True, name="BatcherFlush"
58
+ )
59
+ self.flush_thread.start()
60
+
61
+ def add_message(self, message: Dict[str, Any]) -> bool:
62
+ """Add a message to the batch buffer."""
63
+ with self.buffer_lock:
64
+ if len(self.buffer) >= self.config.max_buffer_size:
65
+ self.logger.warning("Buffer full, dropping message")
66
+ return False
67
+ self.buffer.append(message)
68
+ should_flush = len(self.buffer) >= self.config.batch_size
69
+
70
+ if should_flush:
71
+ self.flush()
72
+ return True
73
+
74
+ def flush(self) -> bool:
75
+ """Flush the current buffer to the callback."""
76
+ with self.buffer_lock:
77
+ if not self.buffer:
78
+ return True
79
+ messages_to_flush = self.buffer.copy()
80
+ self.buffer.clear()
81
+
82
+ start_time = time.time()
83
+ try:
84
+ if self.flush_callback:
85
+ self.flush_callback(messages_to_flush)
86
+
87
+ elapsed = time.time() - start_time
88
+ self.metrics.total_batches += 1
89
+ self.metrics.total_messages += len(messages_to_flush)
90
+ self.metrics.total_flushes += 1
91
+ self.metrics.last_flush_time = elapsed
92
+ if self.metrics.total_flushes > 0:
93
+ n = self.metrics.total_flushes
94
+ self.metrics.avg_flush_time = (
95
+ (self.metrics.avg_flush_time * (n - 1) + elapsed) / n
96
+ )
97
+ self.logger.debug(
98
+ f"Flushed {
99
+ len(messages_to_flush)} messages in {
100
+ elapsed:.3f}s")
101
+ return True
102
+
103
+ except Exception as e:
104
+ self.logger.error(f"Failed to flush batch: {e}")
105
+ # Restore messages to buffer on failure
106
+ with self.buffer_lock:
107
+ self.buffer.extend(messages_to_flush)
108
+ return False
109
+
110
+ def set_flush_callback(
111
+ self, callback: Callable[[List[Dict[str, Any]]], None]):
112
+ self.flush_callback = callback
113
+
114
+ def set_metrics_callback(self, callback: Callable[[BatchMetrics], None]):
115
+ self.metrics_callback = callback
116
+
117
+ def get_metrics(self) -> BatchMetrics:
118
+ with self.buffer_lock:
119
+ self.metrics.buffer_utilization = len(
120
+ self.buffer) / self.config.max_buffer_size
121
+ return self.metrics
122
+
123
+ def _flush_worker(self):
124
+ """Background worker for periodic flushing."""
125
+ while not self.shutdown_event.wait(self.config.flush_interval):
126
+ with self.buffer_lock:
127
+ has_data = bool(self.buffer)
128
+ if has_data:
129
+ self.flush()
130
+
131
+ def shutdown(self):
132
+ """Shutdown the batcher and flush remaining messages."""
133
+ self.shutdown_event.set()
134
+ if self.config.flush_on_shutdown:
135
+ self.flush()
136
+ if self.flush_thread.is_alive():
137
+ self.flush_thread.join(timeout=5.0)
138
+ self.logger.info("Message batcher shutdown complete")
139
+
140
+
141
+ class CSVBatchWriter:
142
+ """Batch writer for CSV files."""
143
+
144
+ def __init__(
145
+ self,
146
+ output_dir: str,
147
+ batch_size: int = 100,
148
+ format: str = "csv"):
149
+ self.output_dir = Path(output_dir)
150
+ self.output_dir.mkdir(parents=True, exist_ok=True)
151
+ self.batch_size = batch_size
152
+ self.format = format
153
+ self.logger = logging.getLogger(__name__)
154
+
155
+ self.active_files: Dict[str, Dict] = {}
156
+ self.file_locks: Dict[str, threading.Lock] = {}
157
+ self.buffers: Dict[str, List[Dict[str, Any]]] = {}
158
+ self.buffer_locks: Dict[str, threading.Lock] = {}
159
+ # Fieldnames fixed at file creation (from the first message's keys).
160
+ # Later messages with different keys are still written safely: extra
161
+ # keys are dropped, missing keys are left blank — this prevents a
162
+ # transient schema change (e.g. a sensor briefly absent) from
163
+ # crash-looping the writer or corrupting the CSV.
164
+ self.fieldnames: Dict[str, List[str]] = {}
165
+
166
+ def write_batch(self, messages: List[Dict[str, Any]]):
167
+ """Write a batch of messages, grouped by device UID."""
168
+ device_messages: Dict[str, List] = {}
169
+ for message in messages:
170
+ uid = message.get("uid", "unknown")
171
+ device_messages.setdefault(uid, []).append(message)
172
+
173
+ for uid, msgs in device_messages.items():
174
+ self._write_device_batch(uid, msgs)
175
+
176
+ def _write_device_batch(self, uid: str, messages: List[Dict[str, Any]]):
177
+ if uid not in self.active_files:
178
+ self._setup_device_file(uid, messages)
179
+
180
+ lock = self.buffer_locks.setdefault(uid, threading.Lock())
181
+ with lock:
182
+ self.buffers.setdefault(uid, []).extend(messages)
183
+ if len(self.buffers[uid]) >= self.batch_size:
184
+ self._flush_device_buffer(uid)
185
+
186
+ def _setup_device_file(self, uid: str, messages: List[Dict[str, Any]]):
187
+ """Create the CSV file and write headers using the first message."""
188
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
189
+ filename = f"log_{timestamp}_{uid}.{self.format}"
190
+ filepath = self.output_dir / filename
191
+
192
+ self.active_files[uid] = {
193
+ "filepath": filepath,
194
+ "headers_written": False,
195
+ }
196
+ self.file_locks[uid] = threading.Lock()
197
+ self.buffer_locks[uid] = threading.Lock()
198
+
199
+ # Write headers immediately using the first message so the file
200
+ # exists on disk and is non-empty even before the first flush.
201
+ if messages and self.format == "csv":
202
+ headers = list(messages[0].keys())
203
+ self.fieldnames[uid] = headers
204
+ with open(filepath, "w", newline="", encoding="utf-8") as f:
205
+ csv.DictWriter(f, fieldnames=headers).writeheader()
206
+ self.active_files[uid]["headers_written"] = True
207
+
208
+ def _flush_device_buffer(self, uid: str):
209
+ """Flush the in-memory buffer for a device to disk."""
210
+ buf = self.buffers.get(uid)
211
+ if not buf:
212
+ return
213
+
214
+ messages_to_write = buf.copy()
215
+ buf.clear()
216
+
217
+ file_info = self.active_files[uid]
218
+ with self.file_locks[uid]:
219
+ try:
220
+ if self.format == "csv":
221
+ self._write_csv_batch(uid, file_info, messages_to_write)
222
+ elif self.format == "json":
223
+ self._write_json_batch(file_info, messages_to_write)
224
+ self.logger.debug(
225
+ f"Wrote {
226
+ len(messages_to_write)} rows for {uid}")
227
+ except Exception as e:
228
+ self.logger.error(f"Failed to write batch for {uid}: {e}")
229
+ buf.extend(messages_to_write) # restore on failure
230
+
231
+ def _write_csv_batch(self, uid: str, file_info: Dict,
232
+ messages: List[Dict[str, Any]]):
233
+ filepath = file_info["filepath"]
234
+ headers = self.fieldnames.get(uid) or list(messages[0].keys())
235
+ with open(filepath, "a", newline="", encoding="utf-8") as f:
236
+ # extrasaction="ignore": drop keys not in the file's header (e.g. a
237
+ # sensor that reappeared with a new field) instead of raising.
238
+ # restval="": leave missing keys blank instead of raising.
239
+ writer = csv.DictWriter(
240
+ f,
241
+ fieldnames=headers,
242
+ extrasaction="ignore",
243
+ restval="")
244
+ if not file_info["headers_written"]:
245
+ writer.writeheader()
246
+ file_info["headers_written"] = True
247
+ writer.writerows(messages)
248
+
249
+ def _write_json_batch(self, file_info: Dict,
250
+ messages: List[Dict[str, Any]]):
251
+ filepath = file_info["filepath"]
252
+ with open(filepath, "a", encoding="utf-8") as f:
253
+ for message in messages:
254
+ f.write(json.dumps(message) + "\n")
255
+
256
+ def flush_all(self):
257
+ """Flush all device buffers to disk."""
258
+ for uid in list(self.buffers.keys()):
259
+ lock = self.buffer_locks.get(uid)
260
+ if lock:
261
+ with lock:
262
+ self._flush_device_buffer(uid)
263
+ else:
264
+ self._flush_device_buffer(uid)
265
+
266
+ def get_stats(self) -> Dict[str, Any]:
267
+ return {
268
+ "active_devices": len(self.active_files),
269
+ "total_buffered": sum(len(b) for b in self.buffers.values()),
270
+ "buffer_sizes": {uid: len(b) for uid, b in self.buffers.items()},
271
+ }
272
+
273
+
274
+ class BatchingLogger:
275
+ """High-level batching logger combining MessageBatcher and
276
+ CSVBatchWriter."""
277
+
278
+ def __init__(
279
+ self,
280
+ output_dir: str,
281
+ config: BatchConfig = None,
282
+ format: str = "csv"):
283
+ self.config = config or BatchConfig()
284
+ self.format = format
285
+ self.logger = logging.getLogger(__name__)
286
+
287
+ self.writer = CSVBatchWriter(
288
+ output_dir, self.config.batch_size, self.format)
289
+ self.batcher = MessageBatcher(self.config)
290
+ self.batcher.set_flush_callback(self.writer.write_batch)
291
+ self.batcher.set_metrics_callback(self._on_metrics_update)
292
+ self.metrics_history: List[BatchMetrics] = []
293
+
294
+ def add_message(self, message: Dict[str, Any]) -> bool:
295
+ return self.batcher.add_message(message)
296
+
297
+ def add_messages(self, messages: List[Dict[str, Any]]) -> int:
298
+ return sum(1 for m in messages if self.add_message(m))
299
+
300
+ def flush(self) -> bool:
301
+ """Flush all buffered messages to disk."""
302
+ # flushes batcher buffer -> writer.write_batch()
303
+ self.batcher.flush()
304
+ self.writer.flush_all() # flushes writer device buffers -> disk
305
+ return True
306
+
307
+ def set_flush_callback(self, callback: Callable):
308
+ """Retained for API compatibility — callback is no longer needed."""
309
+ pass
310
+
311
+ def get_metrics(self) -> BatchMetrics:
312
+ return self.batcher.get_metrics()
313
+
314
+ def _on_metrics_update(self, metrics: BatchMetrics):
315
+ self.metrics_history.append(metrics)
316
+ if len(self.metrics_history) > 1000:
317
+ self.metrics_history.pop(0)
318
+
319
+ def get_performance_summary(self) -> Dict[str, Any]:
320
+ metrics = self.get_metrics()
321
+ return {
322
+ "buffer_utilization": f"{metrics.buffer_utilization:.1%}",
323
+ "total_messages": metrics.total_messages,
324
+ "total_batches": metrics.total_batches,
325
+ "avg_flush_time_ms": metrics.avg_flush_time * 1000,
326
+ "writer_stats": self.writer.get_stats(),
327
+ }
328
+
329
+ def shutdown(self):
330
+ """Shutdown the batching logger, flushing all remaining data."""
331
+ self.batcher.shutdown()
332
+ self.writer.flush_all()
333
+ self.logger.info("Batching logger shutdown complete")
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Convenience factories
338
+ # ---------------------------------------------------------------------------
339
+
340
+ def create_csv_batcher(
341
+ output_dir: str, batch_size: int = 100, flush_interval: float = 30.0
342
+ ) -> BatchingLogger:
343
+ config = BatchConfig(batch_size=batch_size, flush_interval=flush_interval)
344
+ return BatchingLogger(output_dir, config, format="csv")
345
+
346
+
347
+ def create_json_batcher(
348
+ output_dir: str, batch_size: int = 500, flush_interval: float = 60.0
349
+ ) -> BatchingLogger:
350
+ config = BatchConfig(batch_size=batch_size, flush_interval=flush_interval)
351
+ return BatchingLogger(output_dir, config, format="json")
352
+
353
+
354
+ def create_high_frequency_batcher(
355
+ output_dir: str, batch_size: int = 10, flush_interval: float = 5.0
356
+ ) -> BatchingLogger:
357
+ config = BatchConfig(
358
+ batch_size=batch_size,
359
+ flush_interval=flush_interval,
360
+ max_buffer_size=1000)
361
+ return BatchingLogger(output_dir, config, format="csv")
@@ -0,0 +1,3 @@
1
+ # benchlab/csv_log/requirements.txt
2
+ pyserial>=3.5
3
+ benchlab-pycore>=0.5.1