From 9b55421059641d8211f3ba79557e3f14b2f5a64e Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 27 Mar 2026 05:00:00 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(gui):=20=E9=87=8D=E6=9E=84=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=B3=A2=E5=BD=A2=E7=9B=91=E6=B5=8B=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] - 此提交是上位机 GUI 的功能升级,不是缺陷修复或纯样式微调。 - 此提交将旧的单文件界面重构为模块化界面,并补齐实时波形监测所需的数据层与 mock 预览入口。 [新增功能] - 此提交新增基于 pyqtgraph 的实时波形显示、值卡、日志面板和拼贴式监测布局。 - 此提交新增 DeviceManager、MonitorStore 和 SamplePoint,用于接收周期采样流、缓存时序数据并向界面分发。 - 此提交新增 sparam-gui-mock 入口与周期推流模拟,支持在无硬件时预览界面与数据流。 [实现方案] - 此提交将 host/gui.py 拆分为 host/gui/ 包,按主窗口、侧栏、顶部栏、波形、值卡、日志和样式分离职责。 - 此提交在测试中的 Socket 模拟设备上补充周期推流能力,并新增监测缓冲测试,覆盖持续采样与 ring buffer 行为。 - 此提交补充 pyqtgraph GUI 依赖与 lockfile 更新,使 mock 预览和正式界面走同一套渲染路径。 [影响范围] - 影响 host/gui、host/sparam 和 host/tests 相关模块,以及 host/pyproject.toml 与 host/uv.lock。 - 运行方式新增 uv run sparam-gui-mock,原有 sparam-gui 入口继续保留。 - 已验证 cd host && uv run --extra test pytest -q 通过,现有主机侧测试保持绿色。 --- host/gui.py | 412 ------------------------ host/gui/__init__.py | 3 + host/gui/main.py | 18 ++ host/gui/main_window.py | 448 +++++++++++++++++++++++++++ host/gui/mock_preview.py | 64 ++++ host/gui/styles/catppuccin.py | 134 ++++++++ host/gui/widgets/log_panel.py | 18 ++ host/gui/widgets/sidebar.py | 217 +++++++++++++ host/gui/widgets/toolbar.py | 56 ++++ host/gui/widgets/value_card.py | 53 ++++ host/gui/widgets/waveform_plot.py | 80 +++++ host/pyproject.toml | 5 +- host/sparam/__init__.py | 6 + host/sparam/device_manager.py | 56 ++++ host/sparam/monitor_store.py | 45 +++ host/tests/test_monitor_store.py | 30 ++ host/tests/test_socket_simulation.py | 97 +++++- host/uv.lock | 320 ++++++++++++++++++- 18 files changed, 1636 insertions(+), 426 deletions(-) delete mode 100644 host/gui.py create mode 100644 host/gui/__init__.py create mode 100644 host/gui/main.py create mode 100644 host/gui/main_window.py create mode 100644 host/gui/mock_preview.py create mode 100644 host/gui/styles/catppuccin.py create mode 100644 host/gui/widgets/log_panel.py create mode 100644 host/gui/widgets/sidebar.py create mode 100644 host/gui/widgets/toolbar.py create mode 100644 host/gui/widgets/value_card.py create mode 100644 host/gui/widgets/waveform_plot.py create mode 100644 host/sparam/device_manager.py create mode 100644 host/sparam/monitor_store.py create mode 100644 host/tests/test_monitor_store.py diff --git a/host/gui.py b/host/gui.py deleted file mode 100644 index 4378eb5..0000000 --- a/host/gui.py +++ /dev/null @@ -1,412 +0,0 @@ -import struct -import sys -import time -from pathlib import Path -from typing import Optional - -from PySide6.QtCore import QTimer -from PySide6.QtWidgets import ( - QApplication, - QFileDialog, - QFormLayout, - QGridLayout, - QHBoxLayout, - QLabel, - QLineEdit, - QListWidget, - QMainWindow, - QMessageBox, - QPushButton, - QSpinBox, - QTextEdit, - QVBoxLayout, - QWidget, - QComboBox, - QTableWidget, - QTableWidgetItem, -) - -from sparam import DataType, Device, SerialConnection, ElfParser - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("sparam GUI (PySide6)") - self.resize(980, 640) - - self.conn: Optional[SerialConnection] = None - self.device: Optional[Device] = None - self.parser = ElfParser() - self.current_elf_path: Optional[str] = None - self.monitor_var_name: Optional[str] = None - self.monitor_timer = QTimer(self) - self.monitor_timer.timeout.connect(self.poll_monitored_var) - - self._build_ui() - self.refresh_ports() - - def _build_ui(self): - root = QWidget(self) - self.setCentralWidget(root) - layout = QVBoxLayout(root) - - conn_box = QWidget() - conn_layout = QGridLayout(conn_box) - - self.port_combo = QComboBox() - self.refresh_btn = QPushButton("Refresh") - self.refresh_btn.clicked.connect(self.refresh_ports) - - self.baud_spin = QSpinBox() - self.baud_spin.setRange(1200, 2000000) - self.baud_spin.setValue(115200) - - self.device_id_spin = QSpinBox() - self.device_id_spin.setRange(1, 255) - self.device_id_spin.setValue(1) - - self.connect_btn = QPushButton("Connect") - self.connect_btn.clicked.connect(self.toggle_connect) - - conn_layout.addWidget(QLabel("Port"), 0, 0) - conn_layout.addWidget(self.port_combo, 0, 1) - conn_layout.addWidget(self.refresh_btn, 0, 2) - conn_layout.addWidget(QLabel("Baud"), 0, 3) - conn_layout.addWidget(self.baud_spin, 0, 4) - conn_layout.addWidget(QLabel("Device ID"), 0, 5) - conn_layout.addWidget(self.device_id_spin, 0, 6) - conn_layout.addWidget(self.connect_btn, 0, 7) - - elf_box = QWidget() - elf_layout = QHBoxLayout(elf_box) - self.elf_path_edit = QLineEdit() - self.elf_path_edit.setPlaceholderText("Select .elf / .out / .map") - self.browse_btn = QPushButton("Browse") - self.browse_btn.clicked.connect(self.browse_elf) - self.load_elf_btn = QPushButton("Load Symbols") - self.load_elf_btn.clicked.connect(self.load_elf) - - elf_layout.addWidget(QLabel("ELF/MAP")) - elf_layout.addWidget(self.elf_path_edit, 1) - elf_layout.addWidget(self.browse_btn) - elf_layout.addWidget(self.load_elf_btn) - - body = QWidget() - body_layout = QHBoxLayout(body) - - left_panel = QWidget() - left_layout = QVBoxLayout(left_panel) - self.var_filter_edit = QLineEdit() - self.var_filter_edit.setPlaceholderText("Filter variable name prefix") - self.var_filter_edit.textChanged.connect(self.filter_variables) - self.var_list = QListWidget() - self.var_list.currentTextChanged.connect(self.on_var_selected) - - left_layout.addWidget(QLabel("Variables")) - left_layout.addWidget(self.var_filter_edit) - left_layout.addWidget(self.var_list, 1) - - right_panel = QWidget() - right_layout = QVBoxLayout(right_panel) - - form_host = QWidget() - form = QFormLayout(form_host) - self.var_name_label = QLabel("-") - self.var_addr_label = QLabel("-") - self.var_type_label = QLabel("-") - - self.value_edit = QLineEdit() - self.value_edit.setPlaceholderText("Value") - self.dtype_combo = QComboBox() - self.dtype_combo.addItems([dt.name.lower() for dt in DataType]) - - form.addRow("Name", self.var_name_label) - form.addRow("Address", self.var_addr_label) - form.addRow("Detected Type", self.var_type_label) - form.addRow("Write Type", self.dtype_combo) - form.addRow("Value", self.value_edit) - - action_row = QWidget() - action_layout = QHBoxLayout(action_row) - self.read_btn = QPushButton("Read") - self.write_btn = QPushButton("Write") - self.monitor_btn = QPushButton("Start Periodic") - self.stop_monitor_btn = QPushButton("Stop") - self.monitor_interval_ms = QSpinBox() - self.monitor_interval_ms.setRange(50, 5000) - self.monitor_interval_ms.setValue(200) - self.monitor_interval_ms.setSuffix(" ms") - self.read_btn.clicked.connect(self.read_selected) - self.write_btn.clicked.connect(self.write_selected) - self.monitor_btn.clicked.connect(self.start_periodic_read) - self.stop_monitor_btn.clicked.connect(self.stop_periodic_read) - action_layout.addWidget(self.read_btn) - action_layout.addWidget(self.write_btn) - action_layout.addWidget(self.monitor_interval_ms) - action_layout.addWidget(self.monitor_btn) - action_layout.addWidget(self.stop_monitor_btn) - - self.monitor_table = QTableWidget(0, 3) - self.monitor_table.setHorizontalHeaderLabels(["Time", "Variable", "Value"]) - - self.log_view = QTextEdit() - self.log_view.setReadOnly(True) - - right_layout.addWidget(QLabel("Selected Variable")) - right_layout.addWidget(form_host) - right_layout.addWidget(action_row) - right_layout.addWidget(QLabel("Periodic Read List")) - right_layout.addWidget(self.monitor_table, 1) - right_layout.addWidget(QLabel("Log")) - right_layout.addWidget(self.log_view, 1) - - body_layout.addWidget(left_panel, 1) - body_layout.addWidget(right_panel, 1) - - layout.addWidget(conn_box) - layout.addWidget(elf_box) - layout.addWidget(body, 1) - - def _log(self, text: str): - self.log_view.append(text) - - def refresh_ports(self): - self.port_combo.clear() - for port in SerialConnection.list_ports(): - self.port_combo.addItem(port) - if self.port_combo.count() == 0: - self._log("No serial ports found.") - - def toggle_connect(self): - if self.conn and self.conn.is_open(): - self.conn.close() - self.conn = None - self.device = None - self.connect_btn.setText("Connect") - self._log("Disconnected.") - return - - port = self.port_combo.currentText().strip() - if not port: - QMessageBox.warning(self, "Connection", "Please select a serial port.") - return - - conn = SerialConnection(port, self.baud_spin.value(), timeout=1.0) - if not conn.open(): - reason = conn.last_error or "port busy or unavailable" - self._log(f"CONNECT FAIL: unable to open {port} ({reason})") - QMessageBox.critical(self, "Connection", f"Failed to open {port}.") - return - - device = Device(conn, self.device_id_spin.value(), elf_parser=self.parser) - if not device.ping(timeout=1.0): - conn.close() - reason = device.last_error or "ping timeout" - self._log(f"CONNECT FAIL: {reason}") - QMessageBox.warning(self, "Connection", "Ping failed. Check device id and cable.") - return - - self.conn = conn - self.device = device - self.connect_btn.setText("Disconnect") - self._log(f"Connected to {port} (baud={self.baud_spin.value()}).") - - def browse_elf(self): - path, _ = QFileDialog.getOpenFileName( - self, - "Open ELF or MAP", - str(Path.cwd()), - "ELF/MAP Files (*.elf *.out *.map);;All Files (*)", - ) - if path: - self.elf_path_edit.setText(path) - - def load_elf(self): - filepath = self.elf_path_edit.text().strip() - if not filepath: - QMessageBox.warning(self, "Load Symbols", "Please choose an ELF/MAP file.") - return - - try: - if filepath.endswith((".elf", ".out")): - variables = self.parser.parse_elf(filepath) - elif filepath.endswith(".map"): - variables = self.parser.parse_map(filepath) - else: - variables = self.parser.parse(filepath) - except Exception as exc: - QMessageBox.critical(self, "Load Symbols", f"Failed to parse file: {exc}") - return - - self.current_elf_path = filepath - self.var_list.clear() - for v in variables: - self.var_list.addItem(v.name) - - self._log(f"Loaded {len(variables)} variables from {Path(filepath).name}.") - if not self.device: - self._log("Symbol-only mode: connect a device to enable Read/Write.") - - def filter_variables(self, text: str): - prefix = text.strip() - for i in range(self.var_list.count()): - item = self.var_list.item(i) - item.setHidden(bool(prefix) and not item.text().startswith(prefix)) - - def on_var_selected(self, name: str): - if not name: - return - - var = self.parser.get_variable(name) - if not var: - return - - self.var_name_label.setText(var.name) - self.var_addr_label.setText(f"0x{var.address:08X}") - self.var_type_label.setText(var.var_type) - self.dtype_combo.setCurrentText(self._dtype_to_name(var.dtype_code)) - - @staticmethod - def _dtype_to_name(dtype_code: int) -> str: - try: - return DataType(dtype_code).name.lower() - except ValueError: - return DataType.UINT32.name.lower() - - def _selected_variable(self): - name = self.var_list.currentItem().text() if self.var_list.currentItem() else "" - return self.parser.get_variable(name) if name else None - - def read_selected(self): - var = self._selected_variable() - if not var: - self._log("READ FAIL: no variable selected") - QMessageBox.warning(self, "Read", "Please select a variable first.") - return - if not self.device: - self._log("READ FAIL: no device connected") - QMessageBox.warning(self, "Read", "No device connected. Load symbols works offline, Read needs a serial device.") - return - - self.device.stop_monitor() - - value = self.device.read_value(var, timeout=1.0) - if value is None: - self._log(f"READ FAIL {var.name}: {self.device.last_error or 'unknown error'}") - QMessageBox.warning(self, "Read", "Read failed.") - return - - dtype = DataType(var.dtype_code) if var.dtype_code in [d.value for d in DataType] else DataType.UINT32 - try: - decoded = struct.unpack(dtype.format_char, value[: dtype.size])[0] - self.value_edit.setText(str(decoded)) - self._log(f"READ {var.name} = {decoded}") - except Exception: - hex_value = value.hex() - self.value_edit.setText(hex_value) - self._log(f"READ {var.name} = 0x{hex_value}") - - def write_selected(self): - var = self._selected_variable() - if not var: - self._log("WRITE FAIL: no variable selected") - QMessageBox.warning(self, "Write", "Please select a variable first.") - return - if not self.device: - self._log("WRITE FAIL: no device connected") - QMessageBox.warning(self, "Write", "No device connected. Load symbols works offline, Write needs a serial device.") - return - - self.device.stop_monitor() - - raw_text = self.value_edit.text().strip() - if not raw_text: - self._log("WRITE FAIL: empty value") - QMessageBox.warning(self, "Write", "Please input a value.") - return - - dtype_name = self.dtype_combo.currentText().upper() - dtype = DataType[dtype_name] - - try: - if dtype == DataType.FLOAT: - payload = struct.pack(dtype.format_char, float(raw_text)) - else: - payload = struct.pack(dtype.format_char, int(raw_text, 0)) - except Exception as exc: - self._log(f"WRITE FAIL {var.name}: invalid value {raw_text} ({exc})") - QMessageBox.warning(self, "Write", f"Invalid value: {exc}") - return - - if self.device.write_single(var, payload, timeout=1.0, dtype_override=dtype): - self._log(f"WRITE {var.name} <= {raw_text} ({dtype.name})") - else: - self._log(f"WRITE FAIL {var.name}: {self.device.last_error or 'unknown error'}") - QMessageBox.warning(self, "Write", "Write failed.") - - def start_periodic_read(self): - var = self._selected_variable() - if not var: - self._log("MONITOR FAIL: no variable selected") - return - if not self.device: - self._log("MONITOR FAIL: no device connected") - return - - self.monitor_var_name = var.name - self.monitor_timer.start(self.monitor_interval_ms.value()) - self._log( - f"MONITOR START {var.name} every {self.monitor_interval_ms.value()} ms" - ) - - def stop_periodic_read(self): - if self.monitor_timer.isActive(): - self.monitor_timer.stop() - self._log("MONITOR STOP") - - def poll_monitored_var(self): - if not self.monitor_var_name or not self.device: - return - - var = self.parser.get_variable(self.monitor_var_name) - if not var: - self._log("MONITOR FAIL: variable not found in parser") - self.stop_periodic_read() - return - - value = self.device.read_value(var, timeout=0.5) - if value is None: - self._log(f"MONITOR FAIL {var.name}: {self.device.last_error or 'unknown error'}") - return - - dtype = DataType(var.dtype_code) if var.dtype_code in [d.value for d in DataType] else DataType.UINT32 - try: - decoded = struct.unpack(dtype.format_char, value[: dtype.size])[0] - shown = str(decoded) - except Exception: - shown = "0x" + value.hex() - - row = self.monitor_table.rowCount() - self.monitor_table.insertRow(row) - self.monitor_table.setItem(row, 0, QTableWidgetItem(time.strftime("%H:%M:%S"))) - self.monitor_table.setItem(row, 1, QTableWidgetItem(var.name)) - self.monitor_table.setItem(row, 2, QTableWidgetItem(shown)) - self.monitor_table.scrollToBottom() - - def closeEvent(self, event): - self.monitor_timer.stop() - if self.conn and self.conn.is_open(): - self.conn.close() - super().closeEvent(event) - - -def run_gui(): - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec()) - - -if __name__ == "__main__": - run_gui() \ No newline at end of file diff --git a/host/gui/__init__.py b/host/gui/__init__.py new file mode 100644 index 0000000..4cc14c4 --- /dev/null +++ b/host/gui/__init__.py @@ -0,0 +1,3 @@ +from .main import run_gui + +__all__ = ["run_gui"] diff --git a/host/gui/main.py b/host/gui/main.py new file mode 100644 index 0000000..f7c1eaa --- /dev/null +++ b/host/gui/main.py @@ -0,0 +1,18 @@ +import sys + +from PySide6.QtWidgets import QApplication + +from .main_window import MainWindow +from .styles.catppuccin import build_stylesheet + + +def run_gui(): + app = QApplication(sys.argv) + app.setStyleSheet(build_stylesheet()) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + run_gui() diff --git a/host/gui/main_window.py b/host/gui/main_window.py new file mode 100644 index 0000000..62dc4d7 --- /dev/null +++ b/host/gui/main_window.py @@ -0,0 +1,448 @@ +import csv +from pathlib import Path +from typing import Dict, List, Optional + +from PySide6.QtCore import QObject, Qt, Signal +from PySide6.QtWidgets import ( + QFileDialog, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QMainWindow, + QMessageBox, + QScrollArea, + QSplitter, + QVBoxLayout, + QWidget, +) + +from sparam import DataType, Device, DeviceManager, ElfParser, MonitorStore, SerialConnection + +from .styles.catppuccin import SERIES_COLORS +from .widgets.log_panel import LogPanel +from .widgets.sidebar import Sidebar +from .widgets.toolbar import Toolbar +from .widgets.value_card import ValueCard +from .widgets.waveform_plot import WaveformPlot + + +class DeviceBridge(QObject): + sample_received = Signal(str, float, float) + + def emit_sample(self, sample): + self.sample_received.emit(sample.name, sample.timestamp, sample.value) + + +class MainWindow(QMainWindow): + RATE_OPTIONS = { + "1 ms": 1, + "5 ms": 2, + "10 ms": 3, + "20 ms": 4, + "50 ms": 5, + "100 ms": 6, + "200 ms": 7, + "500 ms": 8, + } + WINDOW_OPTIONS = { + "5 s": 5.0, + "10 s": 10.0, + "30 s": 30.0, + "Infinite": None, + } + + def __init__(self): + super().__init__() + self.setWindowTitle("sparam") + self.resize(1360, 860) + + self.parser = ElfParser() + self.current_symbol_path: Optional[str] = None + self.conn: Optional[SerialConnection] = None + self.device: Optional[Device] = None + self.device_manager: Optional[DeviceManager] = None + self.bridge: Optional[DeviceBridge] = None + self.store = MonitorStore(max_points=1200) + self.monitored_names: List[str] = [] + self.cards: Dict[str, ValueCard] = {} + self.monitor_active = False + self.monitor_paused = False + self.connection_fields: Dict[str, QLabel] = {} + self.monitor_fields: Dict[str, QLabel] = {} + + self._build_ui() + self._refresh_ports() + + def _build_ui(self): + root = QWidget(self) + self.setCentralWidget(root) + layout = QVBoxLayout(root) + layout.setContentsMargins(18, 18, 18, 18) + layout.setSpacing(14) + + self.toolbar = Toolbar() + + splitter = QSplitter(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + + self.sidebar = Sidebar() + self.sidebar.refresh_requested.connect(self._refresh_ports) + self.sidebar.connect_requested.connect(self._toggle_connection) + self.sidebar.load_symbols_requested.connect(self._browse_symbols) + self.sidebar.pause_requested.connect(self._toggle_pause) + self.sidebar.export_png_requested.connect(self._export_png) + self.sidebar.export_csv_requested.connect(self._export_csv) + self.sidebar.window_changed.connect(self._set_time_window) + self.sidebar.rate_changed.connect(self._handle_rate_change) + self.sidebar.variable_activated.connect(self._toggle_variable_monitor) + self.sidebar.selection_changed.connect(self._preview_variable) + + main_panel = QWidget() + main_layout = QGridLayout(main_panel) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setHorizontalSpacing(14) + main_layout.setVerticalSpacing(14) + + self.waveform = WaveformPlot() + connection_card, self.connection_fields = self._create_summary_card( + "Connection Overview", + "Current transport and symbol source", + ["Port", "Baud", "Device", "Symbols"], + ) + monitor_card, self.monitor_fields = self._create_summary_card( + "Monitor Session", + "Live sampling session information", + ["Variables", "Rate", "Window", "Mode"], + ) + + cards_shell = QFrame() + cards_shell.setObjectName("cardShelf") + cards_layout = QVBoxLayout(cards_shell) + cards_layout.setContentsMargins(12, 12, 12, 12) + cards_layout.setSpacing(10) + cards_title = QLabel("Live Values") + cards_title.setProperty("muted", True) + cards_layout.addWidget(cards_title) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll_contents = QWidget() + self.cards_layout = QHBoxLayout(scroll_contents) + self.cards_layout.setContentsMargins(0, 0, 0, 0) + self.cards_layout.setSpacing(10) + self.cards_layout.addStretch(1) + scroll.setWidget(scroll_contents) + cards_layout.addWidget(scroll) + + self.log_panel = LogPanel() + + main_layout.addWidget(self.waveform, 0, 0, 2, 2) + main_layout.addWidget(connection_card, 0, 2) + main_layout.addWidget(monitor_card, 1, 2) + main_layout.addWidget(cards_shell, 2, 0, 1, 2) + main_layout.addWidget(self.log_panel, 2, 2) + main_layout.setColumnStretch(0, 3) + main_layout.setColumnStretch(1, 2) + main_layout.setColumnStretch(2, 2) + main_layout.setRowStretch(0, 4) + main_layout.setRowStretch(1, 3) + main_layout.setRowStretch(2, 2) + + splitter.addWidget(self.sidebar) + splitter.addWidget(main_panel) + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 4) + + layout.addWidget(self.toolbar) + layout.addWidget(splitter, 1) + self._refresh_summary_cards() + + def _create_summary_card(self, title: str, subtitle: str, field_names: List[str]): + card = QFrame() + card.setObjectName("summaryCard") + layout = QVBoxLayout(card) + layout.setContentsMargins(14, 14, 14, 14) + layout.setSpacing(10) + + title_label = QLabel(title) + title_label.setProperty("sectionTitle", True) + subtitle_label = QLabel(subtitle) + subtitle_label.setProperty("muted", True) + + layout.addWidget(title_label) + layout.addWidget(subtitle_label) + + fields: Dict[str, QLabel] = {} + for name in field_names: + row = QHBoxLayout() + row.setContentsMargins(0, 0, 0, 0) + key = QLabel(name) + key.setProperty("muted", True) + value = QLabel("--") + value.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + row.addWidget(key) + row.addWidget(value, 1) + layout.addLayout(row) + fields[name] = value + + layout.addStretch(1) + return card, fields + + def _log(self, message: str): + self.log_panel.append_line(message) + + def _refresh_ports(self): + ports = SerialConnection.list_ports() + self.sidebar.set_ports(ports) + if not ports: + self._log("No serial ports found.") + self._refresh_summary_cards() + + def _browse_symbols(self): + path, _ = QFileDialog.getOpenFileName( + self, + "Open ELF or MAP", + str(Path.cwd()), + "ELF/MAP Files (*.elf *.out *.map);;All Files (*)", + ) + if path: + self._load_symbols(path) + + def _load_symbols(self, filepath: str): + try: + variables = self.parser.parse(filepath) + except Exception as exc: + QMessageBox.critical(self, "Load Symbols", f"Failed to parse file: {exc}") + return + + self.current_symbol_path = filepath + self.sidebar.set_variables(variables) + self._log(f"Loaded {len(variables)} variables from {Path(filepath).name}.") + self._refresh_summary_cards() + + def _toggle_connection(self): + if self.conn and self.conn.is_open(): + self._disconnect_device() + return + + port = self.sidebar.current_port() + if not port: + QMessageBox.warning(self, "Connection", "Please select a serial port.") + return + + conn = SerialConnection( + port, + self.sidebar.current_baudrate(), + timeout=1.0, + ) + if not conn.open(): + reason = conn.last_error or "port busy or unavailable" + self._log(f"CONNECT FAIL: unable to open {port} ({reason})") + QMessageBox.critical(self, "Connection", f"Failed to open {port}.") + return + + device = Device(conn, self.sidebar.current_device_id(), elf_parser=self.parser) + if not device.ping(timeout=1.0): + conn.close() + reason = device.last_error or "ping timeout" + self._log(f"CONNECT FAIL: {reason}") + QMessageBox.warning(self, "Connection", "Ping failed. Check device id and cable.") + return + + self.conn = conn + self.device = device + self.device_manager = DeviceManager(device) + self.bridge = DeviceBridge() + self.device_manager.add_callback(self.bridge.emit_sample) + self.bridge.sample_received.connect(self._on_sample_received) + self.toolbar.set_connected(True) + self.sidebar.set_connected(True) + self._log(f"Connected to {port} (baud={self.sidebar.current_baudrate()}).") + self._refresh_summary_cards() + self._restart_monitoring_if_needed() + + def _disconnect_device(self): + if self.device_manager: + self.device_manager.stop_monitor() + if self.conn and self.conn.is_open(): + self.conn.close() + self.conn = None + self.device = None + self.device_manager = None + self.bridge = None + self.monitor_active = False + self.toolbar.set_connected(False) + self.sidebar.set_connected(False) + self._log("Disconnected.") + self._refresh_summary_cards() + + def _preview_variable(self, name: str): + variable = self.parser.get_variable(name) + if not variable: + return + self.toolbar.set_status_text( + f"{variable.name} 0x{variable.address:08X} {variable.var_type}" + ) + + def _toggle_variable_monitor(self, name: str): + variable = self.parser.get_variable(name) + if not variable: + return + + if name in self.monitored_names: + self.monitored_names = [item for item in self.monitored_names if item != name] + self.sidebar.set_monitored(name, False) + self.waveform.remove_variable(name) + self._remove_card(name) + self._log(f"Removed {name} from monitor.") + else: + self.monitored_names.append(name) + color = self._series_color_for(name) + self.sidebar.set_monitored(name, True) + self.waveform.add_variable(name, color) + self._ensure_card(name, color) + self._log(f"Added {name} to monitor.") + + self._refresh_summary_cards() + self._restart_monitoring_if_needed() + + def _series_color_for(self, name: str) -> str: + if name in self.monitored_names: + index = self.monitored_names.index(name) + else: + index = len(self.monitored_names) + return SERIES_COLORS[index % len(SERIES_COLORS)] + + def _ensure_card(self, name: str, color: str): + if name in self.cards: + return + card = ValueCard(name, color) + self.cards[name] = card + self.cards_layout.insertWidget(max(0, self.cards_layout.count() - 1), card) + + def _remove_card(self, name: str): + card = self.cards.pop(name, None) + if card is None: + return + card.setParent(None) + card.deleteLater() + + def _toggle_pause(self): + self.monitor_paused = not self.monitor_paused + self.waveform.set_paused(self.monitor_paused) + self.toolbar.set_paused(self.monitor_paused) + self.sidebar.set_paused(self.monitor_paused) + self._log("Monitor paused." if self.monitor_paused else "Monitor resumed.") + self._refresh_summary_cards() + + def _set_time_window(self, label: str): + self.waveform.set_time_window(self.WINDOW_OPTIONS[label]) + self._refresh_summary_cards() + + def _handle_rate_change(self, _label: str): + self._refresh_summary_cards() + self._restart_monitoring_if_needed() + + def _restart_monitoring_if_needed(self): + if not self.device_manager or not self.monitored_names: + if self.device_manager: + self.device_manager.stop_monitor() + self.monitor_active = False + self._refresh_summary_cards() + return + + variables = [ + self.parser.get_variable(name) + for name in self.monitored_names + if self.parser.get_variable(name) + ] + if not variables: + return + + self.device_manager.stop_monitor() + self.monitor_active = self.device_manager.start_monitor( + variables, + self.RATE_OPTIONS[self.sidebar.current_rate_label()], + ) + if self.monitor_active: + self._log( + f"Streaming {len(variables)} variable(s) at {self.sidebar.current_rate_label()}." + ) + elif self.device: + self._log(f"MONITOR FAIL: {self.device.last_error or 'unknown error'}") + self._refresh_summary_cards() + + def _on_sample_received(self, name: str, timestamp: float, value: float): + if self.monitor_paused: + return + self.store.append(name, timestamp, value) + card = self.cards.get(name) + if card: + card.update_value(value) + self.waveform.update_data(name, timestamp, value) + self._refresh_summary_cards() + + def _export_png(self): + path, _ = QFileDialog.getSaveFileName( + self, + "Export Waveform PNG", + str(Path.cwd() / "waveform.png"), + "PNG Files (*.png)", + ) + if not path: + return + try: + self.waveform.export_png(path) + self._log(f"Exported waveform PNG to {path}.") + except Exception as exc: + QMessageBox.warning(self, "Export PNG", f"Failed to export PNG: {exc}") + + def _export_csv(self): + path, _ = QFileDialog.getSaveFileName( + self, + "Export Waveform CSV", + str(Path.cwd() / "waveform.csv"), + "CSV Files (*.csv)", + ) + if not path: + return + try: + with open(path, "w", newline="", encoding="utf-8") as handle: + writer = csv.writer(handle) + writer.writerow(["timestamp", "name", "value"]) + writer.writerows(self.store.export_rows()) + self._log(f"Exported waveform CSV to {path}.") + except Exception as exc: + QMessageBox.warning(self, "Export CSV", f"Failed to export CSV: {exc}") + + def closeEvent(self, event): + if self.device_manager: + self.device_manager.stop_monitor() + if self.conn and self.conn.is_open(): + self.conn.close() + super().closeEvent(event) + + def _refresh_summary_cards(self): + self.connection_fields["Port"].setText( + self.sidebar.current_port() or "Not selected" + ) + self.connection_fields["Baud"].setText(str(self.sidebar.current_baudrate())) + self.connection_fields["Device"].setText(str(self.sidebar.current_device_id())) + self.connection_fields["Symbols"].setText( + Path(self.current_symbol_path).name if self.current_symbol_path else "None" + ) + + self.monitor_fields["Variables"].setText(str(len(self.monitored_names))) + self.monitor_fields["Rate"].setText(self.sidebar.current_rate_label()) + self.monitor_fields["Window"].setText(self.sidebar.window_combo.currentText()) + if self.monitor_paused: + mode = "Paused" + elif self.monitor_active: + mode = "Streaming" + elif self.device: + mode = "Armed" + else: + mode = "Offline" + self.monitor_fields["Mode"].setText(mode) diff --git a/host/gui/mock_preview.py b/host/gui/mock_preview.py new file mode 100644 index 0000000..adf4248 --- /dev/null +++ b/host/gui/mock_preview.py @@ -0,0 +1,64 @@ +import math +import sys +import time + +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QApplication + +from sparam.elf_parser import Variable + +from .main_window import MainWindow +from .styles.catppuccin import build_stylesheet + + +class MockPreviewController: + def __init__(self, window: MainWindow): + self.window = window + self.start_time = time.time() + self.tick = 0 + self.variables = [ + Variable("motor_speed", 0x20000000, 4, "uint32_t"), + Variable("motor_current", 0x20000004, 4, "uint32_t"), + Variable("pid_kp", 0x20000008, 4, "float"), + ] + self.timer = QTimer(window) + self.timer.timeout.connect(self.push_samples) + self._setup_window() + + def _setup_window(self): + self.window.parser.variables = {item.name: item for item in self.variables} + self.window.sidebar.set_variables(self.variables) + self.window.toolbar.set_status_text("Mock preview mode: synthetic waveform data") + self.window._log("Loaded mock variables for UI preview.") + for variable in self.variables: + self.window._toggle_variable_monitor(variable.name) + self.window.monitor_active = True + self.window._log("Mock preview stream started.") + + def start(self): + self.timer.start(50) + + def push_samples(self): + elapsed = time.time() - self.start_time + self.tick += 1 + speed = 1200 + 280 * math.sin(elapsed * 1.7) + (self.tick % 6) * 4 + current = 42 + 9 * math.sin(elapsed * 2.4 + 0.6) + kp = 1.5 + 0.08 * math.sin(elapsed * 0.45) + + self.window._on_sample_received("motor_speed", time.time(), float(speed)) + self.window._on_sample_received("motor_current", time.time(), float(current)) + self.window._on_sample_received("pid_kp", time.time(), float(kp)) + + +def run_mock_preview(): + app = QApplication(sys.argv) + app.setStyleSheet(build_stylesheet()) + window = MainWindow() + controller = MockPreviewController(window) + controller.start() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + run_mock_preview() diff --git a/host/gui/styles/catppuccin.py b/host/gui/styles/catppuccin.py new file mode 100644 index 0000000..5a57874 --- /dev/null +++ b/host/gui/styles/catppuccin.py @@ -0,0 +1,134 @@ +BACKGROUND = "#f3f4f6" +SIDEBAR = "#f8fafc" +CARD = "#fcfcfd" +CARD_ALT = "#f8fafc" +INPUT_BG = "#f8fafb" +BORDER = "#e5e7eb" +TEXT = "#111827" +MUTED = "#6b7280" +ACCENT = "#1f2937" +SUCCESS = "#4b7a5a" +WARNING = "#9a6b16" +ERROR = "#b25d5d" + +SERIES_COLORS = [ + "#2563eb", + "#0f766e", + "#7c3aed", + "#ea580c", + "#dc2626", +] + + +def build_stylesheet() -> str: + return f""" + QWidget {{ + background: {BACKGROUND}; + color: {TEXT}; + font-family: Monospace; + font-size: 13px; + }} + QMainWindow {{ + background: {BACKGROUND}; + }} + QFrame#toolbar, + QFrame#sidebar, + QFrame#sectionCard, + QFrame#summaryCard, + QWidget#plotPanel, + QFrame#cardShelf, + QFrame#valueCard, + QFrame#logPanel {{ + background: {CARD}; + border: 1px solid {BORDER}; + border-radius: 8px; + }} + QListWidget, + QLineEdit, + QTextEdit, + QComboBox, + QSpinBox, + QPushButton {{ + background: {INPUT_BG}; + color: {TEXT}; + border: 1px solid {BORDER}; + border-radius: 6px; + padding: 7px 10px; + }} + QPushButton:hover {{ + background: {CARD_ALT}; + border-color: #d1d5db; + }} + QPushButton[accent="true"] {{ + background: {ACCENT}; + color: {CARD}; + border-color: {ACCENT}; + font-weight: 700; + }} + QPushButton:pressed {{ + background: #eef2f7; + }} + QPushButton[accent="true"]:pressed {{ + background: #374151; + }} + QPushButton[semantic="success"] {{ + color: {SUCCESS}; + }} + QPushButton[semantic="warning"] {{ + color: {WARNING}; + }} + QPushButton[semantic="error"] {{ + color: {ERROR}; + }} + QLabel[muted="true"] {{ + color: {MUTED}; + }} + QLabel[hero="true"] {{ + font-size: 24px; + font-weight: 700; + letter-spacing: 0.5px; + }} + QLabel[chip="true"] {{ + background: {INPUT_BG}; + border: 1px solid {BORDER}; + border-radius: 999px; + padding: 4px 10px; + font-size: 12px; + }} + QLabel[chip="true"][state="connected"] {{ + color: {SUCCESS}; + border-color: #d6eadc; + }} + QLabel[chip="true"][state="warning"] {{ + color: {WARNING}; + border-color: #eadcb6; + }} + QLabel[sectionTitle="true"] {{ + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + }} + QListWidget::item:selected {{ + background: {CARD_ALT}; + border: 1px solid {BORDER}; + color: {TEXT}; + border-radius: 4px; + }} + QListWidget::item {{ + padding: 6px 8px; + }} + QScrollArea {{ + border: none; + }} + QComboBox::drop-down, + QSpinBox::up-button, + QSpinBox::down-button {{ + border: none; + background: transparent; + }} + QSplitter::handle {{ + background: {BACKGROUND}; + width: 8px; + }} + """ diff --git a/host/gui/widgets/log_panel.py b/host/gui/widgets/log_panel.py new file mode 100644 index 0000000..841c44a --- /dev/null +++ b/host/gui/widgets/log_panel.py @@ -0,0 +1,18 @@ +from time import strftime + +from PySide6.QtWidgets import QFrame, QTextEdit, QVBoxLayout + + +class LogPanel(QFrame): + def __init__(self): + super().__init__() + self.setObjectName("logPanel") + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + + self.text_edit = QTextEdit() + self.text_edit.setReadOnly(True) + layout.addWidget(self.text_edit) + + def append_line(self, message: str): + self.text_edit.append(f"{strftime('%H:%M:%S')} {message}") diff --git a/host/gui/widgets/sidebar.py b/host/gui/widgets/sidebar.py new file mode 100644 index 0000000..f33568c --- /dev/null +++ b/host/gui/widgets/sidebar.py @@ -0,0 +1,217 @@ +from typing import Iterable + +from PySide6.QtCore import Signal +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QComboBox, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QPushButton, + QSpinBox, + QVBoxLayout, + QWidget, +) + + +class Sidebar(QFrame): + refresh_requested = Signal() + connect_requested = Signal() + load_symbols_requested = Signal() + pause_requested = Signal() + export_png_requested = Signal() + export_csv_requested = Signal() + window_changed = Signal(str) + rate_changed = Signal(str) + variable_activated = Signal(str) + selection_changed = Signal(str) + + def __init__(self): + super().__init__() + self.setObjectName("sidebar") + self.setFixedWidth(258) + + layout = QVBoxLayout(self) + layout.setContentsMargins(14, 14, 14, 14) + layout.setSpacing(12) + + layout.addWidget(self._build_connection_section()) + layout.addWidget(self._build_monitor_section()) + layout.addWidget(self._build_export_section()) + layout.addWidget(self._build_variable_section(), 1) + + def _build_connection_section(self): + section = self._section_shell("Transport") + body = section.layout().itemAt(1).widget().layout() + + self.port_combo = QComboBox() + self.baud_spin = QSpinBox() + self.baud_spin.setRange(1200, 2_000_000) + self.baud_spin.setValue(115200) + self.device_id_spin = QSpinBox() + self.device_id_spin.setRange(1, 255) + self.device_id_spin.setValue(1) + + self.connect_btn = QPushButton("Connect") + self.connect_btn.setProperty("accent", True) + self.refresh_btn = QPushButton("Refresh") + self.load_symbols_btn = QPushButton("Symbols") + + self.connect_btn.clicked.connect(self.connect_requested.emit) + self.refresh_btn.clicked.connect(self.refresh_requested.emit) + self.load_symbols_btn.clicked.connect(self.load_symbols_requested.emit) + + body.addWidget(self._field("Port", self.port_combo)) + body.addWidget(self._field("Baud", self.baud_spin)) + body.addWidget(self._field("Device", self.device_id_spin)) + + row = QHBoxLayout() + row.setSpacing(8) + row.addWidget(self.connect_btn, 1) + row.addWidget(self.refresh_btn, 1) + body.addLayout(row) + body.addWidget(self.load_symbols_btn) + return section + + def _build_monitor_section(self): + section = self._section_shell("Monitor") + body = section.layout().itemAt(1).widget().layout() + + self.rate_combo = QComboBox() + self.rate_combo.addItems(["10 ms", "20 ms", "50 ms", "100 ms", "200 ms", "500 ms"]) + self.rate_combo.setCurrentText("10 ms") + self.rate_combo.currentTextChanged.connect(self.rate_changed.emit) + + self.window_combo = QComboBox() + self.window_combo.addItems(["5 s", "10 s", "30 s", "Infinite"]) + self.window_combo.currentTextChanged.connect(self.window_changed.emit) + + self.pause_btn = QPushButton("Pause") + self.pause_btn.clicked.connect(self.pause_requested.emit) + + body.addWidget(self._field("Rate", self.rate_combo)) + body.addWidget(self._field("Window", self.window_combo)) + body.addWidget(self.pause_btn) + return section + + def _build_export_section(self): + section = self._section_shell("Capture") + body = section.layout().itemAt(1).widget().layout() + + self.export_png_btn = QPushButton("PNG Snapshot") + self.export_csv_btn = QPushButton("CSV Export") + self.export_png_btn.clicked.connect(self.export_png_requested.emit) + self.export_csv_btn.clicked.connect(self.export_csv_requested.emit) + + body.addWidget(self.export_png_btn) + body.addWidget(self.export_csv_btn) + return section + + def _build_variable_section(self): + section = self._section_shell("Variables") + body = section.layout().itemAt(1).widget().layout() + + helper = QLabel("Double-click to pin a symbol into the board.") + helper.setProperty("muted", True) + self.filter_edit = QLineEdit() + self.filter_edit.setPlaceholderText("Search variables") + self.filter_edit.textChanged.connect(self._apply_filter) + + self.list_widget = QListWidget() + self.list_widget.itemDoubleClicked.connect( + lambda item: self.variable_activated.emit(item.data(1) or item.text()) + ) + self.list_widget.currentItemChanged.connect(self._on_current_changed) + + body.addWidget(helper) + body.addWidget(self.filter_edit) + body.addWidget(self.list_widget, 1) + return section + + def _section_shell(self, title: str): + shell = QFrame() + shell.setObjectName("sectionCard") + outer = QVBoxLayout(shell) + outer.setContentsMargins(12, 12, 12, 12) + outer.setSpacing(10) + + header = QLabel(title) + header.setProperty("sectionTitle", True) + content = QWidget() + content_layout = QVBoxLayout(content) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(8) + + outer.addWidget(header) + outer.addWidget(content) + return shell + + def _field(self, label: str, control): + wrap = QWidget() + layout = QVBoxLayout(wrap) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + caption = QLabel(label) + caption.setProperty("muted", True) + layout.addWidget(caption) + layout.addWidget(control) + return wrap + + def set_variables(self, variables: Iterable): + self.list_widget.clear() + for variable in sorted(variables, key=lambda item: item.name): + item = QListWidgetItem(variable.name) + item.setData(1, variable.name) + self.list_widget.addItem(item) + + def set_monitored(self, name: str, monitored: bool): + for index in range(self.list_widget.count()): + item = self.list_widget.item(index) + if item.data(1) == name: + item.setText(name) + item.setData(1, name) + font = QFont(item.font()) + font.setBold(monitored) + item.setFont(font) + break + + def set_ports(self, ports: Iterable[str]): + current = self.port_combo.currentText() + self.port_combo.clear() + self.port_combo.addItems(list(ports)) + if current: + index = self.port_combo.findText(current) + if index >= 0: + self.port_combo.setCurrentIndex(index) + + def current_port(self) -> str: + return self.port_combo.currentText().strip() + + def current_baudrate(self) -> int: + return self.baud_spin.value() + + def current_device_id(self) -> int: + return self.device_id_spin.value() + + def current_rate_label(self) -> str: + return self.rate_combo.currentText() + + def set_connected(self, connected: bool): + self.connect_btn.setText("Disconnect" if connected else "Connect") + + def set_paused(self, paused: bool): + self.pause_btn.setText("Resume" if paused else "Pause") + + def _apply_filter(self, text: str): + prefix = text.strip().lower() + for index in range(self.list_widget.count()): + item = self.list_widget.item(index) + name = item.data(1) or item.text() + item.setHidden(bool(prefix) and prefix not in name.lower()) + + def _on_current_changed(self, current: QListWidgetItem, _previous: QListWidgetItem): + if current: + self.selection_changed.emit(current.data(1) or current.text()) diff --git a/host/gui/widgets/toolbar.py b/host/gui/widgets/toolbar.py new file mode 100644 index 0000000..d446729 --- /dev/null +++ b/host/gui/widgets/toolbar.py @@ -0,0 +1,56 @@ +from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QWidget + + +class Toolbar(QFrame): + def __init__(self): + super().__init__() + self.setObjectName("toolbar") + + layout = QHBoxLayout(self) + layout.setContentsMargins(18, 12, 18, 12) + layout.setSpacing(16) + + brand_wrap = QWidget() + brand_layout = QHBoxLayout(brand_wrap) + brand_layout.setContentsMargins(0, 0, 0, 0) + brand_layout.setSpacing(10) + + self.brand = QLabel("sparam") + self.brand.setProperty("hero", True) + self.caption = QLabel("serial tuning monitor") + self.caption.setProperty("muted", True) + + brand_layout.addWidget(self.brand) + brand_layout.addWidget(self.caption) + brand_layout.addStretch(1) + + self.state_chip = QLabel("Idle") + self.state_chip.setProperty("chip", True) + self.status_label = QLabel("Pick a symbol file and start a monitor session.") + self.status_label.setProperty("muted", True) + + layout.addWidget(brand_wrap, 1) + layout.addWidget(self.state_chip) + layout.addWidget(self.status_label, 2) + + def set_status_text(self, text: str): + self.status_label.setText(text) + + def set_connected(self, connected: bool): + self.state_chip.setText("Connected" if connected else "Offline") + self.state_chip.setProperty("state", "connected" if connected else "idle") + self.style().unpolish(self.state_chip) + self.style().polish(self.state_chip) + + def set_paused(self, paused: bool): + if paused: + self.state_chip.setText("Paused") + self.state_chip.setProperty("state", "warning") + else: + current_state = self.state_chip.property("state") + if current_state == "connected": + self.state_chip.setText("Connected") + elif current_state == "idle": + self.state_chip.setText("Offline") + self.style().unpolish(self.state_chip) + self.style().polish(self.state_chip) diff --git a/host/gui/widgets/value_card.py b/host/gui/widgets/value_card.py new file mode 100644 index 0000000..f60041a --- /dev/null +++ b/host/gui/widgets/value_card.py @@ -0,0 +1,53 @@ +from PySide6.QtWidgets import QFrame, QLabel, QHBoxLayout, QVBoxLayout, QWidget + +from ..styles.catppuccin import ERROR, MUTED, SUCCESS + + +class ValueCard(QFrame): + def __init__(self, name: str, color: str): + super().__init__() + self.setObjectName("valueCard") + self._last_value = None + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + stripe = QWidget() + stripe.setFixedWidth(2) + stripe.setStyleSheet( + f"background: {color}; border-top-left-radius: 8px; border-bottom-left-radius: 8px;" + ) + + body = QWidget() + body_layout = QVBoxLayout(body) + body_layout.setContentsMargins(14, 12, 14, 12) + body_layout.setSpacing(6) + + self.name_label = QLabel(name) + self.value_label = QLabel("--") + self.value_label.setStyleSheet("font-size: 23px; font-weight: 650;") + self.delta_label = QLabel("Waiting for data") + self.delta_label.setProperty("muted", True) + self.delta_label.setStyleSheet("font-size: 12px;") + + body_layout.addWidget(self.name_label) + body_layout.addWidget(self.value_label) + body_layout.addWidget(self.delta_label) + + layout.addWidget(stripe) + layout.addWidget(body) + + def update_value(self, value: float): + self.value_label.setText(f"{value:.3f}") + if self._last_value is None: + self.delta_label.setText("First sample") + self.delta_label.setStyleSheet(f"color: {MUTED}; font-size: 12px;") + else: + delta = value - self._last_value + direction = "up" if delta >= 0 else "down" + self.delta_label.setText(f"{direction} {delta:+.3f}") + self.delta_label.setStyleSheet( + f"color: {SUCCESS if delta >= 0 else ERROR}; font-size: 12px;" + ) + self._last_value = value diff --git a/host/gui/widgets/waveform_plot.py b/host/gui/widgets/waveform_plot.py new file mode 100644 index 0000000..d0b9dee --- /dev/null +++ b/host/gui/widgets/waveform_plot.py @@ -0,0 +1,80 @@ +from typing import Dict, Optional + +import pyqtgraph as pg +from pyqtgraph.exporters import ImageExporter +from PySide6.QtWidgets import QVBoxLayout, QWidget + + +class WaveformPlot(QWidget): + def __init__(self): + super().__init__() + self.setObjectName("plotPanel") + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + + self.plot_widget = pg.PlotWidget() + self.plot_widget.setBackground("#fcfcfd") + self.plot_widget.showGrid(x=True, y=True, alpha=0.05) + self.plot_widget.setLabel("left", "Value") + self.plot_widget.setLabel("bottom", "Time", units="s") + self.plot_widget.addLegend(offset=(8, 8)) + self.plot_widget.getAxis("left").setTextPen("#6b7280") + self.plot_widget.getAxis("bottom").setTextPen("#6b7280") + self.plot_widget.getAxis("left").setPen("#d1d5db") + self.plot_widget.getAxis("bottom").setPen("#d1d5db") + layout.addWidget(self.plot_widget) + + self._curves: Dict[str, pg.PlotCurveItem] = {} + self._timestamps: Dict[str, list] = {} + self._values: Dict[str, list] = {} + self._time_window: Optional[float] = 10.0 + self._paused = False + + def add_variable(self, name: str, color: str): + if name in self._curves: + return + curve = self.plot_widget.plot(name=name, pen=pg.mkPen(color=color, width=1.6)) + self._curves[name] = curve + self._timestamps[name] = [] + self._values[name] = [] + + def remove_variable(self, name: str): + curve = self._curves.pop(name, None) + if curve is not None: + self.plot_widget.removeItem(curve) + self._timestamps.pop(name, None) + self._values.pop(name, None) + + def update_data(self, name: str, timestamp: float, value: float): + if self._paused or name not in self._curves: + return + + timestamps = self._timestamps[name] + values = self._values[name] + timestamps.append(timestamp) + values.append(value) + + if self._time_window is not None: + cutoff = timestamp - self._time_window + while timestamps and timestamps[0] < cutoff: + timestamps.pop(0) + values.pop(0) + + if not timestamps: + return + + origin = timestamps[0] + self._curves[name].setData( + [item - origin for item in timestamps], + values, + ) + + def set_time_window(self, seconds: Optional[float]): + self._time_window = seconds + + def set_paused(self, paused: bool): + self._paused = paused + + def export_png(self, filepath: str): + exporter = ImageExporter(self.plot_widget.plotItem) + exporter.export(filepath) diff --git a/host/pyproject.toml b/host/pyproject.toml index c92c4f6..09164b1 100644 --- a/host/pyproject.toml +++ b/host/pyproject.toml @@ -12,13 +12,14 @@ dependencies = [ ] [project.optional-dependencies] -gui = ["PySide6>=6.6"] +gui = ["PySide6>=6.6", "pyqtgraph>=0.13"] test = ["pytest>=8.0"] [project.scripts] sparam = "cli:main" sparam-gui = "cli:launch_gui" +sparam-gui-mock = "gui.mock_preview:run_mock_preview" [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" diff --git a/host/sparam/__init__.py b/host/sparam/__init__.py index f4a7c36..f72ec9b 100644 --- a/host/sparam/__init__.py +++ b/host/sparam/__init__.py @@ -3,6 +3,8 @@ from .serial_conn import SerialConnection from .socket_conn import SocketConnection from .device import Device, Variable +from .device_manager import DeviceManager, SamplePoint +from .monitor_store import MonitorStore, TimeSeries __all__ = [ "Protocol", @@ -14,6 +16,10 @@ "SocketConnection", "Device", "Variable", + "DeviceManager", + "SamplePoint", + "MonitorStore", + "TimeSeries", ] __version__ = "0.1.0" diff --git a/host/sparam/device_manager.py b/host/sparam/device_manager.py new file mode 100644 index 0000000..916672b --- /dev/null +++ b/host/sparam/device_manager.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass +import time +from typing import Callable, List, Optional + +from .device import Device +from .protocol import DataType + + +@dataclass +class SamplePoint: + name: str + timestamp: float + value: float + + +class DeviceManager: + def __init__(self, device: Device): + self.device = device + self._callbacks: List[Callable[[SamplePoint], None]] = [] + self._receiving_started = False + + def add_callback(self, callback: Callable[[SamplePoint], None]) -> None: + self._callbacks.append(callback) + + def remove_callback(self, callback: Callable[[SamplePoint], None]) -> None: + self._callbacks = [item for item in self._callbacks if item != callback] + + def start_monitor(self, variables, rate: int) -> bool: + if ( + not self._receiving_started + and hasattr(self.device.connection, "start_receive") + ): + self.device.connection.start_receive(self.device.on_frame_received) + self._receiving_started = True + + return self.device.start_monitor(variables, rate, self._on_data) + + def stop_monitor(self) -> bool: + return self.device.stop_monitor() + + def _on_data(self, name: str, raw_value: bytes) -> None: + variable = self.device.get_variable(name) + if variable and variable.dtype_code: + try: + value = Device.bytes_to_value( + raw_value[: DataType(variable.dtype_code).size], + DataType(variable.dtype_code), + ) + except Exception: + value = float(int.from_bytes(raw_value[:4], "little", signed=False)) + else: + value = float(int.from_bytes(raw_value[:4], "little", signed=False)) + + sample = SamplePoint(name=name, timestamp=time.time(), value=float(value)) + for callback in list(self._callbacks): + callback(sample) diff --git a/host/sparam/monitor_store.py b/host/sparam/monitor_store.py new file mode 100644 index 0000000..a889bf6 --- /dev/null +++ b/host/sparam/monitor_store.py @@ -0,0 +1,45 @@ +from collections import deque +from dataclasses import dataclass +from typing import Deque, Dict, List, Tuple + + +@dataclass +class TimeSeries: + timestamps: List[float] + values: List[float] + + +class MonitorStore: + def __init__(self, max_points: int = 1000): + self.max_points = max_points + self._timestamps: Dict[str, Deque[float]] = {} + self._values: Dict[str, Deque[float]] = {} + + def append(self, name: str, timestamp: float, value: float) -> None: + if name not in self._timestamps: + self._timestamps[name] = deque(maxlen=self.max_points) + self._values[name] = deque(maxlen=self.max_points) + + self._timestamps[name].append(timestamp) + self._values[name].append(value) + + def series(self, name: str) -> TimeSeries: + return TimeSeries( + timestamps=list(self._timestamps.get(name, [])), + values=list(self._values.get(name, [])), + ) + + def latest_value(self, name: str): + values = self._values.get(name) + if not values: + return None + return values[-1] + + def export_rows(self) -> List[Tuple[float, str, float]]: + rows: List[Tuple[float, str, float]] = [] + for name in self._timestamps: + timestamps = self._timestamps[name] + values = self._values[name] + rows.extend(zip(timestamps, [name] * len(timestamps), values)) + rows.sort(key=lambda item: item[0]) + return rows diff --git a/host/tests/test_monitor_store.py b/host/tests/test_monitor_store.py new file mode 100644 index 0000000..09e9192 --- /dev/null +++ b/host/tests/test_monitor_store.py @@ -0,0 +1,30 @@ +from sparam.monitor_store import MonitorStore + + +def test_monitor_store_keeps_latest_points_with_ring_buffer_limit(): + store = MonitorStore(max_points=3) + + store.append("motor_speed", 1.0, 10.0) + store.append("motor_speed", 2.0, 20.0) + store.append("motor_speed", 3.0, 30.0) + store.append("motor_speed", 4.0, 40.0) + + series = store.series("motor_speed") + + assert series.timestamps == [2.0, 3.0, 4.0] + assert series.values == [20.0, 30.0, 40.0] + assert store.latest_value("motor_speed") == 40.0 + + +def test_monitor_store_exports_rows_in_timestamp_order(): + store = MonitorStore(max_points=5) + + store.append("speed", 1.0, 100.0) + store.append("current", 1.5, 20.0) + store.append("speed", 2.0, 110.0) + + assert store.export_rows() == [ + (1.0, "speed", 100.0), + (1.5, "current", 20.0), + (2.0, "speed", 110.0), + ] diff --git a/host/tests/test_socket_simulation.py b/host/tests/test_socket_simulation.py index 9a6e310..7a3e5d4 100644 --- a/host/tests/test_socket_simulation.py +++ b/host/tests/test_socket_simulation.py @@ -1,9 +1,11 @@ import socket import struct import threading +import time from sparam import Device, SocketConnection, Protocol, CommandType from sparam.elf_parser import Variable +from sparam.device_manager import DeviceManager class SimulatedDeviceServer: @@ -20,18 +22,26 @@ def __init__(self, host: str = "127.0.0.1", port: int = 0, device_id: int = 1): self.memory = { 0x20000000: struct.pack("= 3 + assert all(sample.name == "motor_speed" for sample in samples) + assert all(isinstance(sample.timestamp, float) for sample in samples) + assert samples[-1].value > samples[0].value diff --git a/host/uv.lock b/host/uv.lock index b404b1f..fae3103 100644 --- a/host/uv.lock +++ b/host/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.8" resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", "python_full_version < '3.9'", ] @@ -28,7 +29,8 @@ name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, @@ -84,13 +86,254 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "numpy" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/9b/027bec52c633f6556dba6b722d9a0befb40498b9ceddd29cbe67a45a127c/numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", size = 10911229, upload-time = "2023-06-26T13:39:33.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/80/6cdfb3e275d95155a34659163b83c09e3a3ff9f1456880bec6cc63d71083/numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", size = 19789140, upload-time = "2023-06-26T13:22:33.184Z" }, + { url = "https://files.pythonhosted.org/packages/64/5f/3f01d753e2175cfade1013eea08db99ba1ee4bdb147ebcf3623b75d12aa7/numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", size = 13854297, upload-time = "2023-06-26T13:22:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b3/2f9c21d799fa07053ffa151faccdceeb69beec5a010576b8991f614021f7/numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", size = 13995611, upload-time = "2023-06-26T13:23:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/10/be/ae5bf4737cb79ba437879915791f6f26d92583c738d7d960ad94e5c36adf/numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", size = 17282357, upload-time = "2023-06-26T13:23:51.446Z" }, + { url = "https://files.pythonhosted.org/packages/c0/64/908c1087be6285f40e4b3e79454552a701664a079321cff519d8c7051d06/numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", size = 12429222, upload-time = "2023-06-26T13:24:13.849Z" }, + { url = "https://files.pythonhosted.org/packages/22/55/3d5a7c1142e0d9329ad27cece17933b0e2ab4e54ddc5c1861fbfeb3f7693/numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", size = 14841514, upload-time = "2023-06-26T13:24:38.129Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cc/5ed2280a27e5dab12994c884f1f4d8c3bd4d885d02ae9e52a9d213a6a5e2/numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", size = 19775508, upload-time = "2023-06-26T13:25:08.882Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bc/77635c657a3668cf652806210b8662e1aff84b818a55ba88257abf6637a8/numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", size = 13840033, upload-time = "2023-06-26T13:25:33.417Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4c/96cdaa34f54c05e97c1c50f39f98d608f96f0677a6589e64e53104e22904/numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", size = 13991951, upload-time = "2023-06-26T13:25:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/22/97/dfb1a31bb46686f09e68ea6ac5c63fdee0d22d7b23b8f3f7ea07712869ef/numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", size = 17278923, upload-time = "2023-06-26T13:26:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/35/e2/76a11e54139654a324d107da1d98f99e7aa2a7ef97cfd7c631fba7dbde71/numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", size = 12422446, upload-time = "2023-06-26T13:26:49.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ec/ebef2f7d7c28503f958f0f8b992e7ce606fb74f9e891199329d5f5f87404/numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", size = 14834466, upload-time = "2023-06-26T13:27:16.029Z" }, + { url = "https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", size = 19780722, upload-time = "2023-06-26T13:27:49.573Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", size = 13843102, upload-time = "2023-06-26T13:28:12.288Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/2586a50ad72e8dbb1d8381f837008a0321a3516dfd7cb57fc8cf7e4bb06b/numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", size = 14039616, upload-time = "2023-06-26T13:28:35.659Z" }, + { url = "https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", size = 17316263, upload-time = "2023-06-26T13:29:09.272Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/8d328f0b91c733aa9aa7ee540dbc49b58796c862b4fbcb1146c701e888da/numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", size = 12455660, upload-time = "2023-06-26T13:29:33.434Z" }, + { url = "https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", size = 14868112, upload-time = "2023-06-26T13:29:58.385Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/d5b0402b801c8a8b56b04c1e85c6165efab298d2f0ab741c2406516ede3a/numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", size = 19816549, upload-time = "2023-06-26T13:30:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/638aaa446f39113a3ed38b37a66243e21b38110d021bfcb940c383e120f2/numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", size = 13879950, upload-time = "2023-06-26T13:31:01.787Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/91894916e50627476cff1a4e4363ab6179d01077d71b9afed41d9e1f18bf/numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9", size = 14030228, upload-time = "2023-06-26T13:31:26.696Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/d7b2a0417af6428440c0ad7cb9799073e507b1a465f827d058b826236964/numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", size = 17311170, upload-time = "2023-06-26T13:31:56.615Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/e02ace5d7dfccee796c37b995c63322674daf88ae2f4a4724c5dd0afcc91/numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", size = 12454918, upload-time = "2023-06-26T13:32:16.8Z" }, + { url = "https://files.pythonhosted.org/packages/63/38/6cc19d6b8bfa1d1a459daf2b3fe325453153ca7019976274b6f33d8b5663/numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", size = 14867441, upload-time = "2023-06-26T13:32:40.521Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fd/8dff40e25e937c94257455c237b9b6bf5a30d42dd1cc11555533be099492/numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", size = 19156590, upload-time = "2023-06-26T13:33:10.36Z" }, + { url = "https://files.pythonhosted.org/packages/42/e7/4bf953c6e05df90c6d351af69966384fed8e988d0e8c54dad7103b59f3ba/numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", size = 16705744, upload-time = "2023-06-26T13:33:36.703Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/9106005eb477d022b60b3817ed5937a43dad8fd1f20b0610ea8a32fcb407/numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", size = 14734290, upload-time = "2023-06-26T13:34:05.409Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -117,7 +360,8 @@ name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } @@ -143,6 +387,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyqtgraph" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/b4/3008b3a367251653de280f424b80811a8746a9a6cdf55689d5f619e64b5f/pyqtgraph-0.13.3.tar.gz", hash = "sha256:58108d8411c7054e0841d8b791ee85e101fc296b9b359c0e01dde38a98ff2ace", size = 1398392, upload-time = "2023-04-14T21:31:30.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/57/0a096b8949d0ee5ca32de180f19240ddd5a81015a27c6f2e7342b9044d45/pyqtgraph-0.13.3-py3-none-any.whl", hash = "sha256:fdcc04ac4b32a7bedf1bf3cf74cbb93ab3ba5687791712bbfa8d0712377d2f2b", size = 960972, upload-time = "2023-04-14T21:31:28.425Z" }, +] + +[[package]] +name = "pyqtgraph" +version = "0.13.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/d9/b62d5cddb3caa6e5145664bee5ed90223dee23ca887ed3ee479f2609e40a/pyqtgraph-0.13.7.tar.gz", hash = "sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3", size = 2343380, upload-time = "2024-04-29T02:18:58.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/34/5702b3b7cafe99be1d94b42f100e8cc5e6957b761fcb1cf5f72d492851da/pyqtgraph-0.13.7-py3-none-any.whl", hash = "sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a", size = 1925473, upload-time = "2024-04-29T02:18:56.206Z" }, +] + +[[package]] +name = "pyqtgraph" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, +] + [[package]] name = "pyserial" version = "3.5" @@ -196,7 +487,8 @@ name = "pyside6" version = "6.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "pyside6-addons", version = "6.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -253,7 +545,8 @@ name = "pyside6-addons" version = "6.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "pyside6-essentials", version = "6.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -307,7 +600,8 @@ name = "pyside6-essentials" version = "6.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "shiboken6", version = "6.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -366,7 +660,8 @@ name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, @@ -416,7 +711,8 @@ name = "shiboken6" version = "6.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] wheels = [ { url = "https://files.pythonhosted.org/packages/82/1d/b56b7b694fbc871496435488d1f41c5068de546334850d722756511cef65/shiboken6-6.11.0-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:d88e8a1eb705f2b9ad21db08a61ae1dc0c773e5cd86a069de0754c4cf1f9b43b", size = 476085, upload-time = "2026-03-23T12:47:05.724Z" }, @@ -440,6 +736,9 @@ dependencies = [ [package.optional-dependencies] gui = [ + { name = "pyqtgraph", version = "0.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyqtgraph", version = "0.13.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pyqtgraph", version = "0.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyside6", version = "6.6.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pyside6", version = "6.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "pyside6", version = "6.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -455,6 +754,7 @@ requires-dist = [ { name = "click", specifier = ">=8.0" }, { name = "crcmod", specifier = ">=1.7" }, { name = "pyelftools", specifier = ">=0.29" }, + { name = "pyqtgraph", marker = "extra == 'gui'", specifier = ">=0.13" }, { name = "pyserial", specifier = ">=3.5" }, { name = "pyside6", marker = "extra == 'gui'", specifier = ">=6.6" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, @@ -532,7 +832,7 @@ name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } From 4ca0a9781c5d94c7a9b621a3ee855554a126d345 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 27 Mar 2026 05:00:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(host):=20=E5=BC=95=E5=85=A5=20Python?= =?UTF-8?q?=20=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] - 此提交是主机侧工程的维护性建设,不是新功能开发或缺陷修复。 - 此提交在项目仍然较小时引入统一的 lint、format 与 type-check 工具链,并同步清理现有代码。 [维护内容] - 此提交新增 Ruff、Mypy、.editorconfig、VS Code 任务与 README 开发说明。 - 此提交补充 host 目录的格式化、静态检查、类型检查与测试命令,并完善缓存目录忽略规则。 - 此提交将 host 侧现有模块补齐必要类型标注,修正连接抽象、GUI 方法签名与测试模拟类型。 [实现方案] - 此提交在 host/pyproject.toml 中增加 dev extra,并集中配置 Ruff 与 Mypy 规则。 - 此提交更新 .vscode/tasks.json、host/README.md 与 .gitignore,使本地开发、验证与编辑器入口保持一致。 - 此提交调整 cli.py、host/gui、host/sparam 与 tests 中的类型边界与异常处理,确保工具链在现有代码上直接可用。 [影响范围] - 影响 host 目录全部 Python 模块、开发配置文件与 lockfile。 - 本地质量门禁已验证通过:ruff format、ruff check、mypy、pytest 全部绿色。 - 运行时行为保持兼容,新增的是开发规范与静态质量保障能力。 --- .editorconfig | 11 ++ .gitignore | 5 + .vscode/tasks.json | 82 +++++++++ host/README.md | 8 +- host/cli.py | 57 +++--- host/gui/main.py | 2 +- host/gui/main_window.py | 86 +++++---- host/gui/mock_preview.py | 14 +- host/gui/widgets/log_panel.py | 4 +- host/gui/widgets/sidebar.py | 58 ++++-- host/gui/widgets/toolbar.py | 8 +- host/gui/widgets/value_card.py | 14 +- host/gui/widgets/waveform_plot.py | 20 +- host/pyproject.toml | 49 +++++ host/sparam/__init__.py | 8 +- host/sparam/device.py | 53 +++--- host/sparam/device_manager.py | 12 +- host/sparam/elf_parser.py | 17 +- host/sparam/monitor_store.py | 4 +- host/sparam/protocol.py | 10 +- host/sparam/serial_conn.py | 45 +++-- host/sparam/socket_conn.py | 26 +-- host/tests/test_monitor_store.py | 4 +- host/tests/test_socket_simulation.py | 42 +++-- host/uv.lock | 262 ++++++++++++++++++++++++++- 25 files changed, 696 insertions(+), 205 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c3882fa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*.{py,c,h,lua,md,json,toml,yml,yaml}] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index b77b205..b632fd0 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,12 @@ dkms.conf *.dwo /host/sparam/__pycache__ /host/tests/__pycache__ +/host/gui/__pycache__ +/host/gui/styles/__pycache__ +/host/gui/widgets/__pycache__ .vscode/c_cpp_properties.json .vscode/compile_commands.json /host/__pycache__ +/host/.pytest_cache/ +/host/.venv/ /.xmake diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a585a8c..bf8e830 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,88 @@ { "version": "2.0.0", "tasks": [ + { + "label": "sparam: host format", + "type": "shell", + "command": "uv", + "args": [ + "run", + "--extra", + "dev", + "ruff", + "format", + "." + ], + "options": { + "cwd": "${workspaceFolder}/host" + }, + "problemMatcher": [] + }, + { + "label": "sparam: host lint", + "type": "shell", + "command": "uv", + "args": [ + "run", + "--extra", + "dev", + "ruff", + "check", + "." + ], + "options": { + "cwd": "${workspaceFolder}/host" + }, + "problemMatcher": [] + }, + { + "label": "sparam: host type-check", + "type": "shell", + "command": "uv", + "args": [ + "run", + "--extra", + "dev", + "mypy", + "cli.py", + "gui", + "sparam", + "tests" + ], + "options": { + "cwd": "${workspaceFolder}/host" + }, + "problemMatcher": [] + }, + { + "label": "sparam: host quality", + "type": "shell", + "command": "echo Host quality checks done", + "dependsOn": [ + "sparam: host format", + "sparam: host lint", + "sparam: host type-check", + "sparam: host test" + ], + "dependsOrder": "sequence", + "problemMatcher": [] + }, + { + "label": "sparam: host test", + "type": "shell", + "command": "uv", + "args": [ + "run", + "--extra", + "test", + "pytest", + "-q" + ], + "options": { + "cwd": "${workspaceFolder}/host" + }, + "problemMatcher": [] + }, { "label": "sparam: demo build", "type": "shell", diff --git a/host/README.md b/host/README.md index 6480513..cd45bac 100644 --- a/host/README.md +++ b/host/README.md @@ -4,14 +4,20 @@ Python host tools for sparam, including protocol encoding/decoding, device commu ## Development -- Install dependencies (core + test + gui): `uv sync --extra test --extra gui` +- Install dependencies (core + dev + test + gui): `uv sync --extra dev --extra test --extra gui` +- Format code: `uv run --extra dev ruff format .` +- Run linter: `uv run --extra dev ruff check .` +- Run type checks: `uv run --extra dev mypy cli.py gui sparam tests` - Run tests: `uv run --extra test pytest -q` +- Run the full local quality gate: + `uv run --extra dev ruff format . && uv run --extra dev ruff check . && uv run --extra dev mypy cli.py gui sparam tests && uv run --extra test pytest -q` ## GUI (PySide6) - If you already ran `uv sync` only, add GUI deps with: `uv sync --extra gui` - Launch GUI by script: `uv run sparam-gui` - Or launch from CLI command: `uv run sparam gui` +- Launch the synthetic preview window: `uv run --extra gui sparam-gui-mock` The GUI currently supports: diff --git a/host/cli.py b/host/cli.py index ee60abd..bea332b 100644 --- a/host/cli.py +++ b/host/cli.py @@ -1,30 +1,32 @@ -import click +import csv import struct import sys -from typing import Optional +import time +from typing import Optional, TextIO, Tuple + +import click from sparam import ( - SerialConnection, + DataType, Device, ElfParser, - DataType, Protocol, + SerialConnection, ) @click.group() @click.pass_context -def main(ctx): +def main(ctx: click.Context) -> None: ctx.ensure_object(dict) -def launch_gui(): +def launch_gui() -> None: try: from gui import run_gui except ImportError as exc: click.echo( - "GUI dependencies are missing: " - f"{exc}. Install with `uv sync --extra gui`.", + f"GUI dependencies are missing: {exc}. Install with `uv sync --extra gui`.", err=True, ) sys.exit(1) @@ -33,7 +35,7 @@ def launch_gui(): @main.command() -def list_ports(): +def list_ports() -> None: ports = SerialConnection.list_ports() for port in ports: click.echo(port) @@ -43,7 +45,7 @@ def list_ports(): @click.argument("filepath", type=click.Path(exists=True)) @click.option("--prefix", "-p", default=None, help="Filter variables by prefix") @click.option("--size", "-s", default=0, help="Filter by minimum size") -def parse_elf(filepath: str, prefix: Optional[str], size: int): +def parse_elf(filepath: str, prefix: Optional[str], size: int) -> None: parser = ElfParser() variables = parser.parse(filepath) @@ -64,7 +66,7 @@ def parse_elf(filepath: str, prefix: Optional[str], size: int): @click.option("--baud", "-b", default=115200, help="Baud rate") @click.option("--device-id", "-d", default=1, help="Device ID") @click.option("--timeout", "-t", default=1.0, help="Timeout in seconds") -def ping(port: str, baud: int, device_id: int, timeout: float): +def ping(port: str, baud: int, device_id: int, timeout: float) -> None: conn = SerialConnection(port, baud, timeout) if not conn.open(): click.echo("Failed to open port", err=True) @@ -89,7 +91,14 @@ def ping(port: str, baud: int, device_id: int, timeout: float): "--var", "-v", multiple=True, required=True, help="Variable names to read" ) @click.option("--timeout", "-t", default=1.0, help="Timeout in seconds") -def read(port: str, baud: int, device_id: int, elf: str, var: tuple, timeout: float): +def read( + port: str, + baud: int, + device_id: int, + elf: str, + var: Tuple[str, ...], + timeout: float, +) -> None: conn = SerialConnection(port, baud, timeout) if not conn.open(): click.echo("Failed to open port", err=True) @@ -120,7 +129,7 @@ def read(port: str, baud: int, device_id: int, elf: str, var: tuple, timeout: fl dtype = DataType(v.dtype_code) val = struct.unpack(dtype.format_char, value)[0] click.echo(f"{name} = {val}") - except: + except (struct.error, ValueError): click.echo(f"{name} = {value.hex()}") else: click.echo(f"{name} = {value.hex()}") @@ -153,7 +162,7 @@ def write( value: float, var_type: str, timeout: float, -): +) -> None: type_map = { "uint8": DataType.UINT8, "int8": DataType.INT8, @@ -198,7 +207,7 @@ def write( @click.option("--baud", "-b", default=115200, help="Baud rate") @click.option("--device-id", "-d", default=1, help="Device ID") @click.option("--timeout", default=1.0, help="Timeout in seconds") -def stop(port: str, baud: int, device_id: int, timeout: float): +def stop(port: str, baud: int, device_id: int, timeout: float) -> None: conn = SerialConnection(port, baud, timeout) if not conn.open(): click.echo("Failed to open port", err=True) @@ -222,7 +231,9 @@ def stop(port: str, baud: int, device_id: int, timeout: float): "-r", default=3, type=click.IntRange(1, 8), - help="Sample rate (1=1ms, 2=5ms, 3=10ms, 4=20ms, 5=50ms, 6=100ms, 7=200ms, 8=500ms)", + help=( + "Sample rate (1=1ms, 2=5ms, 3=10ms, 4=20ms, 5=50ms, 6=100ms, 7=200ms, 8=500ms)" + ), ) @click.option("--output", "-o", default=None, help="Output CSV file") @click.option("--count", "-c", default=0, help="Number of samples (0 for infinite)") @@ -235,10 +246,7 @@ def monitor( rate: int, output: Optional[str], count: int, -): - import time - import csv - +) -> None: conn = SerialConnection(port, baud, 1.0) if not conn.open(): click.echo("Failed to open port", err=True) @@ -260,8 +268,7 @@ def monitor( conn.close() sys.exit(1) - csv_file = None - csv_writer = None + csv_file: Optional[TextIO] = None if output: csv_file = open(output, "w", newline="") csv_writer = csv.writer(csv_file) @@ -270,7 +277,7 @@ def monitor( sample_count = 0 running = True - def on_data(name: str, value: bytes): + def on_data(name: str, value: bytes) -> None: nonlocal sample_count, running v = device.get_variable(name) if v and v.dtype_code: @@ -278,7 +285,7 @@ def on_data(name: str, value: bytes): dtype = DataType(v.dtype_code) val = struct.unpack(dtype.format_char, value)[0] click.echo(f"{name} = {val}") - except: + except (struct.error, ValueError): click.echo(f"{name} = {value.hex()}") else: click.echo(f"{name} = {value.hex()}") @@ -306,7 +313,7 @@ def on_data(name: str, value: bytes): @main.command() -def gui(): +def gui() -> None: """Launch the desktop GUI.""" launch_gui() diff --git a/host/gui/main.py b/host/gui/main.py index f7c1eaa..d5ec4a3 100644 --- a/host/gui/main.py +++ b/host/gui/main.py @@ -6,7 +6,7 @@ from .styles.catppuccin import build_stylesheet -def run_gui(): +def run_gui() -> None: app = QApplication(sys.argv) app.setStyleSheet(build_stylesheet()) window = MainWindow() diff --git a/host/gui/main_window.py b/host/gui/main_window.py index 62dc4d7..2aaa7db 100644 --- a/host/gui/main_window.py +++ b/host/gui/main_window.py @@ -1,8 +1,9 @@ import csv from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from PySide6.QtCore import QObject, Qt, Signal +from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import ( QFileDialog, QFrame, @@ -17,7 +18,14 @@ QWidget, ) -from sparam import DataType, Device, DeviceManager, ElfParser, MonitorStore, SerialConnection +from sparam import ( + Device, + DeviceManager, + ElfParser, + MonitorStore, + SamplePoint, + SerialConnection, +) from .styles.catppuccin import SERIES_COLORS from .widgets.log_panel import LogPanel @@ -30,7 +38,7 @@ class DeviceBridge(QObject): sample_received = Signal(str, float, float) - def emit_sample(self, sample): + def emit_sample(self, sample: SamplePoint) -> None: self.sample_received.emit(sample.name, sample.timestamp, sample.value) @@ -52,7 +60,7 @@ class MainWindow(QMainWindow): "Infinite": None, } - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("sparam") self.resize(1360, 860) @@ -74,7 +82,7 @@ def __init__(self): self._build_ui() self._refresh_ports() - def _build_ui(self): + def _build_ui(self) -> None: root = QWidget(self) self.setCentralWidget(root) layout = QVBoxLayout(root) @@ -159,7 +167,9 @@ def _build_ui(self): layout.addWidget(splitter, 1) self._refresh_summary_cards() - def _create_summary_card(self, title: str, subtitle: str, field_names: List[str]): + def _create_summary_card( + self, title: str, subtitle: str, field_names: List[str] + ) -> Tuple[QFrame, Dict[str, QLabel]]: card = QFrame() card.setObjectName("summaryCard") layout = QVBoxLayout(card) @@ -181,7 +191,9 @@ def _create_summary_card(self, title: str, subtitle: str, field_names: List[str] key = QLabel(name) key.setProperty("muted", True) value = QLabel("--") - value.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + value.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + ) row.addWidget(key) row.addWidget(value, 1) layout.addLayout(row) @@ -190,17 +202,17 @@ def _create_summary_card(self, title: str, subtitle: str, field_names: List[str] layout.addStretch(1) return card, fields - def _log(self, message: str): + def _log(self, message: str) -> None: self.log_panel.append_line(message) - def _refresh_ports(self): + def _refresh_ports(self) -> None: ports = SerialConnection.list_ports() self.sidebar.set_ports(ports) if not ports: self._log("No serial ports found.") self._refresh_summary_cards() - def _browse_symbols(self): + def _browse_symbols(self) -> None: path, _ = QFileDialog.getOpenFileName( self, "Open ELF or MAP", @@ -210,7 +222,7 @@ def _browse_symbols(self): if path: self._load_symbols(path) - def _load_symbols(self, filepath: str): + def _load_symbols(self, filepath: str) -> None: try: variables = self.parser.parse(filepath) except Exception as exc: @@ -222,7 +234,7 @@ def _load_symbols(self, filepath: str): self._log(f"Loaded {len(variables)} variables from {Path(filepath).name}.") self._refresh_summary_cards() - def _toggle_connection(self): + def _toggle_connection(self) -> None: if self.conn and self.conn.is_open(): self._disconnect_device() return @@ -248,7 +260,9 @@ def _toggle_connection(self): conn.close() reason = device.last_error or "ping timeout" self._log(f"CONNECT FAIL: {reason}") - QMessageBox.warning(self, "Connection", "Ping failed. Check device id and cable.") + QMessageBox.warning( + self, "Connection", "Ping failed. Check device id and cable." + ) return self.conn = conn @@ -263,7 +277,7 @@ def _toggle_connection(self): self._refresh_summary_cards() self._restart_monitoring_if_needed() - def _disconnect_device(self): + def _disconnect_device(self) -> None: if self.device_manager: self.device_manager.stop_monitor() if self.conn and self.conn.is_open(): @@ -278,7 +292,7 @@ def _disconnect_device(self): self._log("Disconnected.") self._refresh_summary_cards() - def _preview_variable(self, name: str): + def _preview_variable(self, name: str) -> None: variable = self.parser.get_variable(name) if not variable: return @@ -286,13 +300,15 @@ def _preview_variable(self, name: str): f"{variable.name} 0x{variable.address:08X} {variable.var_type}" ) - def _toggle_variable_monitor(self, name: str): + def _toggle_variable_monitor(self, name: str) -> None: variable = self.parser.get_variable(name) if not variable: return if name in self.monitored_names: - self.monitored_names = [item for item in self.monitored_names if item != name] + self.monitored_names = [ + item for item in self.monitored_names if item != name + ] self.sidebar.set_monitored(name, False) self.waveform.remove_variable(name) self._remove_card(name) @@ -315,21 +331,21 @@ def _series_color_for(self, name: str) -> str: index = len(self.monitored_names) return SERIES_COLORS[index % len(SERIES_COLORS)] - def _ensure_card(self, name: str, color: str): + def _ensure_card(self, name: str, color: str) -> None: if name in self.cards: return card = ValueCard(name, color) self.cards[name] = card self.cards_layout.insertWidget(max(0, self.cards_layout.count() - 1), card) - def _remove_card(self, name: str): + def _remove_card(self, name: str) -> None: card = self.cards.pop(name, None) if card is None: return card.setParent(None) card.deleteLater() - def _toggle_pause(self): + def _toggle_pause(self) -> None: self.monitor_paused = not self.monitor_paused self.waveform.set_paused(self.monitor_paused) self.toolbar.set_paused(self.monitor_paused) @@ -337,15 +353,15 @@ def _toggle_pause(self): self._log("Monitor paused." if self.monitor_paused else "Monitor resumed.") self._refresh_summary_cards() - def _set_time_window(self, label: str): + def _set_time_window(self, label: str) -> None: self.waveform.set_time_window(self.WINDOW_OPTIONS[label]) self._refresh_summary_cards() - def _handle_rate_change(self, _label: str): + def _handle_rate_change(self, _label: str) -> None: self._refresh_summary_cards() self._restart_monitoring_if_needed() - def _restart_monitoring_if_needed(self): + def _restart_monitoring_if_needed(self) -> None: if not self.device_manager or not self.monitored_names: if self.device_manager: self.device_manager.stop_monitor() @@ -353,11 +369,11 @@ def _restart_monitoring_if_needed(self): self._refresh_summary_cards() return - variables = [ - self.parser.get_variable(name) - for name in self.monitored_names - if self.parser.get_variable(name) - ] + variables = [] + for name in self.monitored_names: + variable = self.parser.get_variable(name) + if variable is not None: + variables.append(variable) if not variables: return @@ -368,13 +384,15 @@ def _restart_monitoring_if_needed(self): ) if self.monitor_active: self._log( - f"Streaming {len(variables)} variable(s) at {self.sidebar.current_rate_label()}." + "Streaming " + f"{len(variables)} variable(s) at " + f"{self.sidebar.current_rate_label()}." ) elif self.device: self._log(f"MONITOR FAIL: {self.device.last_error or 'unknown error'}") self._refresh_summary_cards() - def _on_sample_received(self, name: str, timestamp: float, value: float): + def _on_sample_received(self, name: str, timestamp: float, value: float) -> None: if self.monitor_paused: return self.store.append(name, timestamp, value) @@ -384,7 +402,7 @@ def _on_sample_received(self, name: str, timestamp: float, value: float): self.waveform.update_data(name, timestamp, value) self._refresh_summary_cards() - def _export_png(self): + def _export_png(self) -> None: path, _ = QFileDialog.getSaveFileName( self, "Export Waveform PNG", @@ -399,7 +417,7 @@ def _export_png(self): except Exception as exc: QMessageBox.warning(self, "Export PNG", f"Failed to export PNG: {exc}") - def _export_csv(self): + def _export_csv(self) -> None: path, _ = QFileDialog.getSaveFileName( self, "Export Waveform CSV", @@ -417,14 +435,14 @@ def _export_csv(self): except Exception as exc: QMessageBox.warning(self, "Export CSV", f"Failed to export CSV: {exc}") - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 if self.device_manager: self.device_manager.stop_monitor() if self.conn and self.conn.is_open(): self.conn.close() super().closeEvent(event) - def _refresh_summary_cards(self): + def _refresh_summary_cards(self) -> None: self.connection_fields["Port"].setText( self.sidebar.current_port() or "Not selected" ) diff --git a/host/gui/mock_preview.py b/host/gui/mock_preview.py index adf4248..4a5f9ae 100644 --- a/host/gui/mock_preview.py +++ b/host/gui/mock_preview.py @@ -12,7 +12,7 @@ class MockPreviewController: - def __init__(self, window: MainWindow): + def __init__(self, window: MainWindow) -> None: self.window = window self.start_time = time.time() self.tick = 0 @@ -25,20 +25,22 @@ def __init__(self, window: MainWindow): self.timer.timeout.connect(self.push_samples) self._setup_window() - def _setup_window(self): + def _setup_window(self) -> None: self.window.parser.variables = {item.name: item for item in self.variables} self.window.sidebar.set_variables(self.variables) - self.window.toolbar.set_status_text("Mock preview mode: synthetic waveform data") + self.window.toolbar.set_status_text( + "Mock preview mode: synthetic waveform data" + ) self.window._log("Loaded mock variables for UI preview.") for variable in self.variables: self.window._toggle_variable_monitor(variable.name) self.window.monitor_active = True self.window._log("Mock preview stream started.") - def start(self): + def start(self) -> None: self.timer.start(50) - def push_samples(self): + def push_samples(self) -> None: elapsed = time.time() - self.start_time self.tick += 1 speed = 1200 + 280 * math.sin(elapsed * 1.7) + (self.tick % 6) * 4 @@ -50,7 +52,7 @@ def push_samples(self): self.window._on_sample_received("pid_kp", time.time(), float(kp)) -def run_mock_preview(): +def run_mock_preview() -> None: app = QApplication(sys.argv) app.setStyleSheet(build_stylesheet()) window = MainWindow() diff --git a/host/gui/widgets/log_panel.py b/host/gui/widgets/log_panel.py index 841c44a..bddec86 100644 --- a/host/gui/widgets/log_panel.py +++ b/host/gui/widgets/log_panel.py @@ -4,7 +4,7 @@ class LogPanel(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setObjectName("logPanel") layout = QVBoxLayout(self) @@ -14,5 +14,5 @@ def __init__(self): self.text_edit.setReadOnly(True) layout.addWidget(self.text_edit) - def append_line(self, message: str): + def append_line(self, message: str) -> None: self.text_edit.append(f"{strftime('%H:%M:%S')} {message}") diff --git a/host/gui/widgets/sidebar.py b/host/gui/widgets/sidebar.py index f33568c..84d16a0 100644 --- a/host/gui/widgets/sidebar.py +++ b/host/gui/widgets/sidebar.py @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import Iterable, Optional, cast from PySide6.QtCore import Signal from PySide6.QtGui import QFont @@ -16,6 +16,8 @@ QWidget, ) +from sparam.elf_parser import Variable + class Sidebar(QFrame): refresh_requested = Signal() @@ -29,7 +31,7 @@ class Sidebar(QFrame): variable_activated = Signal(str) selection_changed = Signal(str) - def __init__(self): + def __init__(self) -> None: super().__init__() self.setObjectName("sidebar") self.setFixedWidth(258) @@ -43,9 +45,9 @@ def __init__(self): layout.addWidget(self._build_export_section()) layout.addWidget(self._build_variable_section(), 1) - def _build_connection_section(self): + def _build_connection_section(self) -> QFrame: section = self._section_shell("Transport") - body = section.layout().itemAt(1).widget().layout() + body = self._section_body(section) self.port_combo = QComboBox() self.baud_spin = QSpinBox() @@ -76,12 +78,14 @@ def _build_connection_section(self): body.addWidget(self.load_symbols_btn) return section - def _build_monitor_section(self): + def _build_monitor_section(self) -> QFrame: section = self._section_shell("Monitor") - body = section.layout().itemAt(1).widget().layout() + body = self._section_body(section) self.rate_combo = QComboBox() - self.rate_combo.addItems(["10 ms", "20 ms", "50 ms", "100 ms", "200 ms", "500 ms"]) + self.rate_combo.addItems( + ["10 ms", "20 ms", "50 ms", "100 ms", "200 ms", "500 ms"] + ) self.rate_combo.setCurrentText("10 ms") self.rate_combo.currentTextChanged.connect(self.rate_changed.emit) @@ -97,9 +101,9 @@ def _build_monitor_section(self): body.addWidget(self.pause_btn) return section - def _build_export_section(self): + def _build_export_section(self) -> QFrame: section = self._section_shell("Capture") - body = section.layout().itemAt(1).widget().layout() + body = self._section_body(section) self.export_png_btn = QPushButton("PNG Snapshot") self.export_csv_btn = QPushButton("CSV Export") @@ -110,9 +114,9 @@ def _build_export_section(self): body.addWidget(self.export_csv_btn) return section - def _build_variable_section(self): + def _build_variable_section(self) -> QFrame: section = self._section_shell("Variables") - body = section.layout().itemAt(1).widget().layout() + body = self._section_body(section) helper = QLabel("Double-click to pin a symbol into the board.") helper.setProperty("muted", True) @@ -131,7 +135,7 @@ def _build_variable_section(self): body.addWidget(self.list_widget, 1) return section - def _section_shell(self, title: str): + def _section_shell(self, title: str) -> QFrame: shell = QFrame() shell.setObjectName("sectionCard") outer = QVBoxLayout(shell) @@ -149,7 +153,17 @@ def _section_shell(self, title: str): outer.addWidget(content) return shell - def _field(self, label: str, control): + def _section_body(self, section: QFrame) -> QVBoxLayout: + section_layout = cast(QVBoxLayout, section.layout()) + item = section_layout.itemAt(1) + assert item is not None + body_widget = item.widget() + assert body_widget is not None + body_layout = body_widget.layout() + assert isinstance(body_layout, QVBoxLayout) + return body_layout + + def _field(self, label: str, control: QWidget) -> QWidget: wrap = QWidget() layout = QVBoxLayout(wrap) layout.setContentsMargins(0, 0, 0, 0) @@ -160,14 +174,14 @@ def _field(self, label: str, control): layout.addWidget(control) return wrap - def set_variables(self, variables: Iterable): + def set_variables(self, variables: Iterable[Variable]) -> None: self.list_widget.clear() for variable in sorted(variables, key=lambda item: item.name): item = QListWidgetItem(variable.name) item.setData(1, variable.name) self.list_widget.addItem(item) - def set_monitored(self, name: str, monitored: bool): + def set_monitored(self, name: str, monitored: bool) -> None: for index in range(self.list_widget.count()): item = self.list_widget.item(index) if item.data(1) == name: @@ -178,7 +192,7 @@ def set_monitored(self, name: str, monitored: bool): item.setFont(font) break - def set_ports(self, ports: Iterable[str]): + def set_ports(self, ports: Iterable[str]) -> None: current = self.port_combo.currentText() self.port_combo.clear() self.port_combo.addItems(list(ports)) @@ -199,19 +213,23 @@ def current_device_id(self) -> int: def current_rate_label(self) -> str: return self.rate_combo.currentText() - def set_connected(self, connected: bool): + def set_connected(self, connected: bool) -> None: self.connect_btn.setText("Disconnect" if connected else "Connect") - def set_paused(self, paused: bool): + def set_paused(self, paused: bool) -> None: self.pause_btn.setText("Resume" if paused else "Pause") - def _apply_filter(self, text: str): + def _apply_filter(self, text: str) -> None: prefix = text.strip().lower() for index in range(self.list_widget.count()): item = self.list_widget.item(index) name = item.data(1) or item.text() item.setHidden(bool(prefix) and prefix not in name.lower()) - def _on_current_changed(self, current: QListWidgetItem, _previous: QListWidgetItem): + def _on_current_changed( + self, + current: Optional[QListWidgetItem], + _previous: Optional[QListWidgetItem], + ) -> None: if current: self.selection_changed.emit(current.data(1) or current.text()) diff --git a/host/gui/widgets/toolbar.py b/host/gui/widgets/toolbar.py index d446729..6955272 100644 --- a/host/gui/widgets/toolbar.py +++ b/host/gui/widgets/toolbar.py @@ -2,7 +2,7 @@ class Toolbar(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setObjectName("toolbar") @@ -33,16 +33,16 @@ def __init__(self): layout.addWidget(self.state_chip) layout.addWidget(self.status_label, 2) - def set_status_text(self, text: str): + def set_status_text(self, text: str) -> None: self.status_label.setText(text) - def set_connected(self, connected: bool): + def set_connected(self, connected: bool) -> None: self.state_chip.setText("Connected" if connected else "Offline") self.state_chip.setProperty("state", "connected" if connected else "idle") self.style().unpolish(self.state_chip) self.style().polish(self.state_chip) - def set_paused(self, paused: bool): + def set_paused(self, paused: bool) -> None: if paused: self.state_chip.setText("Paused") self.state_chip.setProperty("state", "warning") diff --git a/host/gui/widgets/value_card.py b/host/gui/widgets/value_card.py index f60041a..8c73d39 100644 --- a/host/gui/widgets/value_card.py +++ b/host/gui/widgets/value_card.py @@ -1,13 +1,15 @@ -from PySide6.QtWidgets import QFrame, QLabel, QHBoxLayout, QVBoxLayout, QWidget +from typing import Optional + +from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget from ..styles.catppuccin import ERROR, MUTED, SUCCESS class ValueCard(QFrame): - def __init__(self, name: str, color: str): + def __init__(self, name: str, color: str) -> None: super().__init__() self.setObjectName("valueCard") - self._last_value = None + self._last_value: Optional[float] = None layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -16,7 +18,9 @@ def __init__(self, name: str, color: str): stripe = QWidget() stripe.setFixedWidth(2) stripe.setStyleSheet( - f"background: {color}; border-top-left-radius: 8px; border-bottom-left-radius: 8px;" + f"background: {color}; " + "border-top-left-radius: 8px; " + "border-bottom-left-radius: 8px;" ) body = QWidget() @@ -38,7 +42,7 @@ def __init__(self, name: str, color: str): layout.addWidget(stripe) layout.addWidget(body) - def update_value(self, value: float): + def update_value(self, value: float) -> None: self.value_label.setText(f"{value:.3f}") if self._last_value is None: self.delta_label.setText("First sample") diff --git a/host/gui/widgets/waveform_plot.py b/host/gui/widgets/waveform_plot.py index d0b9dee..25eb984 100644 --- a/host/gui/widgets/waveform_plot.py +++ b/host/gui/widgets/waveform_plot.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, List, Optional import pyqtgraph as pg from pyqtgraph.exporters import ImageExporter @@ -6,7 +6,7 @@ class WaveformPlot(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setObjectName("plotPanel") layout = QVBoxLayout(self) @@ -25,12 +25,12 @@ def __init__(self): layout.addWidget(self.plot_widget) self._curves: Dict[str, pg.PlotCurveItem] = {} - self._timestamps: Dict[str, list] = {} - self._values: Dict[str, list] = {} + self._timestamps: Dict[str, List[float]] = {} + self._values: Dict[str, List[float]] = {} self._time_window: Optional[float] = 10.0 self._paused = False - def add_variable(self, name: str, color: str): + def add_variable(self, name: str, color: str) -> None: if name in self._curves: return curve = self.plot_widget.plot(name=name, pen=pg.mkPen(color=color, width=1.6)) @@ -38,14 +38,14 @@ def add_variable(self, name: str, color: str): self._timestamps[name] = [] self._values[name] = [] - def remove_variable(self, name: str): + def remove_variable(self, name: str) -> None: curve = self._curves.pop(name, None) if curve is not None: self.plot_widget.removeItem(curve) self._timestamps.pop(name, None) self._values.pop(name, None) - def update_data(self, name: str, timestamp: float, value: float): + def update_data(self, name: str, timestamp: float, value: float) -> None: if self._paused or name not in self._curves: return @@ -69,12 +69,12 @@ def update_data(self, name: str, timestamp: float, value: float): values, ) - def set_time_window(self, seconds: Optional[float]): + def set_time_window(self, seconds: Optional[float]) -> None: self._time_window = seconds - def set_paused(self, paused: bool): + def set_paused(self, paused: bool) -> None: self._paused = paused - def export_png(self, filepath: str): + def export_png(self, filepath: str) -> None: exporter = ImageExporter(self.plot_widget.plotItem) exporter.export(filepath) diff --git a/host/pyproject.toml b/host/pyproject.toml index 09164b1..2c0f1db 100644 --- a/host/pyproject.toml +++ b/host/pyproject.toml @@ -12,6 +12,10 @@ dependencies = [ ] [project.optional-dependencies] +dev = [ + "mypy>=1.11", + "ruff>=0.11", +] gui = ["PySide6>=6.6", "pyqtgraph>=0.13"] test = ["pytest>=8.0"] @@ -23,3 +27,48 @@ sparam-gui-mock = "gui.mock_preview:run_mock_preview" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.ruff] +line-length = 88 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "E", + "F", + "I", + "B", + "UP", + "N", +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" + +[tool.mypy] +python_version = "3.9" +pretty = true +show_error_codes = true +warn_unused_configs = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_return_any = true +check_untyped_defs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +no_implicit_optional = true +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "crcmod", + "crcmod.*", + "serial", + "serial.*", + "pyqtgraph", + "pyqtgraph.*", +] +ignore_missing_imports = true diff --git a/host/sparam/__init__.py b/host/sparam/__init__.py index f72ec9b..6dfb839 100644 --- a/host/sparam/__init__.py +++ b/host/sparam/__init__.py @@ -1,10 +1,10 @@ -from .protocol import Protocol, CommandType, DataType, ErrorCode -from .elf_parser import ElfParser -from .serial_conn import SerialConnection -from .socket_conn import SocketConnection from .device import Device, Variable from .device_manager import DeviceManager, SamplePoint +from .elf_parser import ElfParser from .monitor_store import MonitorStore, TimeSeries +from .protocol import CommandType, DataType, ErrorCode, Protocol +from .serial_conn import SerialConnection +from .socket_conn import SocketConnection __all__ = [ "Protocol", diff --git a/host/sparam/device.py b/host/sparam/device.py index 41cdc9c..ef7f148 100644 --- a/host/sparam/device.py +++ b/host/sparam/device.py @@ -1,18 +1,12 @@ -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Callable import struct import time +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from .protocol import ( - Protocol, - Frame, - CommandType, - DataType, - ErrorCode, - SAMPLE_RATES, -) -from .serial_conn import SerialConnection from .elf_parser import ElfParser, Variable +from .protocol import CommandType, DataType, Frame, Protocol +from .serial_conn import SerialConnection +from .socket_conn import SocketConnection @dataclass @@ -23,13 +17,19 @@ class MonitoredVar: last_update: Optional[float] = None +AcceptFrame = Callable[[Frame], bool] +WriteBatch = List[Tuple[Variable, bytes]] +Connection = Union[SerialConnection, SocketConnection] +DeviceInfo = Dict[str, object] + + class Device: def __init__( self, - connection: SerialConnection, + connection: Connection, device_id: int, elf_parser: Optional[ElfParser] = None, - ): + ) -> None: self.connection = connection self.device_id = device_id self.parser = elf_parser or ElfParser() @@ -43,7 +43,7 @@ def __init__( def last_error(self) -> str: return self._last_error - def _set_error_from_response(self, response: Optional[Frame], op: str): + def _set_error_from_response(self, response: Optional[Frame], op: str) -> None: if response is None: self._last_error = f"{op}: timeout or no response" return @@ -63,7 +63,7 @@ def _send_and_wait_filtered( self, data: bytes, timeout: float, - accept, + accept: AcceptFrame, ) -> Optional[Frame]: response = self.connection.send_and_wait( data, @@ -77,6 +77,8 @@ def _send_and_wait_filtered( if isinstance(self.connection, SerialConnection) and self.connection.is_open(): try: ser = self.connection._serial + if ser is None: + return None ser.reset_input_buffer() ser.write(data) ser.timeout = timeout @@ -97,7 +99,7 @@ def _send_and_wait_filtered( frame = Protocol.decode(bytes(buf[i:end])) if frame is None: continue - if accept and not accept(frame): + if not accept(frame): continue return frame @@ -132,7 +134,7 @@ def ping(self, timeout: float = 1.0) -> bool: self._set_error_from_response(response, "ping") return False - def query_info(self, timeout: float = 1.0) -> Optional[Dict]: + def query_info(self, timeout: float = 1.0) -> Optional[DeviceInfo]: data = Protocol.encode_query_info(self.device_id) response = self._send_and_wait_filtered( data, @@ -165,12 +167,15 @@ def read_single( data, timeout, accept=lambda f: ( - (f.command >= CommandType.READ_SINGLE and f.command <= CommandType.READ_500MS) + ( + f.command >= CommandType.READ_SINGLE + and f.command <= CommandType.READ_500MS + ) or f.is_nack() ), ) - results = {} + results: Dict[str, bytes] = {} if response: if response.is_nack(): self._set_error_from_response(response, "read") @@ -223,8 +228,8 @@ def write_single( return False - def write_batch(self, writes: List[tuple], timeout: float = 1.0) -> bool: - encoded_writes = [] + def write_batch(self, writes: WriteBatch, timeout: float = 1.0) -> bool: + encoded_writes: List[Tuple[int, DataType, bytes]] = [] for var, value in writes: dtype = DataType(var.dtype_code) if var.dtype_code else DataType.UINT32 encoded_writes.append((var.address, dtype, value)) @@ -281,7 +286,7 @@ def stop_monitor(self) -> bool: self._set_error_from_response(response, "stop_monitor") return False - def on_frame_received(self, frame: Frame): + def on_frame_received(self, frame: Frame) -> None: if ( frame.command >= CommandType.READ_1MS and frame.command <= CommandType.READ_500MS @@ -298,9 +303,9 @@ def on_frame_received(self, frame: Frame): break @staticmethod - def bytes_to_value(data: bytes, dtype: DataType) -> any: + def bytes_to_value(data: bytes, dtype: DataType) -> Any: return struct.unpack(dtype.format_char, data)[0] @staticmethod - def value_to_bytes(value: any, dtype: DataType) -> bytes: + def value_to_bytes(value: Any, dtype: DataType) -> bytes: return struct.pack(dtype.format_char, value) diff --git a/host/sparam/device_manager.py b/host/sparam/device_manager.py index 916672b..0d449a1 100644 --- a/host/sparam/device_manager.py +++ b/host/sparam/device_manager.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass import time -from typing import Callable, List, Optional +from dataclasses import dataclass +from typing import Callable, List from .device import Device +from .elf_parser import Variable from .protocol import DataType @@ -25,10 +26,9 @@ def add_callback(self, callback: Callable[[SamplePoint], None]) -> None: def remove_callback(self, callback: Callable[[SamplePoint], None]) -> None: self._callbacks = [item for item in self._callbacks if item != callback] - def start_monitor(self, variables, rate: int) -> bool: - if ( - not self._receiving_started - and hasattr(self.device.connection, "start_receive") + def start_monitor(self, variables: List[Variable], rate: int) -> bool: + if not self._receiving_started and hasattr( + self.device.connection, "start_receive" ): self.device.connection.start_receive(self.device.on_frame_received) self._receiving_started = True diff --git a/host/sparam/elf_parser.py b/host/sparam/elf_parser.py index 302aeb2..63a760a 100644 --- a/host/sparam/elf_parser.py +++ b/host/sparam/elf_parser.py @@ -1,7 +1,6 @@ -from dataclasses import dataclass -from typing import Dict, List, Optional -import struct import re +from dataclasses import dataclass +from typing import Any, Dict, List, Optional @dataclass @@ -32,14 +31,14 @@ def dtype_code(self) -> int: class ElfParser: - def __init__(self): + def __init__(self) -> None: self.variables: Dict[str, Variable] = {} def parse_elf(self, filepath: str) -> List[Variable]: try: from elftools.elf.elffile import ELFFile - except ImportError: - raise ImportError("pyelftools is required: pip install pyelftools") + except ImportError as exc: + raise ImportError("pyelftools is required: pip install pyelftools") from exc self.variables.clear() @@ -52,7 +51,7 @@ def parse_elf(self, filepath: str) -> List[Variable]: return list(self.variables.values()) - def _parse_section_symbols(self, elf, section): + def _parse_section_symbols(self, elf: Any, section: Any) -> None: from elftools.elf.sections import SymbolTableSection for s in elf.iter_sections(): @@ -67,7 +66,7 @@ def _parse_section_symbols(self, elf, section): sym_section = elf.get_section(sym["st_shndx"]) if sym_section.name not in [".data", ".bss", ".noinit"]: continue - except: + except Exception: continue name = sym.name @@ -95,7 +94,7 @@ def _guess_type(self, size: int) -> str: def parse_map(self, filepath: str) -> List[Variable]: self.variables.clear() - with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + with open(filepath, encoding="utf-8", errors="ignore") as f: content = f.read() patterns = [ diff --git a/host/sparam/monitor_store.py b/host/sparam/monitor_store.py index a889bf6..540e1f7 100644 --- a/host/sparam/monitor_store.py +++ b/host/sparam/monitor_store.py @@ -1,6 +1,6 @@ from collections import deque from dataclasses import dataclass -from typing import Deque, Dict, List, Tuple +from typing import Deque, Dict, List, Optional, Tuple @dataclass @@ -29,7 +29,7 @@ def series(self, name: str) -> TimeSeries: values=list(self._values.get(name, [])), ) - def latest_value(self, name: str): + def latest_value(self, name: str) -> Optional[float]: values = self._values.get(name) if not values: return None diff --git a/host/sparam/protocol.py b/host/sparam/protocol.py index b13096e..57d025e 100644 --- a/host/sparam/protocol.py +++ b/host/sparam/protocol.py @@ -1,7 +1,8 @@ -from enum import IntEnum +import struct from dataclasses import dataclass +from enum import IntEnum from typing import List, Optional, Tuple -import struct + import crcmod.predefined @@ -104,7 +105,7 @@ class Protocol: @classmethod def crc16(cls, data: bytes) -> int: - return cls._CRC16_MODBUS(data) + return int(cls._CRC16_MODBUS(data)) @classmethod def encode(cls, device_id: int, command: int, data: bytes = b"") -> bytes: @@ -143,10 +144,11 @@ def decode(cls, raw: bytes) -> Optional[Frame]: @classmethod def encode_read(cls, device_id: int, addresses: List[int], rate: int = 0) -> bytes: + command: int if rate == 0: command = CommandType.READ_SINGLE else: - command = CommandType.READ_SINGLE + rate + command = int(CommandType.READ_SINGLE) + rate data = b"".join(struct.pack(" bool: self.last_error = str(exc) return False - def close(self): + def close(self) -> None: self._stop_event.set() if self._rx_thread: self._rx_thread.join(timeout=1.0) @@ -58,23 +58,29 @@ def is_open(self) -> bool: def send(self, data: bytes) -> bool: if not self.is_open(): return False + serial_port = self._serial + if serial_port is None: + return False try: - self._serial.write(data) + serial_port.write(data) return True except serial.SerialException: return False - def _receive_loop(self): + def _receive_loop(self) -> None: while not self._stop_event.is_set() and self._serial: try: - data = self._serial.read(64) + serial_port = self._serial + if serial_port is None: + break + data = serial_port.read(64) if data: self._rx_buffer.extend(data) self._try_parse_frames() except serial.SerialException: break - def _try_parse_frames(self): + def _try_parse_frames(self) -> None: while len(self._rx_buffer) >= 7: if self._rx_buffer[0] != 0xAA or self._rx_buffer[1] != 0x55: self._rx_buffer.pop(0) @@ -118,7 +124,7 @@ def _pop_next_frame(self) -> Optional[Frame]: return None - def start_receive(self, on_frame: Callable[[Frame], None]): + def start_receive(self, on_frame: Callable[[Frame], None]) -> None: self._on_frame = on_frame self._stop_event.clear() self._rx_thread = Thread(target=self._receive_loop, daemon=True) @@ -138,7 +144,7 @@ def send_and_wait( result: Optional[Frame] = None event = Event() - def on_response(frame: Frame): + def on_response(frame: Frame) -> None: nonlocal result if accept_frame and not accept_frame(frame): return @@ -158,8 +164,11 @@ def on_response(frame: Frame): # Sync request-response path for CLI/GUI commands that don't start RX thread. self._rx_buffer.clear() + serial_port = self._serial + if serial_port is None: + return None try: - self._serial.reset_input_buffer() + serial_port.reset_input_buffer() except serial.SerialException: return None @@ -169,11 +178,11 @@ def on_response(frame: Frame): # Give UART/USB bridge a moment to push the full response into host buffer. time.sleep(0.02) - self._serial.timeout = timeout + serial_port.timeout = timeout buf = bytearray() for _ in range(3): try: - chunk = self._serial.read(64) + chunk = serial_port.read(64) except serial.SerialException: return None @@ -199,10 +208,12 @@ def on_response(frame: Frame): return None - def __enter__(self): + def __enter__(self) -> "SerialConnection": self.open() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, exc_type: object, exc_val: object, exc_tb: object + ) -> Literal[False]: self.close() return False diff --git a/host/sparam/socket_conn.py b/host/sparam/socket_conn.py index 1ebe0bd..fc29b30 100644 --- a/host/sparam/socket_conn.py +++ b/host/sparam/socket_conn.py @@ -1,9 +1,9 @@ -from typing import Optional, Callable -from threading import Thread, Event import socket import time +from threading import Event, Thread +from typing import Callable, Literal, Optional -from .protocol import Protocol, Frame +from .protocol import Frame, Protocol class SocketConnection: @@ -19,14 +19,16 @@ def __init__(self, host: str, port: int, timeout: float = 1.0): def open(self) -> bool: try: - self._sock = socket.create_connection((self.host, self.port), timeout=self.timeout) + self._sock = socket.create_connection( + (self.host, self.port), timeout=self.timeout + ) self._sock.settimeout(0.2) return True except OSError: self._sock = None return False - def close(self): + def close(self) -> None: self._stop_event.set() if self._rx_thread: self._rx_thread.join(timeout=1.0) @@ -51,7 +53,7 @@ def send(self, data: bytes) -> bool: except OSError: return False - def _receive_loop(self): + def _receive_loop(self) -> None: while not self._stop_event.is_set() and self._sock: try: data = self._sock.recv(256) @@ -64,7 +66,7 @@ def _receive_loop(self): except OSError: break - def _try_parse_frames(self): + def _try_parse_frames(self) -> None: while len(self._rx_buffer) >= 7: if self._rx_buffer[0] != 0xAA or self._rx_buffer[1] != 0x55: self._rx_buffer.pop(0) @@ -85,7 +87,7 @@ def _try_parse_frames(self): if frame and self._on_frame: self._on_frame(frame) - def start_receive(self, on_frame: Callable[[Frame], None]): + def start_receive(self, on_frame: Callable[[Frame], None]) -> None: self._on_frame = on_frame self._stop_event.clear() self._rx_thread = Thread(target=self._receive_loop, daemon=True) @@ -100,7 +102,7 @@ def send_and_wait( result: Optional[Frame] = None event = Event() - def on_response(frame: Frame): + def on_response(frame: Frame) -> None: nonlocal result if accept_frame and not accept_frame(frame): return @@ -135,10 +137,12 @@ def on_response(frame: Frame): self._on_frame = old_callback return result - def __enter__(self): + def __enter__(self) -> "SocketConnection": self.open() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, exc_type: object, exc_val: object, exc_tb: object + ) -> Literal[False]: self.close() return False diff --git a/host/tests/test_monitor_store.py b/host/tests/test_monitor_store.py index 09e9192..8126e25 100644 --- a/host/tests/test_monitor_store.py +++ b/host/tests/test_monitor_store.py @@ -1,7 +1,7 @@ from sparam.monitor_store import MonitorStore -def test_monitor_store_keeps_latest_points_with_ring_buffer_limit(): +def test_monitor_store_keeps_latest_points_with_ring_buffer_limit() -> None: store = MonitorStore(max_points=3) store.append("motor_speed", 1.0, 10.0) @@ -16,7 +16,7 @@ def test_monitor_store_keeps_latest_points_with_ring_buffer_limit(): assert store.latest_value("motor_speed") == 40.0 -def test_monitor_store_exports_rows_in_timestamp_order(): +def test_monitor_store_exports_rows_in_timestamp_order() -> None: store = MonitorStore(max_points=5) store.append("speed", 1.0, 100.0) diff --git a/host/tests/test_socket_simulation.py b/host/tests/test_socket_simulation.py index 7a3e5d4..844b51b 100644 --- a/host/tests/test_socket_simulation.py +++ b/host/tests/test_socket_simulation.py @@ -2,14 +2,18 @@ import struct import threading import time +from typing import List, Optional -from sparam import Device, SocketConnection, Protocol, CommandType +from sparam import CommandType, Device, Protocol, SocketConnection +from sparam.device_manager import DeviceManager, SamplePoint from sparam.elf_parser import Variable -from sparam.device_manager import DeviceManager +from sparam.protocol import Frame class SimulatedDeviceServer: - def __init__(self, host: str = "127.0.0.1", port: int = 0, device_id: int = 1): + def __init__( + self, host: str = "127.0.0.1", port: int = 0, device_id: int = 1 + ) -> None: self.host = host self.port = port self.device_id = device_id @@ -22,16 +26,16 @@ def __init__(self, host: str = "127.0.0.1", port: int = 0, device_id: int = 1): self.memory = { 0x20000000: struct.pack(" None: self._thread.start() - def stop(self): + def stop(self) -> None: self._stop.set() self._stream_stop.set() try: @@ -44,7 +48,7 @@ def stop(self): self._stream_thread.join(timeout=1.0) self._sock.close() - def _serve(self): + def _serve(self) -> None: while not self._stop.is_set(): try: conn, _ = self._sock.accept() @@ -88,7 +92,9 @@ def _serve(self): if response: conn.sendall(response) - def _start_streaming(self, conn, command, addresses): + def _start_streaming( + self, conn: socket.socket, command: int, addresses: List[int] + ) -> None: self._stream_stop.set() if self._stream_thread: self._stream_thread.join(timeout=1.0) @@ -98,7 +104,7 @@ def _start_streaming(self, conn, command, addresses): self._stream_rate = command - CommandType.READ_SINGLE self._stream_addresses = addresses - def stream(): + def stream() -> None: tick = 0 intervals = { 1: 0.001, @@ -120,6 +126,8 @@ def stream(): payload.extend(struct.pack(" bytes: if frame.command == CommandType.HEARTBEAT: return Protocol.encode(self.device_id, CommandType.ACK) @@ -139,7 +147,9 @@ def _handle_frame(self, frame, conn): address = struct.unpack(" None: server = SimulatedDeviceServer(device_id=1) server.start() @@ -198,7 +208,7 @@ def test_device_ping_read_write_over_socket(): server.stop() -def test_device_manager_receives_periodic_stream_samples(): +def test_device_manager_receives_periodic_stream_samples() -> None: server = SimulatedDeviceServer(device_id=1) server.start() @@ -212,7 +222,7 @@ def test_device_manager_receives_periodic_stream_samples(): size=4, var_type="uint32_t", ) - samples = [] + samples: List[SamplePoint] = [] manager = DeviceManager(device) manager.add_callback(samples.append) diff --git a/host/uv.lock b/host/uv.lock index fae3103..966965a 100644 --- a/host/uv.lock +++ b/host/uv.lock @@ -61,7 +61,7 @@ version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -94,6 +94,222 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", size = 65990, upload-time = "2026-02-17T16:12:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", size = 68640, upload-time = "2026-02-17T16:12:50.24Z" }, + { url = "https://files.pythonhosted.org/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", size = 196099, upload-time = "2026-02-17T16:12:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", size = 206663, upload-time = "2026-02-17T16:12:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", size = 219318, upload-time = "2026-02-17T16:12:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", size = 212191, upload-time = "2026-02-17T16:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", size = 220672, upload-time = "2026-02-17T16:12:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", size = 216172, upload-time = "2026-02-17T16:12:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", size = 214116, upload-time = "2026-02-17T16:13:01.056Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", size = 236664, upload-time = "2026-02-17T16:13:02.314Z" }, + { url = "https://files.pythonhosted.org/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", size = 54368, upload-time = "2026-02-17T16:13:03.549Z" }, + { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "numpy" version = "1.24.4" @@ -343,6 +559,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -677,6 +902,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "ruff" +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +] + [[package]] name = "shiboken6" version = "6.6.3.1" @@ -735,6 +985,11 @@ dependencies = [ ] [package.optional-dependencies] +dev = [ + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, +] gui = [ { name = "pyqtgraph", version = "0.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pyqtgraph", version = "0.13.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, @@ -753,13 +1008,15 @@ test = [ requires-dist = [ { name = "click", specifier = ">=8.0" }, { name = "crcmod", specifier = ">=1.7" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11" }, { name = "pyelftools", specifier = ">=0.29" }, { name = "pyqtgraph", marker = "extra == 'gui'", specifier = ">=0.13" }, { name = "pyserial", specifier = ">=3.5" }, { name = "pyside6", marker = "extra == 'gui'", specifier = ">=6.6" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11" }, ] -provides-extras = ["gui", "test"] +provides-extras = ["dev", "gui", "test"] [[package]] name = "tomli" @@ -832,6 +1089,7 @@ name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ + "python_full_version >= '3.11'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] From 309be80d6cee6d96e887108db3fa36c4fcba8eac Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 27 Mar 2026 06:00:00 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(gui):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9B=91=E6=B5=8B=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] - 此提交是上位机界面的结构性重构,不是新增协议能力或缺陷修复。 - 此提交将现有监测界面从表单式工具布局调整为更适合实时调参的工作台布局,并同步收敛视觉层级。 [重构动机] - 旧界面的色块层级过于平均,左侧表单、波形、值卡和日志之间缺少明确主次。 - 当前布局更像通用桌面工具,不能充分突出波形主视图,也没有形成稳定的 inspector 区域。 - mock 预览缺少界面骨架约束,后续继续调样式时容易把结构做散。 [重构方案] - 此提交将主窗口重组为左侧控制栏、中央信号工作区、右侧 inspector 三段式结构,并将值卡区收敛为更轻的 signal stats strip。 - 此提交重写 GUI 样式色阶,区分页面底、壳层、面板、卡片和输入控件层级,同时收敛按钮、chip、图表网格和语义色的视觉权重。 - 此提交调整 toolbar、sidebar、waveform、value card、log panel 与 mock preview 的细节,并新增 GUI 布局测试钉住 workspaceShell、inspectorPanel 和 signalStatsStrip 三个关键锚点。 [影响范围] - 影响 host/gui 下的主窗口、样式和主要部件,以及 host/tests/test_gui_layout.py。 - 界面行为保持兼容,监测、导出、mock 预览和现有数据链路仍按原方式工作。 - 本地已验证 ruff、mypy、offscreen pytest 全绿,mock 预览离屏实例化正常。 --- host/gui/main_window.py | 112 ++++++++++++----------- host/gui/mock_preview.py | 1 + host/gui/styles/catppuccin.py | 142 ++++++++++++++++++------------ host/gui/widgets/log_panel.py | 12 ++- host/gui/widgets/sidebar.py | 18 ++-- host/gui/widgets/toolbar.py | 19 ++-- host/gui/widgets/value_card.py | 17 ++-- host/gui/widgets/waveform_plot.py | 33 ++++--- host/tests/test_gui_layout.py | 31 +++++++ 9 files changed, 245 insertions(+), 140 deletions(-) create mode 100644 host/tests/test_gui_layout.py diff --git a/host/gui/main_window.py b/host/gui/main_window.py index 2aaa7db..dcb9ceb 100644 --- a/host/gui/main_window.py +++ b/host/gui/main_window.py @@ -7,13 +7,11 @@ from PySide6.QtWidgets import ( QFileDialog, QFrame, - QGridLayout, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QScrollArea, - QSplitter, QVBoxLayout, QWidget, ) @@ -86,14 +84,10 @@ def _build_ui(self) -> None: root = QWidget(self) self.setCentralWidget(root) layout = QVBoxLayout(root) - layout.setContentsMargins(18, 18, 18, 18) - layout.setSpacing(14) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) self.toolbar = Toolbar() - - splitter = QSplitter(Qt.Orientation.Horizontal) - splitter.setChildrenCollapsible(False) - self.sidebar = Sidebar() self.sidebar.refresh_requested.connect(self._refresh_ports) self.sidebar.connect_requested.connect(self._toggle_connection) @@ -106,32 +100,36 @@ def _build_ui(self) -> None: self.sidebar.variable_activated.connect(self._toggle_variable_monitor) self.sidebar.selection_changed.connect(self._preview_variable) - main_panel = QWidget() - main_layout = QGridLayout(main_panel) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setHorizontalSpacing(14) - main_layout.setVerticalSpacing(14) + workspace = QWidget() + workspace.setObjectName("workspaceShell") + workspace_layout = QHBoxLayout(workspace) + workspace_layout.setContentsMargins(0, 0, 0, 0) + workspace_layout.setSpacing(10) - self.waveform = WaveformPlot() - connection_card, self.connection_fields = self._create_summary_card( - "Connection Overview", - "Current transport and symbol source", - ["Port", "Baud", "Device", "Symbols"], - ) - monitor_card, self.monitor_fields = self._create_summary_card( - "Monitor Session", - "Live sampling session information", - ["Variables", "Rate", "Window", "Mode"], - ) + center_column = QWidget() + center_layout = QVBoxLayout(center_column) + center_layout.setContentsMargins(0, 0, 0, 0) + center_layout.setSpacing(10) - cards_shell = QFrame() - cards_shell.setObjectName("cardShelf") - cards_layout = QVBoxLayout(cards_shell) - cards_layout.setContentsMargins(12, 12, 12, 12) - cards_layout.setSpacing(10) - cards_title = QLabel("Live Values") - cards_title.setProperty("muted", True) - cards_layout.addWidget(cards_title) + self.waveform = WaveformPlot() + center_layout.addWidget(self.waveform, 1) + + self.stats_strip = QFrame() + self.stats_strip.setObjectName("signalStatsStrip") + cards_layout = QVBoxLayout(self.stats_strip) + cards_layout.setContentsMargins(12, 10, 12, 12) + cards_layout.setSpacing(8) + + stats_title_row = QHBoxLayout() + stats_title_row.setContentsMargins(0, 0, 0, 0) + stats_title = QLabel("Pinned Signals") + stats_title.setProperty("sectionTitle", True) + stats_caption = QLabel("Live sample snapshot from the active monitor stream") + stats_caption.setProperty("muted", True) + stats_title_row.addWidget(stats_title) + stats_title_row.addStretch(1) + stats_title_row.addWidget(stats_caption) + cards_layout.addLayout(stats_title_row) scroll = QScrollArea() scroll.setWidgetResizable(True) @@ -139,32 +137,42 @@ def _build_ui(self) -> None: scroll_contents = QWidget() self.cards_layout = QHBoxLayout(scroll_contents) self.cards_layout.setContentsMargins(0, 0, 0, 0) - self.cards_layout.setSpacing(10) + self.cards_layout.setSpacing(8) self.cards_layout.addStretch(1) scroll.setWidget(scroll_contents) cards_layout.addWidget(scroll) + center_layout.addWidget(self.stats_strip) + + inspector = QFrame() + inspector.setObjectName("inspectorPanel") + inspector.setFixedWidth(318) + inspector_layout = QVBoxLayout(inspector) + inspector_layout.setContentsMargins(12, 12, 12, 12) + inspector_layout.setSpacing(10) + + connection_card, self.connection_fields = self._create_summary_card( + "Connection Overview", + "Current transport and symbol source", + ["Port", "Baud", "Device", "Symbols"], + ) + monitor_card, self.monitor_fields = self._create_summary_card( + "Monitor Session", + "Live sampling session information", + ["Variables", "Rate", "Window", "Mode"], + ) self.log_panel = LogPanel() - main_layout.addWidget(self.waveform, 0, 0, 2, 2) - main_layout.addWidget(connection_card, 0, 2) - main_layout.addWidget(monitor_card, 1, 2) - main_layout.addWidget(cards_shell, 2, 0, 1, 2) - main_layout.addWidget(self.log_panel, 2, 2) - main_layout.setColumnStretch(0, 3) - main_layout.setColumnStretch(1, 2) - main_layout.setColumnStretch(2, 2) - main_layout.setRowStretch(0, 4) - main_layout.setRowStretch(1, 3) - main_layout.setRowStretch(2, 2) - - splitter.addWidget(self.sidebar) - splitter.addWidget(main_panel) - splitter.setStretchFactor(0, 1) - splitter.setStretchFactor(1, 4) + inspector_layout.addWidget(connection_card) + inspector_layout.addWidget(monitor_card) + inspector_layout.addWidget(self.log_panel, 1) + + workspace_layout.addWidget(self.sidebar) + workspace_layout.addWidget(center_column, 1) + workspace_layout.addWidget(inspector) layout.addWidget(self.toolbar) - layout.addWidget(splitter, 1) + layout.addWidget(workspace, 1) self._refresh_summary_cards() def _create_summary_card( @@ -173,8 +181,8 @@ def _create_summary_card( card = QFrame() card.setObjectName("summaryCard") layout = QVBoxLayout(card) - layout.setContentsMargins(14, 14, 14, 14) - layout.setSpacing(10) + layout.setContentsMargins(12, 10, 12, 12) + layout.setSpacing(8) title_label = QLabel(title) title_label.setProperty("sectionTitle", True) diff --git a/host/gui/mock_preview.py b/host/gui/mock_preview.py index 4a5f9ae..615620a 100644 --- a/host/gui/mock_preview.py +++ b/host/gui/mock_preview.py @@ -28,6 +28,7 @@ def __init__(self, window: MainWindow) -> None: def _setup_window(self) -> None: self.window.parser.variables = {item.name: item for item in self.variables} self.window.sidebar.set_variables(self.variables) + self.window.toolbar.set_preview() self.window.toolbar.set_status_text( "Mock preview mode: synthetic waveform data" ) diff --git a/host/gui/styles/catppuccin.py b/host/gui/styles/catppuccin.py index 5a57874..cd890ee 100644 --- a/host/gui/styles/catppuccin.py +++ b/host/gui/styles/catppuccin.py @@ -1,47 +1,66 @@ -BACKGROUND = "#f3f4f6" -SIDEBAR = "#f8fafc" -CARD = "#fcfcfd" -CARD_ALT = "#f8fafc" -INPUT_BG = "#f8fafb" -BORDER = "#e5e7eb" -TEXT = "#111827" -MUTED = "#6b7280" -ACCENT = "#1f2937" -SUCCESS = "#4b7a5a" -WARNING = "#9a6b16" -ERROR = "#b25d5d" +PAGE_BG = "#f2f1ee" +SHELL_BG = "#ebe8e2" +PANEL_BG = "#f7f5f1" +CARD_BG = "#fbfaf7" +INPUT_BG = "#f1eee8" +BORDER = "#ddd7ce" +BORDER_STRONG = "#cbc3b7" +TEXT = "#28241f" +MUTED = "#777064" +ACCENT = "#2f2b27" +ACCENT_SOFT = "#e7e2d8" +SUCCESS = "#5d8a68" +WARNING = "#a07b32" +ERROR = "#b86a63" SERIES_COLORS = [ "#2563eb", "#0f766e", - "#7c3aed", - "#ea580c", - "#dc2626", + "#8b5cf6", + "#c97a2b", + "#4b5563", ] def build_stylesheet() -> str: return f""" QWidget {{ - background: {BACKGROUND}; + background: {PAGE_BG}; color: {TEXT}; font-family: Monospace; - font-size: 13px; + font-size: 12px; }} QMainWindow {{ - background: {BACKGROUND}; + background: {PAGE_BG}; + }} + QWidget#workspaceShell {{ + background: transparent; + }} + QFrame#toolbar {{ + background: {PANEL_BG}; + border: 1px solid {BORDER}; + border-radius: 14px; + }} + QFrame#sidebar {{ + background: {SHELL_BG}; + border: 1px solid {BORDER}; + border-radius: 16px; }} - QFrame#toolbar, - QFrame#sidebar, QFrame#sectionCard, QFrame#summaryCard, + QFrame#signalStatsStrip, + QFrame#inspectorPanel, QWidget#plotPanel, - QFrame#cardShelf, QFrame#valueCard, QFrame#logPanel {{ - background: {CARD}; + background: {PANEL_BG}; border: 1px solid {BORDER}; - border-radius: 8px; + border-radius: 14px; + }} + QFrame#summaryCard, + QFrame#logPanel, + QFrame#valueCard {{ + background: {CARD_BG}; }} QListWidget, QLineEdit, @@ -52,83 +71,98 @@ def build_stylesheet() -> str: background: {INPUT_BG}; color: {TEXT}; border: 1px solid {BORDER}; - border-radius: 6px; + border-radius: 10px; padding: 7px 10px; }} + QLineEdit, + QComboBox, + QSpinBox {{ + min-height: 18px; + }} + QPushButton {{ + font-weight: 600; + }} QPushButton:hover {{ - background: {CARD_ALT}; - border-color: #d1d5db; + background: {CARD_BG}; + border-color: {BORDER_STRONG}; }} QPushButton[accent="true"] {{ background: {ACCENT}; - color: {CARD}; + color: {CARD_BG}; border-color: {ACCENT}; - font-weight: 700; }} QPushButton:pressed {{ - background: #eef2f7; + background: {ACCENT_SOFT}; }} QPushButton[accent="true"]:pressed {{ - background: #374151; - }} - QPushButton[semantic="success"] {{ - color: {SUCCESS}; - }} - QPushButton[semantic="warning"] {{ - color: {WARNING}; - }} - QPushButton[semantic="error"] {{ - color: {ERROR}; + background: #1f1c19; }} QLabel[muted="true"] {{ color: {MUTED}; }} QLabel[hero="true"] {{ - font-size: 24px; + font-size: 15px; font-weight: 700; - letter-spacing: 0.5px; + letter-spacing: 0.4px; + }} + QLabel[sectionTitle="true"] {{ + font-size: 11px; + font-weight: 700; + letter-spacing: 1.0px; + text-transform: uppercase; + color: {TEXT}; }} QLabel[chip="true"] {{ background: {INPUT_BG}; border: 1px solid {BORDER}; border-radius: 999px; padding: 4px 10px; - font-size: 12px; + font-size: 11px; + font-weight: 700; }} QLabel[chip="true"][state="connected"] {{ color: {SUCCESS}; - border-color: #d6eadc; + border-color: #d5dfd6; + background: #f3f7f3; }} QLabel[chip="true"][state="warning"] {{ color: {WARNING}; - border-color: #eadcb6; + border-color: #e4d8bd; + background: #faf6eb; }} - QLabel[sectionTitle="true"] {{ - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.8px; + QLabel[chip="true"][state="idle"] {{ + color: {MUTED}; + }} + QListWidget {{ + outline: none; + }} + QListWidget::item {{ + padding: 8px 8px; + border-radius: 8px; + margin: 1px 0; }} QListWidget::item:selected {{ - background: {CARD_ALT}; + background: {CARD_BG}; border: 1px solid {BORDER}; color: {TEXT}; - border-radius: 4px; }} - QListWidget::item {{ - padding: 6px 8px; + QTextEdit {{ + background: transparent; + border: none; }} QScrollArea {{ border: none; + background: transparent; }} QComboBox::drop-down, QSpinBox::up-button, QSpinBox::down-button {{ border: none; background: transparent; + width: 16px; }} QSplitter::handle {{ - background: {BACKGROUND}; + background: transparent; width: 8px; }} """ diff --git a/host/gui/widgets/log_panel.py b/host/gui/widgets/log_panel.py index bddec86..cf037f0 100644 --- a/host/gui/widgets/log_panel.py +++ b/host/gui/widgets/log_panel.py @@ -1,6 +1,6 @@ from time import strftime -from PySide6.QtWidgets import QFrame, QTextEdit, QVBoxLayout +from PySide6.QtWidgets import QFrame, QLabel, QTextEdit, QVBoxLayout class LogPanel(QFrame): @@ -8,7 +8,15 @@ def __init__(self) -> None: super().__init__() self.setObjectName("logPanel") layout = QVBoxLayout(self) - layout.setContentsMargins(12, 12, 12, 12) + layout.setContentsMargins(12, 10, 12, 12) + layout.setSpacing(8) + + title = QLabel("Activity") + title.setProperty("sectionTitle", True) + subtitle = QLabel("Recent device, monitor and export events") + subtitle.setProperty("muted", True) + layout.addWidget(title) + layout.addWidget(subtitle) self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) diff --git a/host/gui/widgets/sidebar.py b/host/gui/widgets/sidebar.py index 84d16a0..9cebf60 100644 --- a/host/gui/widgets/sidebar.py +++ b/host/gui/widgets/sidebar.py @@ -34,11 +34,11 @@ class Sidebar(QFrame): def __init__(self) -> None: super().__init__() self.setObjectName("sidebar") - self.setFixedWidth(258) + self.setFixedWidth(232) layout = QVBoxLayout(self) - layout.setContentsMargins(14, 14, 14, 14) - layout.setSpacing(12) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) layout.addWidget(self._build_connection_section()) layout.addWidget(self._build_monitor_section()) @@ -60,7 +60,7 @@ def _build_connection_section(self) -> QFrame: self.connect_btn = QPushButton("Connect") self.connect_btn.setProperty("accent", True) self.refresh_btn = QPushButton("Refresh") - self.load_symbols_btn = QPushButton("Symbols") + self.load_symbols_btn = QPushButton("Load Symbols") self.connect_btn.clicked.connect(self.connect_requested.emit) self.refresh_btn.clicked.connect(self.refresh_requested.emit) @@ -118,7 +118,7 @@ def _build_variable_section(self) -> QFrame: section = self._section_shell("Variables") body = self._section_body(section) - helper = QLabel("Double-click to pin a symbol into the board.") + helper = QLabel("Double-click to pin a signal into the live canvas.") helper.setProperty("muted", True) self.filter_edit = QLineEdit() self.filter_edit.setPlaceholderText("Search variables") @@ -139,15 +139,15 @@ def _section_shell(self, title: str) -> QFrame: shell = QFrame() shell.setObjectName("sectionCard") outer = QVBoxLayout(shell) - outer.setContentsMargins(12, 12, 12, 12) - outer.setSpacing(10) + outer.setContentsMargins(10, 10, 10, 10) + outer.setSpacing(8) header = QLabel(title) header.setProperty("sectionTitle", True) content = QWidget() content_layout = QVBoxLayout(content) content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(8) + content_layout.setSpacing(7) outer.addWidget(header) outer.addWidget(content) @@ -167,7 +167,7 @@ def _field(self, label: str, control: QWidget) -> QWidget: wrap = QWidget() layout = QVBoxLayout(wrap) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) + layout.setSpacing(3) caption = QLabel(label) caption.setProperty("muted", True) layout.addWidget(caption) diff --git a/host/gui/widgets/toolbar.py b/host/gui/widgets/toolbar.py index 6955272..5cbc1ff 100644 --- a/host/gui/widgets/toolbar.py +++ b/host/gui/widgets/toolbar.py @@ -7,8 +7,8 @@ def __init__(self) -> None: self.setObjectName("toolbar") layout = QHBoxLayout(self) - layout.setContentsMargins(18, 12, 18, 12) - layout.setSpacing(16) + layout.setContentsMargins(16, 10, 16, 10) + layout.setSpacing(14) brand_wrap = QWidget() brand_layout = QHBoxLayout(brand_wrap) @@ -17,16 +17,19 @@ def __init__(self) -> None: self.brand = QLabel("sparam") self.brand.setProperty("hero", True) - self.caption = QLabel("serial tuning monitor") + self.caption = QLabel("minimal signal lab") self.caption.setProperty("muted", True) brand_layout.addWidget(self.brand) brand_layout.addWidget(self.caption) brand_layout.addStretch(1) - self.state_chip = QLabel("Idle") + self.state_chip = QLabel("Offline") + self.state_chip.setProperty("state", "idle") self.state_chip.setProperty("chip", True) - self.status_label = QLabel("Pick a symbol file and start a monitor session.") + self.status_label = QLabel( + "Select symbols, arm a monitor stream, inspect response." + ) self.status_label.setProperty("muted", True) layout.addWidget(brand_wrap, 1) @@ -54,3 +57,9 @@ def set_paused(self, paused: bool) -> None: self.state_chip.setText("Offline") self.style().unpolish(self.state_chip) self.style().polish(self.state_chip) + + def set_preview(self) -> None: + self.state_chip.setText("Preview") + self.state_chip.setProperty("state", "connected") + self.style().unpolish(self.state_chip) + self.style().polish(self.state_chip) diff --git a/host/gui/widgets/value_card.py b/host/gui/widgets/value_card.py index 8c73d39..d00b330 100644 --- a/host/gui/widgets/value_card.py +++ b/host/gui/widgets/value_card.py @@ -10,6 +10,7 @@ def __init__(self, name: str, color: str) -> None: super().__init__() self.setObjectName("valueCard") self._last_value: Optional[float] = None + self.setMinimumWidth(156) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -19,21 +20,21 @@ def __init__(self, name: str, color: str) -> None: stripe.setFixedWidth(2) stripe.setStyleSheet( f"background: {color}; " - "border-top-left-radius: 8px; " - "border-bottom-left-radius: 8px;" + "border-top-left-radius: 14px; " + "border-bottom-left-radius: 14px;" ) body = QWidget() body_layout = QVBoxLayout(body) - body_layout.setContentsMargins(14, 12, 14, 12) - body_layout.setSpacing(6) + body_layout.setContentsMargins(12, 10, 12, 10) + body_layout.setSpacing(4) self.name_label = QLabel(name) self.value_label = QLabel("--") - self.value_label.setStyleSheet("font-size: 23px; font-weight: 650;") + self.value_label.setStyleSheet("font-size: 17px; font-weight: 700;") self.delta_label = QLabel("Waiting for data") self.delta_label.setProperty("muted", True) - self.delta_label.setStyleSheet("font-size: 12px;") + self.delta_label.setStyleSheet("font-size: 11px;") body_layout.addWidget(self.name_label) body_layout.addWidget(self.value_label) @@ -46,12 +47,12 @@ def update_value(self, value: float) -> None: self.value_label.setText(f"{value:.3f}") if self._last_value is None: self.delta_label.setText("First sample") - self.delta_label.setStyleSheet(f"color: {MUTED}; font-size: 12px;") + self.delta_label.setStyleSheet(f"color: {MUTED}; font-size: 11px;") else: delta = value - self._last_value direction = "up" if delta >= 0 else "down" self.delta_label.setText(f"{direction} {delta:+.3f}") self.delta_label.setStyleSheet( - f"color: {SUCCESS if delta >= 0 else ERROR}; font-size: 12px;" + f"color: {SUCCESS if delta >= 0 else ERROR}; font-size: 11px;" ) self._last_value = value diff --git a/host/gui/widgets/waveform_plot.py b/host/gui/widgets/waveform_plot.py index 25eb984..efc7a3e 100644 --- a/host/gui/widgets/waveform_plot.py +++ b/host/gui/widgets/waveform_plot.py @@ -2,7 +2,7 @@ import pyqtgraph as pg from pyqtgraph.exporters import ImageExporter -from PySide6.QtWidgets import QVBoxLayout, QWidget +from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget class WaveformPlot(QWidget): @@ -10,18 +10,31 @@ def __init__(self) -> None: super().__init__() self.setObjectName("plotPanel") layout = QVBoxLayout(self) - layout.setContentsMargins(12, 12, 12, 12) + layout.setContentsMargins(12, 10, 12, 12) + layout.setSpacing(8) + + title = QLabel("Signal Canvas") + title.setProperty("sectionTitle", True) + subtitle = QLabel( + "Overlayed monitor stream with a restrained time window and subdued grid" + ) + subtitle.setProperty("muted", True) + layout.addWidget(title) + layout.addWidget(subtitle) self.plot_widget = pg.PlotWidget() - self.plot_widget.setBackground("#fcfcfd") - self.plot_widget.showGrid(x=True, y=True, alpha=0.05) + self.plot_widget.setBackground("#fbfaf7") + self.plot_widget.showGrid(x=True, y=True, alpha=0.08) self.plot_widget.setLabel("left", "Value") self.plot_widget.setLabel("bottom", "Time", units="s") - self.plot_widget.addLegend(offset=(8, 8)) - self.plot_widget.getAxis("left").setTextPen("#6b7280") - self.plot_widget.getAxis("bottom").setTextPen("#6b7280") - self.plot_widget.getAxis("left").setPen("#d1d5db") - self.plot_widget.getAxis("bottom").setPen("#d1d5db") + self.plot_widget.setMenuEnabled(False) + self.plot_widget.hideButtons() + self.plot_widget.addLegend(offset=(10, 10), labelTextSize="10pt") + self.plot_widget.getAxis("left").setTextPen("#777064") + self.plot_widget.getAxis("bottom").setTextPen("#777064") + self.plot_widget.getAxis("left").setPen("#d7d1c7") + self.plot_widget.getAxis("bottom").setPen("#d7d1c7") + self.plot_widget.getPlotItem().layout.setContentsMargins(12, 8, 12, 8) layout.addWidget(self.plot_widget) self._curves: Dict[str, pg.PlotCurveItem] = {} @@ -33,7 +46,7 @@ def __init__(self) -> None: def add_variable(self, name: str, color: str) -> None: if name in self._curves: return - curve = self.plot_widget.plot(name=name, pen=pg.mkPen(color=color, width=1.6)) + curve = self.plot_widget.plot(name=name, pen=pg.mkPen(color=color, width=1.35)) self._curves[name] = curve self._timestamps[name] = [] self._values[name] = [] diff --git a/host/tests/test_gui_layout.py b/host/tests/test_gui_layout.py new file mode 100644 index 0000000..618030e --- /dev/null +++ b/host/tests/test_gui_layout.py @@ -0,0 +1,31 @@ +import importlib.util +import sys +from pathlib import Path +from unittest import SkipTest + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + + +def test_main_window_exposes_signal_lab_layout() -> None: + if importlib.util.find_spec("PySide6") is None: + raise SkipTest("PySide6 is not installed") + if importlib.util.find_spec("pyqtgraph") is None: + raise SkipTest("pyqtgraph is not installed") + + from PySide6.QtWidgets import QApplication + + from gui.main_window import MainWindow + + app = QApplication.instance() or QApplication([]) + window = MainWindow() + + assert window.findChild(type(window.centralWidget()), "workspaceShell") is not None + assert window.findChild(type(window.centralWidget()), "inspectorPanel") is not None + assert ( + window.findChild(type(window.centralWidget()), "signalStatsStrip") is not None + ) + + window.close() + app.quit()