From 6e766c59684a4a6819815bea36be84bb40d8dbaf Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Mon, 19 May 2025 18:17:27 -0700 Subject: [PATCH 01/44] Initial new changes --- diagnostic_interface/main_window.py | 279 +++++++++++------------- diagnostic_interface/plot_canvas.py | 78 +++---- diagnostic_interface/plot_tab.py | 98 ++++----- diagnostic_interface/settings.toml | 2 + diagnostic_interface/settings_window.py | 16 +- diagnostic_interface/timer_widget.py | 24 +- poetry.lock | 13 +- pyproject.toml | 2 + 8 files changed, 251 insertions(+), 261 deletions(-) create mode 100644 diagnostic_interface/settings.toml diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 57839bf9..e77c1f92 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -1,3 +1,5 @@ +import pathlib + from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QMainWindow, @@ -13,6 +15,8 @@ from PyQt5.QtCore import Qt from data_tools import SunbeamClient from diagnostic_interface import TimedWidget, SettingsDialog, PlotTab +from config import settings + # Interface aesthetic parameters WINDOW_TITLE = "Diagnostic Interface" @@ -28,38 +32,17 @@ def __init__(self): self.setWindowTitle(WINDOW_TITLE) self.setGeometry(X_COORD, Y_COORD, WIDTH, HEIGHT) self.tabs = QTabWidget() - self.setCentralWidget(self.tabs) - self.create_home_tab() - - # Timer to refresh plots - self.timer = TimedWidget( - 120000, self.refresh_all_tabs - ) # Timer refreshes after 120 seconds - def refresh_all_tabs(self): - """Refreshes all open plot tabs by requerying data and replotting it.""" - for i in range(self.tabs.count()): - widget = self.tabs.widget(i) - if isinstance(widget, PlotTab): - widget.refresh_plot() - - def create_home_tab(self) -> None: - """ - Creates home tab for the diagnostic interface. The home tab - contains dropdown menus so that we can choose what data we - want to query and plot. - """ + self.setCentralWidget(self.tabs) home_widget = QWidget() layout = QVBoxLayout() - self.client = SunbeamClient() - # Aesthetic changes - home_widget.setStyleSheet("background-color: #4bb4de;") + self.client = SunbeamClient(settings.sunbeam_api_url) # Load and add the team logo logo_label = QLabel() logo_label.setPixmap( - QPixmap("Solar_Logo.png").scaled(800, 600, Qt.KeepAspectRatio) + QPixmap(str(pathlib.Path(__file__).parent / "Solar_Logo.png")).scaled(800, 600, Qt.KeepAspectRatio) ) logo_label.setAlignment(Qt.AlignCenter) # Center the image layout.addWidget(logo_label) @@ -76,27 +59,18 @@ def create_home_tab(self) -> None: self.source_input = QComboBox() self.data_input = QComboBox() - # Load initial data - self.origins = self.client.distinct("origin", []) - self.origin_input.addItems(self.origins) - - self.events = self.client.distinct("event", []) - self.event_input.addItems(self.events) - - self.sources = self.client.distinct("source", []) - self.source_input.addItems(self.sources) + self.origin_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.event_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.source_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.data_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) - self.data_types = self.client.distinct("name", []) - self.data_input.addItems(self.data_types) + self.origin_input.view().setFixedWidth(200) + self.event_input.view().setFixedWidth(200) + self.source_input.view().setFixedWidth(200) + self.data_input.view().setFixedWidth(200) - # Style of the drowpdown menus - for combo in [ - self.origin_input, - self.source_input, - self.event_input, - self.data_input, - ]: - combo.setStyleSheet("background-color: white") + # Load initial data from the API + self.update_filters() # Add to form layout form_layout.addRow("Origin:", self.origin_input) @@ -109,13 +83,11 @@ def create_home_tab(self) -> None: # Button to load the plot submit_button = QPushButton("Load Data") submit_button.clicked.connect(self.create_plot_tab) - submit_button.setStyleSheet("background-color: white") layout.addWidget(submit_button) #Settings button settings_button = QPushButton("Settings") settings_button.clicked.connect(self.edit_settings) - settings_button.setStyleSheet("background-color: white") layout.addWidget(settings_button) home_widget.setLayout(layout) @@ -126,103 +98,109 @@ def create_home_tab(self) -> None: self.event_input.currentTextChanged.connect(self.update_filters) self.source_input.currentTextChanged.connect(self.update_filters) - def update_filters(self) -> None: - """ - Updates all dropdown options based on selected values. This way, you should only - be able to form query combinations that are available. For example, if you choose - 'realtime' as your origin, in the events dropdown you should only see 'FSGP_2024_Day_1' - because that is the only event associated with the 'realtime' pipeline. + # Timer to refresh plots + self.timer = TimedWidget(settings.plot_timer_interval, self.refresh_all_tabs) + + def refresh_all_tabs(self): + """Refreshes all open plot tabs by requerying data and replotting it.""" + for i in range(self.tabs.count()): + widget = self.tabs.widget(i) + if isinstance(widget, PlotTab): + widget.refresh_plot() - :raises Exception: if there is an error while updating the dropdown options. + def update_filters(self): + """ + Updates the dropdown options based on the selected values. + If the API request fails, no data is loaded. """ try: - self.client = SunbeamClient() + # Fetch selected values from the dropdowns - # Initial text selected_origin = self.origin_input.currentText() selected_source = self.source_input.currentText() selected_event = self.event_input.currentText() selected_data = self.data_input.currentText() - # Get valid events based on origin - available_events = set( - self.client.distinct("event", []) - ) # Start with all events - if selected_origin: - available_events &= set( - self.client.distinct("event", {"origin": selected_origin}) - ) # Filter by origin - - # Get valid sources based on origin and event - available_sources = set(self.client.distinct("source", [])) # Start with all - if selected_origin: - available_sources &= set( - self.client.distinct("source", {"origin": selected_origin}) - ) # Filter by origin - if selected_event: - available_sources &= set( - self.client.distinct("source", {"event": selected_event}) - ) # Filter by event - - # Get valid data types based on origin, source, and event - available_data = set(self.client.distinct("name", [])) # Start with all data - if selected_origin: - available_data &= set( - self.client.distinct("name", {"origin": selected_origin}) - ) # Filter by origin - if selected_event: - available_data &= set( - self.client.distinct("name", {"event": selected_event}) - ) # Filter by event - if selected_source: - available_data &= set( - self.client.distinct("name", {"source": selected_source}) - ) # Filter by source - - # Convert back to lists - available_sources = list(available_sources) - available_events = list(available_events) - available_data = list(available_data) - - # Update dropdowns safely - self.source_input.blockSignals(True) # Shuts down ability to take input - self.source_input.clear() - self.source_input.addItems(available_sources) - # Set the selected source to the first available or keep it if it exists - if selected_source in available_sources: - self.source_input.setCurrentText(selected_source) - elif available_sources: - self.source_input.setCurrentText( - available_sources[0] - ) # Select first available option - self.source_input.blockSignals(False) # Can take inputs again - - self.event_input.blockSignals(True) # Can't take inputs - self.event_input.clear() - self.event_input.addItems(available_events) - # Set the selected event to the first available or keep it if it exists - if selected_event in available_events: - self.event_input.setCurrentText(selected_event) - elif available_events: - self.event_input.setCurrentText( - available_events[0] - ) # Select first available option - self.event_input.blockSignals(False) # Can take inputs again - - self.data_input.blockSignals(True) # Can't take inputs - self.data_input.clear() - self.data_input.addItems(available_data) - # Set the selected data to the first available or keep it if it exists - if selected_data in available_data: - self.data_input.setCurrentText(selected_data) - elif available_data: - self.data_input.setCurrentText( - available_data[0] - ) # Select first available option - self.data_input.blockSignals(False) # Can take inputs again + # Filter available events, sources, and data types based on selections + available_origins, available_sources, available_events, available_data = self.filter_data() + + # Update dropdowns + self.update_dropdown(self.origin_input, available_origins, selected_origin) + self.update_dropdown(self.event_input, available_events, selected_event) + self.update_dropdown(self.source_input, available_sources, selected_source) + self.update_dropdown(self.data_input, available_data, selected_data) except Exception as e: print(f"Error updating filters: {e}") + self.clear_dropdowns() + + def filter_data(self) -> tuple[list, list, list, list]: + """ + Filter the available options for a given field based on selected filters. + """ + selected_origin = self.origin_input.currentText() + selected_source = self.source_input.currentText() + selected_event = self.event_input.currentText() + + available_origins = set(self.client.distinct("origin", {})) + + # Get valid events based on origin + available_events = set(self.client.distinct("event", {})) + if selected_origin: + available_events &= set(self.client.distinct("event", {"origin": selected_origin})) + + # Get valid sources based on origin and event + available_sources = set(self.client.distinct("source", {})) + if selected_origin: + # Filter by origin + available_sources &= set(self.client.distinct("source", {"origin": selected_origin})) + if selected_event: + available_sources &= set( + self.client.distinct("source", {"event": selected_event}) + ) # Filter by event + + # Get valid data types based on origin, source, and event + available_data = set(self.client.distinct("name", {})) # Start with all data + if selected_origin: + available_data &= set( + self.client.distinct("name", {"origin": selected_origin}) + ) # Filter by origin + if selected_event: + available_data &= set( + self.client.distinct("name", {"event": selected_event}) + ) # Filter by event + if selected_source: + available_data &= set( + self.client.distinct("name", {"source": selected_source}) + ) # Filter by source + + # Convert back to lists + available_origins = list(available_origins) + available_sources = list(available_sources) + available_events = list(available_events) + available_data = list(available_data) + + return available_origins, available_sources, available_events, available_data + + def update_dropdown(self, dropdown: QComboBox, available_data: list, selected_value: str): + """ Update the dropdown options and select the appropriate value """ + dropdown.blockSignals(True) + dropdown.clear() + dropdown.addItems(available_data) + + # Select the previously selected value if available + if selected_value in available_data: + dropdown.setCurrentText(selected_value) + elif available_data: + dropdown.setCurrentText(available_data[0]) # Select the first available option + dropdown.blockSignals(False) + + def clear_dropdowns(self): + """ Clear all dropdowns in case of API failure """ + self.origin_input.clear() + self.event_input.clear() + self.source_input.clear() + self.data_input.clear() def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. @@ -237,7 +215,7 @@ def create_plot_tab(self): # Creating PlotTab object and adding it to the list of tabs. plot_tab = PlotTab(origin, source, event, data_name) - self.tabs.addTab(plot_tab, f"{data_name} - {event} - {origin} - {source}") + self.tabs.addTab(plot_tab, f"{data_name}") plot_tab.close_requested.connect(self.close_tab) @@ -247,37 +225,28 @@ def close_tab(self, widget) -> None: :param QWidget widget: an element of the GUI you can interact with. In this case, it is the plot. """ - index: int = self.tabs.indexOf( - widget - ) # Checks the index of the tab we want to close; if the tab is not in self.tabs, returns -1 - if ( - index != -1 - ): # Checks that the tab we want to close is in self.tabs. If it isn't (index == -1), do nothing - self.tabs.removeTab( - index - ) # If the tab is in self.tabs (index!= -1), we remove it - + # Checks the index of the tab we want to close; if the tab is not in self.tabs, returns -1 + index: int = self.tabs.indexOf(widget) + if index != -1: # Checks that the tab we want to close is in self.tabs. If it isn't (index == -1), do nothing + self.tabs.removeTab(index) # If the tab is in self.tabs (index!= -1), we remove it def edit_settings(self): """Opens a dialog to change the settings of the interface. We can change the interval between the data is refreshed, as well as the url from the client where we query from.""" - current_interval = self.timer.interval - current_client_address = self.client.__class__.__name__ + current_interval = settings.plot_timer_interval + current_client_address = settings.sunbeam_api_url dialog = SettingsDialog(current_interval, current_client_address, self) if dialog.exec_(): # if user pressed OK - new_interval, new_client_name = dialog.get_settings() - self.timer.interval = new_interval * 1000 # convert back to ms + new_plot_interval, new_client_address = dialog.get_settings() + self.timer.set_interval(new_plot_interval) + + settings.plot_timer_interval = new_plot_interval # Change client - if new_client_name != current_client_address: - if new_client_name == "SunbeamClient": - self.client = SunbeamClient() - elif new_client_name == "OtherClient": - print("Other client selected.") - #from data_tools import OtherClient - #self.client = OtherClient() - - # Reload filters with new client - self.update_filters() \ No newline at end of file + if new_client_address != current_client_address: + settings.sunbeam_api_url = new_client_address + self.client = SunbeamClient(new_client_address) + self.update_filters() + self.refresh_all_tabs() diff --git a/diagnostic_interface/plot_canvas.py b/diagnostic_interface/plot_canvas.py index 48e6b822..b9554ce7 100644 --- a/diagnostic_interface/plot_canvas.py +++ b/diagnostic_interface/plot_canvas.py @@ -1,30 +1,29 @@ import pandas as pd import matplotlib.pyplot as plt +import matplotlib.dates as mdates from PyQt5.QtWidgets import QMessageBox, QFileDialog from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from data_tools import SunbeamClient, TimeSeries +from config import settings + +# Use a light-theme friendly style +# plt.style.use("seaborn-v0_8-dark") +plt.style.use("fivethirtyeight") class PlotCanvas(FigureCanvas): - """A PlotCanvas is the space on which we insert our toolbar - and our plots.""" def __init__(self, parent=None): self.fig, self.ax = plt.subplots() super().__init__(self.fig) self.setParent(parent) + self.current_data = None + self.current_data_name = "" + self.current_event = "" + self.current_origin = "" + self.current_source = "" def query_and_plot(self, origin, source, event, data_name): - """ - This method calls on query_data and then plots the data returned. - - :param str origin: pipeline name - :param str source: pipeline stage - :param str event: race type and race day. - :param str data_name: the type of data that is being queried (e.g. Vehicle_Velocity). - :raises TypeError: if the data is not a TimeSeries - :returns bool: depending on whether it was possible to query and plot the data - """ try: data = self.query_data(origin, source, event, data_name) if not isinstance(data, TimeSeries): @@ -35,44 +34,33 @@ def query_and_plot(self, origin, source, event, data_name): self.current_event = event self.current_origin = origin self.current_source = source + self.ax.clear() - self.ax.plot(data.datetime_x_axis, data) - self.ax.set_title(f"{data_name} - {event}") + self.ax.plot(data.datetime_x_axis, data, linewidth=1) + self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.ax.set_xlabel("Time", fontsize=10) + self.ax.set_ylabel(data_name, fontsize=10) + + # Improve datetime formatting + self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + self.ax.xaxis.set_major_locator(mdates.HourLocator()) + self.fig.autofmt_xdate() + + self.fig.tight_layout() self.draw() + return True + except Exception as e: - QMessageBox.critical( - None, "Plotting Error", f"Error fetching or plotting data:\n{str(e)}" - ) + QMessageBox.critical(None, "Plotting Error", f"Error fetching or plotting data:\n{str(e)}") + return False def query_data(self, origin, source, event, data_name): - """ - This method queries data from SunBeam as a file, and later unwraps it. - - :param str origin: pipeline name - :param str source: pipeline stage - :param str event: race type and race day. - :param str data_name: the type of data that is being queried (e.g. Vehicle_Velocity). - :raises ValueError: if it is not possible to extract data from the queried file. - :returns: a TimeSeries with the values you wanted to query. - """ - client = SunbeamClient() + client = SunbeamClient(settings.sunbeam_api_url) file = client.get_file(origin, event, source, data_name) result = file.unwrap() return result.values if hasattr(result, "values") else result.data - def refresh_plot(self): - """Updates current plot by rerunning query_and_plot.""" - self.query_and_plot( - self.current_origin, - self.current_source, - self.current_event, - self.current_data_name, - ) - def save_data_to_csv(self): - """ - Saves the current data as a CSV file. - """ if self.current_data is None: print("No data available to save.") return @@ -87,11 +75,9 @@ def save_data_to_csv(self): ) if file_name: - df = pd.DataFrame( - { - "Time (s)": range(len(self.current_data)), - f"{self.current_data_name}": self.current_data, - } - ) + df = pd.DataFrame({ + "Time": self.current_data.datetime_x_axis, + f"{self.current_data_name}": self.current_data, + }) df.to_csv(file_name, index=False) print(f"Data saved to {file_name}") diff --git a/diagnostic_interface/plot_tab.py b/diagnostic_interface/plot_tab.py index 548da11c..7299ea31 100644 --- a/diagnostic_interface/plot_tab.py +++ b/diagnostic_interface/plot_tab.py @@ -1,73 +1,73 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QMessageBox -from diagnostic_interface import CustomNavigationToolbar, PlotCanvas +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QMessageBox, + QGroupBox, QHBoxLayout +) from PyQt5.QtCore import pyqtSignal +from diagnostic_interface import CustomNavigationToolbar, PlotCanvas -# Dictionary to store help messages for each plot HELP_MESSAGES = { "VehicleVelocity": "This plot shows velocity over time.\n\n" - "- X-axis: Time (s)\n" - "- Y-axis: Velocity (m/s)\n" - "- Data is sourced from the car's telemetry system.\n", + "- X-axis: Time\n" + "- Y-axis: Velocity (m/s)\n" + "- Data is sourced from the car's telemetry system.\n", } + class PlotTab(QWidget): - """ - A PlotTab is the plot that we insert unto our PlotCanvas. - """ - close_requested = pyqtSignal( - QWidget - ) # Signal to notify MainWindow to close this tab. - - def __init__(self, origin:str, source:str, event:str, data_name:str, parent=None): - """ - :param str origin: pipeline name - :param str source: pipeline stage - :param str event: race type and race day. - :param str data_name: the type of data that is being queried (e.g. Vehicle_Velocity). - """ + close_requested = pyqtSignal(QWidget) + + def __init__(self, origin: str, source: str, event: str, data_name: str, parent=None): super().__init__(parent) + + self.origin = origin + self.source = source + self.event = event + self.data_name = data_name + + # Layout setup self.layout = QVBoxLayout(self) + self.layout.setSpacing(10) + self.layout.setContentsMargins(15, 15, 15, 15) + self.plot_canvas = PlotCanvas(self) - self.toolbar = CustomNavigationToolbar( - canvas=self.plot_canvas - ) # Creating toolbar - self.layout.addWidget(self.toolbar) # Adding toolbar - self.layout.addWidget(self.plot_canvas) # Adding space for plots - self.setLayout(self.layout) - - # Help Button that displays more information on the graph + self.toolbar = CustomNavigationToolbar(canvas=self.plot_canvas) + + # Buttons help_button = QPushButton("Help") + help_button.setObjectName("helpButton") help_button.clicked.connect(lambda: self.show_help_message(data_name, event)) - self.layout.addWidget(help_button) - # Button that closes the tab with the plot close_button = QPushButton("Close Tab") + close_button.setObjectName("closeButton") close_button.clicked.connect(self.request_close) - self.layout.addWidget(close_button) - # Query and plot the initial data - self.plot_canvas.query_and_plot(origin, source, event, data_name) + button_group = QGroupBox("Actions") + button_layout = QHBoxLayout() + button_layout.addWidget(help_button) + button_layout.addWidget(close_button) + button_group.setLayout(button_layout) + + self.layout.addWidget(self.toolbar) + self.layout.addWidget(self.plot_canvas) + self.layout.addWidget(button_group) + + self.setStyleSheet(""" + QPushButton#helpButton, QPushButton#closeButton { + padding: 6px 12px; + border-radius: 8px; + } + """) + + if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): + self.request_close() def refresh_plot(self): - """Updates the data that exists in plot_canvas by rerunning query_and_plot - and replacing the current instance with the new result.""" - self.plot_canvas.query_and_plot( - self.plot_canvas.current_origin, - self.plot_canvas.current_source, - self.plot_canvas.current_event, - self.plot_canvas.current_data_name, - ) + if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): + self.request_close() def request_close(self): - """Emit signal to request closing this tab.""" self.close_requested.emit(self) def show_help_message(self, data_name, event): - """ - After you have clicked on the help button, this function displays a help message with more information on the plot. - """ - message = HELP_MESSAGES.get( - data_name, "No specific help available for this plot." - ) - + message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") QMessageBox.information(self, f"Help: {data_name}", message) diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml new file mode 100644 index 00000000..51ce7eb6 --- /dev/null +++ b/diagnostic_interface/settings.toml @@ -0,0 +1,2 @@ +plot_timer_interval = 15 +sunbeam_api_url = "api.sunbeam.ubcsolar.com" diff --git a/diagnostic_interface/settings_window.py b/diagnostic_interface/settings_window.py index 85739e1b..2eb1afac 100644 --- a/diagnostic_interface/settings_window.py +++ b/diagnostic_interface/settings_window.py @@ -1,4 +1,5 @@ -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSpinBox, QComboBox, QFormLayout +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSpinBox, QLineEdit, QFormLayout + class SettingsDialog(QDialog): def __init__(self, current_interval, current_client_address, parent=None): @@ -14,15 +15,13 @@ def __init__(self, current_interval, current_client_address, parent=None): # Refresh interval selector self.interval_spinbox = QSpinBox() - self.interval_spinbox.setRange(10, 3600) # in seconds - self.interval_spinbox.setValue(current_interval // 1000) # convert ms to s + self.interval_spinbox.setValue(current_interval) # convert ms to s layout.addRow("Refresh Interval (s):", self.interval_spinbox) # Client selector - self.client_combo = QComboBox() - self.client_combo.addItems(["SunbeamClient", "OtherClient"]) # Add more if needed - self.client_combo.setCurrentText(current_client_address) - layout.addRow("Data Client:", self.client_combo) + self.client_input = QLineEdit() + self.client_input.setText(current_client_address) + layout.addRow("Sunbeam API URL:", self.client_input) # OK/Cancel buttons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) @@ -33,4 +32,5 @@ def __init__(self, current_interval, current_client_address, parent=None): self.setLayout(layout) def get_settings(self): - return self.interval_spinbox.value(), self.client_combo.currentText() + print(self.interval_spinbox.value()) + return self.interval_spinbox.value(), self.client_input.text() diff --git a/diagnostic_interface/timer_widget.py b/diagnostic_interface/timer_widget.py index aba20f84..b2a5f053 100644 --- a/diagnostic_interface/timer_widget.py +++ b/diagnostic_interface/timer_widget.py @@ -12,6 +12,26 @@ def __init__(self, interval: int, callback: Callable): :param Callable callback: the function to be called on timeout. """ self.timer = QTimer() - self.interval = interval + self.interval = interval * 1000 self.timer.timeout.connect(callback) - self.timer.start(interval) + + # Delay starting the timer until the event loop has fully initialized + QTimer.singleShot(0, self.start_timer) # Starts the timer after the event loop is ready + + def start_timer(self): + """Start the timer.""" + self.timer.start(self.interval) + + def set_interval(self, interval: int): + """Update the timer interval and restart the timer.""" + self.interval = interval * 1000 + self.timer.stop() # Stop the current timer + self.timer.start(interval) # Restart the timer with the new interval + + def stop(self): + """Stop the timer.""" + self.timer.stop() + + def start(self): + """Start the timer with the current interval.""" + self.timer.start(self.interval) diff --git a/poetry.lock b/poetry.lock index 02cd060f..fd8b8d18 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2910,6 +2910,17 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + [[package]] name = "tornado" version = "6.4.2" @@ -3121,4 +3132,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "1b9e715ce97b1de5e63291ce4039de9c8ec797090aa0e9716a86dc7148dad061" +content-hash = "72c9fc4069921e302b05f118d1154bd1ae437d2d7aa0ad1ccd414fa519782d4c" diff --git a/pyproject.toml b/pyproject.toml index a49f62c9..61ac905e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,3 +76,5 @@ anytree = "^2.12.1" folium = "^0.19.5" pyqt5-stubs = "^5.15.6.0" pyqtwebengine = "^5.15.7" +tomli = "^2.2.1" +tomli-w = "^1.2.0" From ee749a18532ba75470cd4800ca71da08ee4356eb Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Mon, 19 May 2025 20:08:03 -0700 Subject: [PATCH 02/44] More things --- diagnostic_interface/__init__.py | 4 +- diagnostic_interface/data_select.py | 152 ++++++++++++++++++++++++++++ diagnostic_interface/main_window.py | 138 ++----------------------- diagnostic_interface/plot_canvas.py | 4 +- 4 files changed, 164 insertions(+), 134 deletions(-) create mode 100644 diagnostic_interface/data_select.py diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index ee0c8a53..c640918b 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -3,11 +3,13 @@ from .custom_toolbar import CustomNavigationToolbar from .plot_canvas import PlotCanvas from .plot_tab import PlotTab +from .data_select import DataSelect __all__ = [ "TimedWidget", "SettingsDialog", "CustomNavigationToolbar", "PlotCanvas", - "PlotTab" + "PlotTab", + "DataSelect" ] diff --git a/diagnostic_interface/data_select.py b/diagnostic_interface/data_select.py new file mode 100644 index 00000000..13d4d907 --- /dev/null +++ b/diagnostic_interface/data_select.py @@ -0,0 +1,152 @@ + +from PyQt5.QtWidgets import QComboBox, QFormLayout +from data_tools import SunbeamClient +from config import settings + + +class DataSelect(QFormLayout): + def __init__(self, parent=None): + super().__init__(parent) + + # Dropdown menus + self.origin_input = QComboBox() + self.event_input = QComboBox() + self.source_input = QComboBox() + self.data_input = QComboBox() + + self.origin_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.event_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.source_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.data_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) + + self.origin_input.view().setFixedWidth(200) + self.event_input.view().setFixedWidth(200) + self.source_input.view().setFixedWidth(200) + self.data_input.view().setFixedWidth(200) + + # Load initial data from the API + self.update_filters() + + # Add to form layout + self.addRow("Origin:", self.origin_input) + self.addRow("Event:", self.event_input) + self.addRow("Source:", self.source_input) + self.addRow("Data:", self.data_input) + + # Callbacks to update dropdowns to only show existing query combinations + self.origin_input.currentTextChanged.connect(self.update_filters) + self.event_input.currentTextChanged.connect(self.update_filters) + self.source_input.currentTextChanged.connect(self.update_filters) + + @property + def selected_origin(self): + return self.origin_input.currentText() + + @property + def selected_event(self): + return self.event_input.currentText() + + @property + def selected_source(self): + return self.source_input.currentText() + + @property + def selected_data(self): + return self.data_input.currentText() + + def update_filters(self): + """ + Updates the dropdown options based on the selected values. + If the API request fails, no data is loaded. + """ + try: + # Fetch selected values from the dropdowns + + selected_origin = self.origin_input.currentText() + selected_source = self.source_input.currentText() + selected_event = self.event_input.currentText() + selected_data = self.data_input.currentText() + + # Filter available events, sources, and data types based on selections + available_origins, available_sources, available_events, available_data = self.filter_data() + + # Update dropdowns + self.update_dropdown(self.origin_input, available_origins, selected_origin) + self.update_dropdown(self.event_input, available_events, selected_event) + self.update_dropdown(self.source_input, available_sources, selected_source) + self.update_dropdown(self.data_input, available_data, selected_data) + + except Exception as e: + print(f"Error updating filters: {e}") + self.clear_dropdowns() + + def filter_data(self) -> tuple[list, list, list, list]: + """ + Filter the available options for a given field based on selected filters. + """ + selected_origin = self.origin_input.currentText() + selected_source = self.source_input.currentText() + selected_event = self.event_input.currentText() + + client = SunbeamClient(settings.sunbeam_api_url) + + available_origins = set(client.distinct("origin", {})) + + # Get valid events based on origin + available_events = set(client.distinct("event", {})) + if selected_origin: + available_events &= set(client.distinct("event", {"origin": selected_origin})) + + # Get valid sources based on origin and event + available_sources = set(client.distinct("source", {})) + if selected_origin: + # Filter by origin + available_sources &= set(client.distinct("source", {"origin": selected_origin})) + if selected_event: + available_sources &= set( + client.distinct("source", {"event": selected_event}) + ) # Filter by event + + # Get valid data types based on origin, source, and event + available_data = set(client.distinct("name", {})) # Start with all data + if selected_origin: + available_data &= set( + client.distinct("name", {"origin": selected_origin}) + ) # Filter by origin + if selected_event: + available_data &= set( + client.distinct("name", {"event": selected_event}) + ) # Filter by event + if selected_source: + available_data &= set( + client.distinct("name", {"source": selected_source}) + ) # Filter by source + + # Convert back to lists + available_origins = list(available_origins) + available_sources = list(available_sources) + available_events = list(available_events) + available_data = list(available_data) + + return available_origins, available_sources, available_events, available_data + + @staticmethod + def update_dropdown(dropdown: QComboBox, available_data: list, selected_value: str): + """ Update the dropdown options and select the appropriate value """ + dropdown.blockSignals(True) + dropdown.clear() + dropdown.addItems(available_data) + + # Select the previously selected value if available + if selected_value in available_data: + dropdown.setCurrentText(selected_value) + elif available_data: + dropdown.setCurrentText(available_data[0]) # Select the first available option + dropdown.blockSignals(False) + + def clear_dropdowns(self): + """ Clear all dropdowns in case of API failure """ + self.origin_input.clear() + self.event_input.clear() + self.source_input.clear() + self.data_input.clear() diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index e77c1f92..429a7670 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -14,7 +14,7 @@ from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt from data_tools import SunbeamClient -from diagnostic_interface import TimedWidget, SettingsDialog, PlotTab +from diagnostic_interface import TimedWidget, SettingsDialog, PlotTab, DataSelect from config import settings @@ -51,34 +51,9 @@ def __init__(self): title_label = QLabel("Select Data to Plot") title_label.setFont(QFont("Arial", 20)) layout.addWidget(title_label) - form_layout = QFormLayout() - # Dropdown menus - self.origin_input = QComboBox() - self.event_input = QComboBox() - self.source_input = QComboBox() - self.data_input = QComboBox() - - self.origin_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) - self.event_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) - self.source_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) - self.data_input.setSizeAdjustPolicy(QComboBox.AdjustToContents) - - self.origin_input.view().setFixedWidth(200) - self.event_input.view().setFixedWidth(200) - self.source_input.view().setFixedWidth(200) - self.data_input.view().setFixedWidth(200) - - # Load initial data from the API - self.update_filters() - - # Add to form layout - form_layout.addRow("Origin:", self.origin_input) - form_layout.addRow("Event:", self.event_input) - form_layout.addRow("Source:", self.source_input) - form_layout.addRow("Data:", self.data_input) - - layout.addLayout(form_layout) + self.data_select_form = DataSelect() + layout.addLayout(self.data_select_form) # Button to load the plot submit_button = QPushButton("Load Data") @@ -93,11 +68,6 @@ def __init__(self): home_widget.setLayout(layout) self.tabs.addTab(home_widget, "Home") - # Callbacks to update dropdowns to only show existing query combinations - self.origin_input.currentTextChanged.connect(self.update_filters) - self.event_input.currentTextChanged.connect(self.update_filters) - self.source_input.currentTextChanged.connect(self.update_filters) - # Timer to refresh plots self.timer = TimedWidget(settings.plot_timer_interval, self.refresh_all_tabs) @@ -108,110 +78,16 @@ def refresh_all_tabs(self): if isinstance(widget, PlotTab): widget.refresh_plot() - def update_filters(self): - """ - Updates the dropdown options based on the selected values. - If the API request fails, no data is loaded. - """ - try: - # Fetch selected values from the dropdowns - - selected_origin = self.origin_input.currentText() - selected_source = self.source_input.currentText() - selected_event = self.event_input.currentText() - selected_data = self.data_input.currentText() - - # Filter available events, sources, and data types based on selections - available_origins, available_sources, available_events, available_data = self.filter_data() - - # Update dropdowns - self.update_dropdown(self.origin_input, available_origins, selected_origin) - self.update_dropdown(self.event_input, available_events, selected_event) - self.update_dropdown(self.source_input, available_sources, selected_source) - self.update_dropdown(self.data_input, available_data, selected_data) - - except Exception as e: - print(f"Error updating filters: {e}") - self.clear_dropdowns() - - def filter_data(self) -> tuple[list, list, list, list]: - """ - Filter the available options for a given field based on selected filters. - """ - selected_origin = self.origin_input.currentText() - selected_source = self.source_input.currentText() - selected_event = self.event_input.currentText() - - available_origins = set(self.client.distinct("origin", {})) - - # Get valid events based on origin - available_events = set(self.client.distinct("event", {})) - if selected_origin: - available_events &= set(self.client.distinct("event", {"origin": selected_origin})) - - # Get valid sources based on origin and event - available_sources = set(self.client.distinct("source", {})) - if selected_origin: - # Filter by origin - available_sources &= set(self.client.distinct("source", {"origin": selected_origin})) - if selected_event: - available_sources &= set( - self.client.distinct("source", {"event": selected_event}) - ) # Filter by event - - # Get valid data types based on origin, source, and event - available_data = set(self.client.distinct("name", {})) # Start with all data - if selected_origin: - available_data &= set( - self.client.distinct("name", {"origin": selected_origin}) - ) # Filter by origin - if selected_event: - available_data &= set( - self.client.distinct("name", {"event": selected_event}) - ) # Filter by event - if selected_source: - available_data &= set( - self.client.distinct("name", {"source": selected_source}) - ) # Filter by source - - # Convert back to lists - available_origins = list(available_origins) - available_sources = list(available_sources) - available_events = list(available_events) - available_data = list(available_data) - - return available_origins, available_sources, available_events, available_data - - def update_dropdown(self, dropdown: QComboBox, available_data: list, selected_value: str): - """ Update the dropdown options and select the appropriate value """ - dropdown.blockSignals(True) - dropdown.clear() - dropdown.addItems(available_data) - - # Select the previously selected value if available - if selected_value in available_data: - dropdown.setCurrentText(selected_value) - elif available_data: - dropdown.setCurrentText(available_data[0]) # Select the first available option - dropdown.blockSignals(False) - - def clear_dropdowns(self): - """ Clear all dropdowns in case of API failure """ - self.origin_input.clear() - self.event_input.clear() - self.source_input.clear() - self.data_input.clear() - def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. This method contains a connection to the request_close method of the PlotTab class to receive the signal to close a tab.""" # Getting the values that we will query. - origin: str = self.origin_input.currentText() - source: str = self.source_input.currentText() - event: str = self.event_input.currentText() - data_name: str = self.data_input.currentText() + origin: str = self.data_select_form.selected_origin + source: str = self.data_select_form.selected_source + event: str = self.data_select_form.selected_event + data_name: str = self.data_select_form.selected_data # Creating PlotTab object and adding it to the list of tabs. plot_tab = PlotTab(origin, source, event, data_name) diff --git a/diagnostic_interface/plot_canvas.py b/diagnostic_interface/plot_canvas.py index b9554ce7..9cfb2d46 100644 --- a/diagnostic_interface/plot_canvas.py +++ b/diagnostic_interface/plot_canvas.py @@ -7,8 +7,8 @@ from config import settings # Use a light-theme friendly style -# plt.style.use("seaborn-v0_8-dark") -plt.style.use("fivethirtyeight") +plt.style.use("seaborn-v0_8-darkgrid") +# plt.style.use("fivethirtyeight") class PlotCanvas(FigureCanvas): From f4c84caba45db4b72dfc06f6a3b35879e238142a Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Thu, 22 May 2025 23:28:47 -0700 Subject: [PATCH 03/44] Current state --- diagnostic_interface/__init__.py | 15 +- diagnostic_interface/canvas/__init__.py | 7 + .../{ => canvas}/custom_toolbar.py | 0 .../{ => canvas}/plot_canvas.py | 30 ++- diagnostic_interface/dialog/__init__.py | 5 + .../{ => dialog}/settings_window.py | 1 - diagnostic_interface/main_window.py | 36 +-- diagnostic_interface/settings.toml | 4 +- diagnostic_interface/tabs/__init__.py | 9 + diagnostic_interface/tabs/_updatable.py | 11 + diagnostic_interface/{ => tabs}/plot_tab.py | 53 +++- diagnostic_interface/tabs/sunbeam_panel.py | 254 ++++++++++++++++++ diagnostic_interface/widgets/__init__.py | 7 + .../{ => widgets}/data_select.py | 88 +++--- .../{ => widgets}/timer_widget.py | 2 +- poetry.lock | 73 ++++- pyproject.toml | 125 ++++----- 17 files changed, 576 insertions(+), 144 deletions(-) create mode 100644 diagnostic_interface/canvas/__init__.py rename diagnostic_interface/{ => canvas}/custom_toolbar.py (100%) rename diagnostic_interface/{ => canvas}/plot_canvas.py (74%) create mode 100644 diagnostic_interface/dialog/__init__.py rename diagnostic_interface/{ => dialog}/settings_window.py (96%) create mode 100644 diagnostic_interface/tabs/__init__.py create mode 100644 diagnostic_interface/tabs/_updatable.py rename diagnostic_interface/{ => tabs}/plot_tab.py (59%) create mode 100644 diagnostic_interface/tabs/sunbeam_panel.py create mode 100644 diagnostic_interface/widgets/__init__.py rename diagnostic_interface/{ => widgets}/data_select.py (65%) rename diagnostic_interface/{ => widgets}/timer_widget.py (93%) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index c640918b..ad8b4187 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -1,9 +1,8 @@ -from .timer_widget import TimedWidget -from .settings_window import SettingsDialog -from .custom_toolbar import CustomNavigationToolbar -from .plot_canvas import PlotCanvas -from .plot_tab import PlotTab -from .data_select import DataSelect +from .config import settings +from widgets import TimedWidget, DataSelect +from dialog import SettingsDialog +from canvas import PlotCanvas, CustomNavigationToolbar +from tabs import DockerStackTab, PlotTab __all__ = [ "TimedWidget", @@ -11,5 +10,7 @@ "CustomNavigationToolbar", "PlotCanvas", "PlotTab", - "DataSelect" + "DataSelect", + "DockerStackTab", + "settings" ] diff --git a/diagnostic_interface/canvas/__init__.py b/diagnostic_interface/canvas/__init__.py new file mode 100644 index 00000000..00bf973c --- /dev/null +++ b/diagnostic_interface/canvas/__init__.py @@ -0,0 +1,7 @@ +from .custom_toolbar import CustomNavigationToolbar +from .plot_canvas import PlotCanvas + +__all__ = [ + "CustomNavigationToolbar", + "PlotCanvas" +] diff --git a/diagnostic_interface/custom_toolbar.py b/diagnostic_interface/canvas/custom_toolbar.py similarity index 100% rename from diagnostic_interface/custom_toolbar.py rename to diagnostic_interface/canvas/custom_toolbar.py diff --git a/diagnostic_interface/plot_canvas.py b/diagnostic_interface/canvas/plot_canvas.py similarity index 74% rename from diagnostic_interface/plot_canvas.py rename to diagnostic_interface/canvas/plot_canvas.py index 9cfb2d46..7cbbd1e1 100644 --- a/diagnostic_interface/plot_canvas.py +++ b/diagnostic_interface/canvas/plot_canvas.py @@ -4,7 +4,7 @@ from PyQt5.QtWidgets import QMessageBox, QFileDialog from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from data_tools import SunbeamClient, TimeSeries -from config import settings +from diagnostic_interface import settings # Use a light-theme friendly style plt.style.use("seaborn-v0_8-darkgrid") @@ -23,6 +23,8 @@ def __init__(self, parent=None): self.current_origin = "" self.current_source = "" + self.line = None + def query_and_plot(self, origin, source, event, data_name): try: data = self.query_data(origin, source, event, data_name) @@ -35,19 +37,27 @@ def query_and_plot(self, origin, source, event, data_name): self.current_origin = origin self.current_source = source - self.ax.clear() - self.ax.plot(data.datetime_x_axis, data, linewidth=1) - self.ax.set_title(f"{data_name} - {event}", fontsize=12) - self.ax.set_xlabel("Time", fontsize=10) - self.ax.set_ylabel(data_name, fontsize=10) + if self.line is None: + self.line, = self.ax.plot(data.datetime_x_axis, data, linewidth=1) + self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.ax.set_xlabel("Time", fontsize=10) + self.ax.set_ylabel(data_name, fontsize=10) + + # Improve datetime formatting + self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + self.ax.xaxis.set_major_locator(mdates.HourLocator()) + self.fig.autofmt_xdate() - # Improve datetime formatting - self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) - self.ax.xaxis.set_major_locator(mdates.HourLocator()) - self.fig.autofmt_xdate() + else: + # Only update data + self.line.set_xdata(data.datetime_x_axis) + self.line.set_ydata(data) + self.ax.relim() + self.ax.autoscale_view(scalex=True, scaley=True) self.fig.tight_layout() self.draw() + return True except Exception as e: diff --git a/diagnostic_interface/dialog/__init__.py b/diagnostic_interface/dialog/__init__.py new file mode 100644 index 00000000..9cd68346 --- /dev/null +++ b/diagnostic_interface/dialog/__init__.py @@ -0,0 +1,5 @@ +from .settings_window import SettingsDialog + +__all__ = [ + "SettingsDialog" +] diff --git a/diagnostic_interface/settings_window.py b/diagnostic_interface/dialog/settings_window.py similarity index 96% rename from diagnostic_interface/settings_window.py rename to diagnostic_interface/dialog/settings_window.py index 2eb1afac..bf0bd7de 100644 --- a/diagnostic_interface/settings_window.py +++ b/diagnostic_interface/dialog/settings_window.py @@ -32,5 +32,4 @@ def __init__(self, current_interval, current_client_address, parent=None): self.setLayout(layout) def get_settings(self): - print(self.interval_spinbox.value()) return self.interval_spinbox.value(), self.client_input.text() diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 429a7670..cbeed4a4 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -14,8 +14,10 @@ from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt from data_tools import SunbeamClient -from diagnostic_interface import TimedWidget, SettingsDialog, PlotTab, DataSelect -from config import settings +from diagnostic_interface.widgets import TimedWidget, DataSelect +from diagnostic_interface.tabs import DockerStackTab, PlotTab, UpdatableTab +from diagnostic_interface.dialog import SettingsDialog +from diagnostic_interface import settings # Interface aesthetic parameters @@ -32,6 +34,7 @@ def __init__(self): self.setWindowTitle(WINDOW_TITLE) self.setGeometry(X_COORD, Y_COORD, WIDTH, HEIGHT) self.tabs = QTabWidget() + self.tabs.currentChanged.connect(self.on_tab_changed) self.setCentralWidget(self.tabs) home_widget = QWidget() @@ -68,15 +71,8 @@ def __init__(self): home_widget.setLayout(layout) self.tabs.addTab(home_widget, "Home") - # Timer to refresh plots - self.timer = TimedWidget(settings.plot_timer_interval, self.refresh_all_tabs) - - def refresh_all_tabs(self): - """Refreshes all open plot tabs by requerying data and replotting it.""" - for i in range(self.tabs.count()): - widget = self.tabs.widget(i) - if isinstance(widget, PlotTab): - widget.refresh_plot() + self.docker_gui = DockerStackTab() + self.tabs.addTab(self.docker_gui, "Sunbeam") def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. @@ -116,13 +112,17 @@ def edit_settings(self): dialog = SettingsDialog(current_interval, current_client_address, self) if dialog.exec_(): # if user pressed OK new_plot_interval, new_client_address = dialog.get_settings() - self.timer.set_interval(new_plot_interval) settings.plot_timer_interval = new_plot_interval + settings.sunbeam_api_url = new_client_address + + # Refresh settings + self.client = SunbeamClient(settings.sunbeam_api_url) - # Change client - if new_client_address != current_client_address: - settings.sunbeam_api_url = new_client_address - self.client = SunbeamClient(new_client_address) - self.update_filters() - self.refresh_all_tabs() + self.data_select_form.update_filters() + + def on_tab_changed(self, index: int): + for i in range(self.tabs.count()): + widget = self.tabs.widget(i) + if isinstance(widget, UpdatableTab): + widget.set_tab_active(i == index) diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml index 51ce7eb6..e1ba89f7 100644 --- a/diagnostic_interface/settings.toml +++ b/diagnostic_interface/settings.toml @@ -1,2 +1,2 @@ -plot_timer_interval = 15 -sunbeam_api_url = "api.sunbeam.ubcsolar.com" +plot_timer_interval = 1 +sunbeam_api_url = "localhost:8080" diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py new file mode 100644 index 00000000..91eaba84 --- /dev/null +++ b/diagnostic_interface/tabs/__init__.py @@ -0,0 +1,9 @@ +from .sunbeam_panel import DockerStackTab +from .plot_tab import PlotTab +from ._updatable import UpdatableTab + +__all__ = [ + "DockerStackTab", + "PlotTab", + "UpdatableTab" +] diff --git a/diagnostic_interface/tabs/_updatable.py b/diagnostic_interface/tabs/_updatable.py new file mode 100644 index 00000000..98097c21 --- /dev/null +++ b/diagnostic_interface/tabs/_updatable.py @@ -0,0 +1,11 @@ +from typing import runtime_checkable, Protocol + + +@runtime_checkable +class UpdatableTab(Protocol): + def set_tab_active(self, active: bool) -> None: + """ + Set whether this tab is currently active + :param active: Whether this tab is currently active + """ + ... diff --git a/diagnostic_interface/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py similarity index 59% rename from diagnostic_interface/plot_tab.py rename to diagnostic_interface/tabs/plot_tab.py index 7299ea31..25e36ff2 100644 --- a/diagnostic_interface/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -2,8 +2,11 @@ QWidget, QVBoxLayout, QPushButton, QMessageBox, QGroupBox, QHBoxLayout ) -from PyQt5.QtCore import pyqtSignal -from diagnostic_interface import CustomNavigationToolbar, PlotCanvas +from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer + +from diagnostic_interface import settings +from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas + HELP_MESSAGES = { "VehicleVelocity": "This plot shows velocity over time.\n\n" @@ -13,6 +16,25 @@ } +class PlotRefreshWorkerSignals(QObject): + finished = pyqtSignal(bool) # success or failure + + +class PlotRefreshWorker(QRunnable): + def __init__(self, plot_canvas, origin, source, event, data_name): + super().__init__() + self.plot_canvas = plot_canvas + self.origin = origin + self.source = source + self.event = event + self.data_name = data_name + self.signals = PlotRefreshWorkerSignals() + + def run(self): + success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) + self.signals.finished.emit(success) + + class PlotTab(QWidget): close_requested = pyqtSignal(QWidget) @@ -24,6 +46,8 @@ def __init__(self, origin: str, source: str, event: str, data_name: str, parent= self.event = event self.data_name = data_name + self._thread_pool = QThreadPool() + # Layout setup self.layout = QVBoxLayout(self) self.layout.setSpacing(10) @@ -58,11 +82,34 @@ def __init__(self, origin: str, source: str, event: str, data_name: str, parent= } """) + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh_plot) + if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): self.request_close() + def set_tab_active(self, active: bool) -> None: + if active: + self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) + self.refresh_timer.start() + QTimer.singleShot(0, self.refresh_plot) + + else: + self.refresh_timer.stop() + def refresh_plot(self): - if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): + worker = PlotRefreshWorker( + self.plot_canvas, + self.origin, + self.source, + self.event, + self.data_name + ) + worker.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker) + + def _on_plot_refresh_finished(self, success: bool): + if not success: self.request_close() def request_close(self): diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py new file mode 100644 index 00000000..6c653215 --- /dev/null +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -0,0 +1,254 @@ +import subprocess +import threading +import json +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox, QPlainTextEdit, QTextEdit +) +from PyQt5.QtCore import QTimer, Qt +import datetime +import sys +from diagnostic_interface import settings +from data_tools.query import SunbeamClient +from ansi2html import Ansi2HTMLConverter +import os +import pty + + +import os +import pty +import subprocess +from PyQt5.QtWidgets import QTextEdit +from ansi2html import Ansi2HTMLConverter + + +RESET = "\x1b[0m" + + +class AnsiLogViewer(QTextEdit): + def __init__(self): + super().__init__() + self.setReadOnly(True) + self.setAcceptRichText(True) + self.setStyleSheet("QTextEdit { font-family: monospace; }") + self.converter = Ansi2HTMLConverter(dark_bg=False, inline=True) + + self._last_log_time = datetime.datetime.now(datetime.UTC) + + def fetch_ansi_logs(self, project_dir: str): + master_fd, slave_fd = pty.openpty() + since_str = self._last_log_time.isoformat() + "Z" + + proc = subprocess.Popen( + ["docker", "compose", "logs", "--since", since_str], + cwd=project_dir, + stdout=slave_fd, + stderr=subprocess.DEVNULL, + env={**os.environ, "FORCE_COLOR": "1"}, + ) + self._last_log_time = datetime.datetime.utcnow() + + os.close(slave_fd) + + output = b"" + while True: + try: + chunk = os.read(master_fd, 1024) + if not chunk: + break + output += chunk + except OSError: + break + + os.close(master_fd) + ansi_text = output.decode(errors="replace") + + lines = ansi_text.splitlines() + for line in lines: + if '\r' in line: + line = line.split('\r')[-1] + self._remove_last_line() + html = self.converter.convert(RESET + line + RESET, full=False) + self.insertHtml(html + "
") + + self._scroll_to_bottom() + + def _remove_last_line(self): + cursor = self.textCursor() + cursor.movePosition(cursor.End) + cursor.select(cursor.BlockUnderCursor) + cursor.removeSelectedText() + cursor.deletePreviousChar() # remove leftover newline + + def _scroll_to_bottom(self): + scrollbar = self.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + +class TerminalLogBox(QPlainTextEdit): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setReadOnly(True) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def wheelEvent(self, event): + # Disable mouse wheel scrolling + pass + + def keyPressEvent(self, event): + # Disable arrow key scrolling + if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): + return + super().keyPressEvent(event) + + +class DockerStackTab(QWidget): + def __init__(self, ): + super().__init__() + self.project_dir = "/Users/joshuariefman/Solar/sunbeam" + self.stack_running = False + self._init_ui() + self._init_timer() + QTimer.singleShot(0, self.update_status) + + def _init_ui(self): + self.status_label = QLabel("Status: Checking...") + self.status_label.setAlignment(Qt.AlignCenter) + + self.toggle_button = QPushButton("Start Stack") + self.toggle_button.clicked.connect(self.toggle_stack) + + self.log_output = AnsiLogViewer() + self.log_output.setReadOnly(True) + self._last_log_tail = "" + self._max_lines = 50 + self._last_log_time = datetime.datetime.now(datetime.UTC) + + layout = QVBoxLayout() + layout.addWidget(self.status_label) + layout.addWidget(self.toggle_button) + layout.addWidget(self.log_output) + self.setLayout(layout) + + def _init_timer(self): + self.timer = QTimer() + self.timer.timeout.connect(self.update_status) + self.timer.setInterval(1000) # poll every 1.00 seconds + + QTimer.singleShot(0, self.update_status) + + def toggle_stack(self): + if self.stack_running: + self.stop_stack() + else: + self.start_stack() + + def start_stack(self): + def run(): + subprocess.run( + ["docker", "compose", "up", "-d"], + cwd=self.project_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + QTimer.singleShot(0, self.update_status) + + threading.Thread(target=run, daemon=True).start() + + def stop_stack(self): + def run(): + subprocess.run( + ["docker", "compose", "down"], + cwd=self.project_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + QTimer.singleShot(0, self.update_status) + + threading.Thread(target=run, daemon=True).start() + + def update_status(self): + try: + result = subprocess.run( + ["docker", "compose", "ps", "--format", "json"], + cwd=self.project_dir, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + ) + + output = result.stdout.strip() + if output.startswith("["): + services = json.loads(output) + else: + services = [json.loads(line) for line in output.splitlines() if line.strip()] + + except subprocess.TimeoutExpired as e: + QMessageBox.critical(None, "Startup Error", f"Could not connect to Docker runtime. " + f"Is Docker running? Additional Details: \n{str(e)}") + sys.exit(1) + + if not services: + self.stack_running = False + self.status_label.setText("❌ Status: 0%") + self.toggle_button.setText("Start Stack") + self.log_output.clear() + return + + states = [s["State"] for s in services] + num_running_containers = len(list(["running" in s for s in states])) + + client = SunbeamClient(settings.sunbeam_api_url) + if num_running_containers == 6 and client.is_alive(): + self.status_label.setText("✅ Status: 100%") + self.stack_running = True + + else: + self.status_label.setText(f"❌ Status: {int(num_running_containers/7. * 100)}%") + self.stack_running = True + + self.toggle_button.setText("Stop Stack") + self.log_output.fetch_ansi_logs(self.project_dir) + + def set_tab_active(self, active: bool): + if active: + QTimer.singleShot(0, self.update_status) + self.timer.start() + else: + self.timer.stop() + + def fetch_logs(self): + def update_logs(): + try: + since_str = self._last_log_time.isoformat() + "Z" + result = subprocess.run( + ["docker", "compose", "logs", "--since", since_str, "--ansi", "always"], + cwd=self.project_dir, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=5, + env={**os.environ, "FORCE_COLOR": "1"} # <--- 👈 this is key + ) + print(repr(result.stdout)) + print(result.stdout) + self.log_output.append_ansi_text(result.stdout) + + # Update timestamp regardless of whether new logs came in + self._last_log_time = datetime.datetime.utcnow() + + except Exception as e: + self.log_output.append_ansi_text(f"Error retrieving logs:\n{e}") + + QTimer.singleShot(0, update_logs) + + def _trim_log_output(self): + text = self.log_output.toPlainText() + lines = text.splitlines() + if len(lines) > self._max_lines: + trimmed = "\n".join(lines[-self._max_lines:]) + self.log_output.setPlainText(trimmed) + + def _scroll_to_bottom(self): + scroll = self.log_output.verticalScrollBar() + scroll.setValue(scroll.maximum()) diff --git a/diagnostic_interface/widgets/__init__.py b/diagnostic_interface/widgets/__init__.py new file mode 100644 index 00000000..351b6ab1 --- /dev/null +++ b/diagnostic_interface/widgets/__init__.py @@ -0,0 +1,7 @@ +from .data_select import DataSelect +from .timer_widget import TimedWidget + +__all__ = [ + "DataSelect", + "TimedWidget" +] diff --git a/diagnostic_interface/data_select.py b/diagnostic_interface/widgets/data_select.py similarity index 65% rename from diagnostic_interface/data_select.py rename to diagnostic_interface/widgets/data_select.py index 13d4d907..e4d25020 100644 --- a/diagnostic_interface/data_select.py +++ b/diagnostic_interface/widgets/data_select.py @@ -1,7 +1,8 @@ -from PyQt5.QtWidgets import QComboBox, QFormLayout +from PyQt5.QtWidgets import QComboBox, QFormLayout, QMessageBox from data_tools import SunbeamClient -from config import settings +from diagnostic_interface import settings +from requests import exceptions as requests_exceptions class DataSelect(QFormLayout): @@ -90,45 +91,50 @@ def filter_data(self) -> tuple[list, list, list, list]: client = SunbeamClient(settings.sunbeam_api_url) - available_origins = set(client.distinct("origin", {})) - - # Get valid events based on origin - available_events = set(client.distinct("event", {})) - if selected_origin: - available_events &= set(client.distinct("event", {"origin": selected_origin})) - - # Get valid sources based on origin and event - available_sources = set(client.distinct("source", {})) - if selected_origin: - # Filter by origin - available_sources &= set(client.distinct("source", {"origin": selected_origin})) - if selected_event: - available_sources &= set( - client.distinct("source", {"event": selected_event}) - ) # Filter by event - - # Get valid data types based on origin, source, and event - available_data = set(client.distinct("name", {})) # Start with all data - if selected_origin: - available_data &= set( - client.distinct("name", {"origin": selected_origin}) - ) # Filter by origin - if selected_event: - available_data &= set( - client.distinct("name", {"event": selected_event}) - ) # Filter by event - if selected_source: - available_data &= set( - client.distinct("name", {"source": selected_source}) - ) # Filter by source - - # Convert back to lists - available_origins = list(available_origins) - available_sources = list(available_sources) - available_events = list(available_events) - available_data = list(available_data) - - return available_origins, available_sources, available_events, available_data + try: + available_origins = set(client.distinct("origin", {})) + + # Get valid events based on origin + available_events = set(client.distinct("event", {})) + if selected_origin: + available_events &= set(client.distinct("event", {"origin": selected_origin})) + + # Get valid sources based on origin and event + available_sources = set(client.distinct("source", {})) + if selected_origin: + # Filter by origin + available_sources &= set(client.distinct("source", {"origin": selected_origin})) + if selected_event: + available_sources &= set( + client.distinct("source", {"event": selected_event}) + ) # Filter by event + + # Get valid data types based on origin, source, and event + available_data = set(client.distinct("name", {})) # Start with all data + if selected_origin: + available_data &= set( + client.distinct("name", {"origin": selected_origin}) + ) # Filter by origin + if selected_event: + available_data &= set( + client.distinct("name", {"event": selected_event}) + ) # Filter by event + if selected_source: + available_data &= set( + client.distinct("name", {"source": selected_source}) + ) # Filter by source + + # Convert back to lists + available_origins = list(available_origins) + available_sources = list(available_sources) + available_events = list(available_events) + available_data = list(available_data) + + return available_origins, available_sources, available_events, available_data + + except requests_exceptions.Timeout as e: + QMessageBox.critical(None, "Plotting Error", f"Error fetching Sunbeam files:\n{str(e)}") + return [], [], [], [] @staticmethod def update_dropdown(dropdown: QComboBox, available_data: list, selected_value: str): diff --git a/diagnostic_interface/timer_widget.py b/diagnostic_interface/widgets/timer_widget.py similarity index 93% rename from diagnostic_interface/timer_widget.py rename to diagnostic_interface/widgets/timer_widget.py index b2a5f053..9dd9ffbe 100644 --- a/diagnostic_interface/timer_widget.py +++ b/diagnostic_interface/widgets/timer_widget.py @@ -26,7 +26,7 @@ def set_interval(self, interval: int): """Update the timer interval and restart the timer.""" self.interval = interval * 1000 self.timer.stop() # Stop the current timer - self.timer.start(interval) # Restart the timer with the new interval + self.timer.start(self.interval) # Restart the timer with the new interval def stop(self): """Stop the timer.""" diff --git a/poetry.lock b/poetry.lock index fd8b8d18..4301a62b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,21 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "ansi2html" +version = "1.9.2" +description = "Convert text with ANSI color codes to HTML or to LaTeX" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5"}, + {file = "ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5"}, +] + +[package.extras] +docs = ["mkdocs", "mkdocs-material", "mkdocs-material-extensions", "mkdocstrings", "mkdocstrings-python", "pymdown-extensions"] +test = ["pytest", "pytest-cov"] + [[package]] name = "anytree" version = "2.12.1" @@ -2280,6 +2295,22 @@ files = [ {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] +[[package]] +name = "pywinpty" +version = "2.0.15" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e"}, + {file = "pywinpty-2.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:9a6bcec2df2707aaa9d08b86071970ee32c5026e10bcc3cc5f6f391d85baf7ca"}, + {file = "pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc"}, + {file = "pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408"}, + {file = "pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901"}, + {file = "pywinpty-2.0.15-cp39-cp39-win_amd64.whl", hash = "sha256:d261cd88fcd358cfb48a7ca0700db3e1c088c9c10403c9ebc0d8a8b57aa6a117"}, + {file = "pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2342,6 +2373,20 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "qt-py" +version = "1.4.6" +description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." +optional = false +python-versions = "*" +files = [ + {file = "qt_py-1.4.6-py2.py3-none-any.whl", hash = "sha256:1e0f8da9af74f2b3448904fab313f6f79cad56b82895f1a2c541243f00cc244e"}, + {file = "qt_py-1.4.6.tar.gz", hash = "sha256:d26f808a093754f0b44858745965bab138525cffc77c1296a3293171b2e2469f"}, +] + +[package.dependencies] +types-PySide2 = "*" + [[package]] name = "reactivex" version = "4.0.4" @@ -2816,6 +2861,21 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "termqt" +version = "1.0.0" +description = "A terminal emulator widget implemented in Python/Qt." +optional = false +python-versions = ">=3.7" +files = [ + {file = "termqt-1.0.0-py3-none-any.whl", hash = "sha256:bc5c0f6c5086b4ecc84477635b3bb6a41b551cfa31e8b24cf8b7a9bf5a1ff18e"}, + {file = "termqt-1.0.0.tar.gz", hash = "sha256:c181065649a79e54335d6e65eca7d6755eb9a44f9f61a4700d59ff2ca1fa3410"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.1", markers = "platform_system == \"Windows\""} +"Qt.py" = ">=1.3.0" + [[package]] name = "threadpoolctl" version = "3.5.0" @@ -2961,6 +3021,17 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "types-pyside2" +version = "5.15.2.1.7" +description = "The most accurate stubs for PySide2" +optional = false +python-versions = "*" +files = [ + {file = "types_pyside2-5.15.2.1.7-py2.py3-none-any.whl", hash = "sha256:a7bec4cb4657179415ca7ec7c70a45f9f9938664e22f385c85fd7cd724b07d4d"}, + {file = "types_pyside2-5.15.2.1.7.tar.gz", hash = "sha256:1d65072deb97481ad481b3414f94d02fd5da07f5e709c2d439ced14f79b2537c"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -3132,4 +3203,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "72c9fc4069921e302b05f118d1154bd1ae437d2d7aa0ad1ccd414fa519782d4c" +content-hash = "c9b84fdb7f3258bd05aee741e1ad57be4f8254b4491702a22c039bff99fa79e6" diff --git a/pyproject.toml b/pyproject.toml index 61ac905e..29093fa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,80 +1,85 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["setuptools<75", "setuptools-scm<8.2.0"] +build-backend = "setuptools.build_meta" -[tool.poetry] +[project] name = "ubc-solar-simulation" version = "1.1.0" +description = "UBC Solar's Simulation Environment" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} authors = [ - "UBC Solar Strategy Team ", - "Joshua Riefman " + {name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com"}, + {name = "Joshua Riefman", email = "joshuariefman@gmail.com"} ] maintainers = [ - "UBC Solar Strategy Team ", - "Joshua Riefman " + {name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com"}, + {name = "Joshua Riefman", email = "joshuariefman@gmail.com"} ] -description = "UBC Solar's Simulation Environment" -readme = "README.md" keywords = ["car", "simulation", "solar"] -license = "MIT" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Rust", "Natural Language :: English", "Topic :: Scientific/Engineering :: Physics" ] -homepage = "https://ubcsolar.com" -repository = "https://github.com/UBC-Solar/Simulation" -documentation = "https://ubc-solar-simulation.readthedocs.io/en/latest/" -packages = [ - {include = "simulation"} +dependencies = [ + "bayesian_optimization>=1.4.3", + "matplotlib>=3.7.2", + "Pillow>=9.4.0", + "plotly>=5.6.0", + "polyline>=1.4.0", + "pytest>=7.1.1", + "python-dotenv>=1.0.0", + "pytz", + "requests>=2.26.0", + "scikit_learn>=1.2.2", + "setuptools>=76.0.0", + "timezonefinder>=6.0.1", + "tqdm>=4.64.0", + "strenum>=0.4.15", + "cffi>=1.15.1", + "pygad>=3.0.1", + "haversine>=2.8.1", + "gitpython>=3.1.42", + "google-api-core>=2.17.1", + "google-api-python-client>=2.120.0", + "google-auth>=2.28.1", + "google-auth-httplib2>=0.2.0", + "google-auth-oauthlib>=1.2.0", + "googleapis-common-protos>=1.62.0", + "dill>=0.3.8", + "solcast>=1.2.3", + "tzlocal>=5.2", + "mplcursors>=0.5.3", + "pandas>=2.2.2", + "bokeh>=3.5.1", + "ubc-solar-physics>=1.4.2", + "pydantic>=2,<3", + "ubc-solar-data-tools>=1.5.0", + "anytree>=2.12.1", + "folium>=0.19.5", + "pyqt5-stubs>=5.15.6.0", + "pyqtwebengine>=5.15.7", + "tomli>=2.2.1", + "tomli-w>=1.2.0", + "ansi2html>=1.9.2", + "termqt>=1.0.0", + "pyte>=0.8.2", + "pyqtdarktheme>=2.1.0", ] -package-mode = false -[tool.poetry.group.dev.dependencies] -ruff = "^0.11.2" +[project.urls] +Homepage = "https://ubcsolar.com" +Repository = "https://github.com/UBC-Solar/Simulation" +Documentation = "https://ubc-solar-simulation.readthedocs.io/en/latest/" + + +[dependency-groups] +dev = [ + "ruff >=0.11.2" +] [tool.setuptools] packages = ["simulation"] - -[tool.poetry.dependencies] -python = "^3.10" -bayesian_optimization = "^1.4.3" -matplotlib = "^3.7.2" -Pillow = "^9.4.0" -plotly = "^5.6.0" -polyline = "^1.4.0" -pytest = "^7.1.1" -python-dotenv = "^1.0.0" -pytz = "*" -requests = "^2.26.0" -scikit_learn = "^1.2.2" -setuptools = "^76.0.0" -timezonefinder = "^6.0.1" -tqdm = "^4.64.0" -strenum = "^0.4.15" -cffi = "^1.15.1" -pygad = "^3.0.1" -haversine = "^2.8.1" -gitpython = "^3.1.42" -google-api-core = "^2.17.1" -google-api-python-client = "^2.120.0" -google-auth = "^2.28.1" -google-auth-httplib2 = "^0.2.0" -google-auth-oauthlib = "^1.2.0" -googleapis-common-protos = "^1.62.0" -dill = ">=0.3.8" -solcast = "^1.2.3" -tzlocal = "^5.2" -mplcursors = "^0.5.3" -pandas = "^2.2.2" -bokeh = "^3.5.1" -ubc-solar-physics = "^1.4.2" -pydantic = "2.*" -ubc-solar-data-tools = "^1.5.0" -anytree = "^2.12.1" -folium = "^0.19.5" -pyqt5-stubs = "^5.15.6.0" -pyqtwebengine = "^5.15.7" -tomli = "^2.2.1" -tomli-w = "^1.2.0" From 1937713102299a946eefffc50383fc4c27cd42fb Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 00:29:24 -0700 Subject: [PATCH 04/44] More updates --- .../dialog/settings_window.py | 67 ++++++++++++++-- diagnostic_interface/main_window.py | 7 +- diagnostic_interface/settings.toml | 1 + diagnostic_interface/tabs/sunbeam_panel.py | 77 ++++--------------- 4 files changed, 82 insertions(+), 70 deletions(-) diff --git a/diagnostic_interface/dialog/settings_window.py b/diagnostic_interface/dialog/settings_window.py index bf0bd7de..88eb9707 100644 --- a/diagnostic_interface/dialog/settings_window.py +++ b/diagnostic_interface/dialog/settings_window.py @@ -1,28 +1,73 @@ -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSpinBox, QLineEdit, QFormLayout +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSpinBox, QLineEdit, QFormLayout, QFileDialog, QToolButton, \ + QHBoxLayout, QWidget, QMessageBox, QSizePolicy, QLabel, QFrame +from PyQt5.QtCore import QTimer +import os +from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout +from PyQt5.QtGui import QFontMetrics +from PyQt5.QtCore import Qt +import sys class SettingsDialog(QDialog): - def __init__(self, current_interval, current_client_address, parent=None): + def __init__(self, current_interval, current_client_address, current_sunbeam_path, parent=None): """ :param int current_interval: the current interval that is set on our timer. :param str current_client_address: the URL from where we are currently querying data. + :param str current_sunbeam_path: the current Sunbeam path """ super().__init__(parent) self.setWindowTitle("Settings") - self.setFixedSize(300, 150) + self.setFixedSize(500, 200) layout = QFormLayout() # Refresh interval selector self.interval_spinbox = QSpinBox() self.interval_spinbox.setValue(current_interval) # convert ms to s - layout.addRow("Refresh Interval (s):", self.interval_spinbox) + layout.addRow("Plot Refresh Interval:", self.interval_spinbox) # Client selector self.client_input = QLineEdit() self.client_input.setText(current_client_address) layout.addRow("Sunbeam API URL:", self.client_input) + self.selected_sunbeam_path = current_sunbeam_path + # QLabel for path text + self.sunbeam_path_label = QLabel() + self.sunbeam_path_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + + # Elide long path + font_metrics = QFontMetrics(self.sunbeam_path_label.font()) + elided = font_metrics.elidedText(self.selected_sunbeam_path, Qt.ElideLeft, 200) + self.sunbeam_path_label.setText(elided) + self.sunbeam_path_label.setToolTip(self.selected_sunbeam_path) + + # QFrame to create a visible box + self.frame = QFrame() + self.frame.setFrameShape(QFrame.StyledPanel) + self.frame.setFrameShadow(QFrame.Sunken) + self.frame.setLineWidth(2) + self.frame.setLayout(QVBoxLayout()) + self.frame.layout().addWidget(self.sunbeam_path_label) + self.frame.layout().setContentsMargins(2, 2, 2, 2) + + self.browse_button = QToolButton() + font = self.browse_button.font() + font.setPointSize(14) # or 18, 24, etc. + self.browse_button.setFont(font) + self.browse_button.setText("📁") + self.browse_button.setFixedWidth(32) # optional to keep size tidy + self.browse_button.clicked.connect(self.select_folder) + + hbox = QHBoxLayout() + hbox.addWidget(self.frame) + hbox.addWidget(self.browse_button) + hbox.setContentsMargins(0, 0, 0, 0) + + path_widget = QWidget() + path_widget.setLayout(hbox) + layout.addRow("Sunbeam Path:", path_widget) + # OK/Cancel buttons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) @@ -31,5 +76,17 @@ def __init__(self, current_interval, current_client_address, parent=None): self.setLayout(layout) + def select_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select folder with docker-compose.yaml") + + if folder: + if any(os.path.isfile(os.path.join(folder, f)) for f in ["docker-compose.yaml", "docker-compose.yml"]): + self.selected_sunbeam_path = folder + self.sunbeam_path_label.setText(self.selected_sunbeam_path) + self.sunbeam_path_label.setToolTip(self.selected_sunbeam_path) + + else: + QMessageBox.warning(self, "Warning", "Please select a folder with docker-compose.yaml!") + def get_settings(self): - return self.interval_spinbox.value(), self.client_input.text() + return self.interval_spinbox.value(), self.client_input.text(), self.selected_sunbeam_path diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index cbeed4a4..5ca530b8 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -108,17 +108,18 @@ def edit_settings(self): where we query from.""" current_interval = settings.plot_timer_interval current_client_address = settings.sunbeam_api_url + current_sunbeam_path = settings.sunbeam_path - dialog = SettingsDialog(current_interval, current_client_address, self) + dialog = SettingsDialog(current_interval, current_client_address, current_sunbeam_path, self) if dialog.exec_(): # if user pressed OK - new_plot_interval, new_client_address = dialog.get_settings() + new_plot_interval, new_client_address, sunbeam_path = dialog.get_settings() settings.plot_timer_interval = new_plot_interval settings.sunbeam_api_url = new_client_address + settings.sunbeam_path = sunbeam_path # Refresh settings self.client = SunbeamClient(settings.sunbeam_api_url) - self.data_select_form.update_filters() def on_tab_changed(self, index: int): diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml index e1ba89f7..d788f070 100644 --- a/diagnostic_interface/settings.toml +++ b/diagnostic_interface/settings.toml @@ -1,2 +1,3 @@ plot_timer_interval = 1 sunbeam_api_url = "localhost:8080" +sunbeam_path = "/Users/joshuariefman/Solar/sunbeam" diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py index 6c653215..2a68d6cb 100644 --- a/diagnostic_interface/tabs/sunbeam_panel.py +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -1,19 +1,13 @@ -import subprocess import threading import json from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox, QPlainTextEdit, QTextEdit + QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox ) from PyQt5.QtCore import QTimer, Qt import datetime import sys from diagnostic_interface import settings from data_tools.query import SunbeamClient -from ansi2html import Ansi2HTMLConverter -import os -import pty - - import os import pty import subprocess @@ -28,6 +22,7 @@ class AnsiLogViewer(QTextEdit): def __init__(self): super().__init__() self.setReadOnly(True) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setAcceptRichText(True) self.setStyleSheet("QTextEdit { font-family: monospace; }") self.converter = Ansi2HTMLConverter(dark_bg=False, inline=True) @@ -83,13 +78,6 @@ def _scroll_to_bottom(self): scrollbar = self.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) - -class TerminalLogBox(QPlainTextEdit): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setReadOnly(True) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - def wheelEvent(self, event): # Disable mouse wheel scrolling pass @@ -104,11 +92,9 @@ def keyPressEvent(self, event): class DockerStackTab(QWidget): def __init__(self, ): super().__init__() - self.project_dir = "/Users/joshuariefman/Solar/sunbeam" self.stack_running = False self._init_ui() self._init_timer() - QTimer.singleShot(0, self.update_status) def _init_ui(self): self.status_label = QLabel("Status: Checking...") @@ -134,8 +120,6 @@ def _init_timer(self): self.timer.timeout.connect(self.update_status) self.timer.setInterval(1000) # poll every 1.00 seconds - QTimer.singleShot(0, self.update_status) - def toggle_stack(self): if self.stack_running: self.stop_stack() @@ -146,9 +130,10 @@ def start_stack(self): def run(): subprocess.run( ["docker", "compose", "up", "-d"], - cwd=self.project_dir, + cwd=settings.sunbeam_path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=True, ) QTimer.singleShot(0, self.update_status) @@ -158,7 +143,7 @@ def stop_stack(self): def run(): subprocess.run( ["docker", "compose", "down"], - cwd=self.project_dir, + cwd=settings.sunbeam_path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) @@ -170,11 +155,12 @@ def update_status(self): try: result = subprocess.run( ["docker", "compose", "ps", "--format", "json"], - cwd=self.project_dir, + cwd=settings.sunbeam_path, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=0.5, + check=True, ) output = result.stdout.strip() @@ -183,10 +169,13 @@ def update_status(self): else: services = [json.loads(line) for line in output.splitlines() if line.strip()] - except subprocess.TimeoutExpired as e: - QMessageBox.critical(None, "Startup Error", f"Could not connect to Docker runtime. " - f"Is Docker running? Additional Details: \n{str(e)}") - sys.exit(1) + except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + self.timer.stop() + QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service stack.\n" + f"Is Docker running and is Sunbeam Path set? \n" + f"Additional Details: \n{str(e)}") + + services = None if not services: self.stack_running = False @@ -208,7 +197,7 @@ def update_status(self): self.stack_running = True self.toggle_button.setText("Stop Stack") - self.log_output.fetch_ansi_logs(self.project_dir) + self.log_output.fetch_ansi_logs(settings.sunbeam_path) def set_tab_active(self, active: bool): if active: @@ -216,39 +205,3 @@ def set_tab_active(self, active: bool): self.timer.start() else: self.timer.stop() - - def fetch_logs(self): - def update_logs(): - try: - since_str = self._last_log_time.isoformat() + "Z" - result = subprocess.run( - ["docker", "compose", "logs", "--since", since_str, "--ansi", "always"], - cwd=self.project_dir, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - timeout=5, - env={**os.environ, "FORCE_COLOR": "1"} # <--- 👈 this is key - ) - print(repr(result.stdout)) - print(result.stdout) - self.log_output.append_ansi_text(result.stdout) - - # Update timestamp regardless of whether new logs came in - self._last_log_time = datetime.datetime.utcnow() - - except Exception as e: - self.log_output.append_ansi_text(f"Error retrieving logs:\n{e}") - - QTimer.singleShot(0, update_logs) - - def _trim_log_output(self): - text = self.log_output.toPlainText() - lines = text.splitlines() - if len(lines) > self._max_lines: - trimmed = "\n".join(lines[-self._max_lines:]) - self.log_output.setPlainText(trimmed) - - def _scroll_to_bottom(self): - scroll = self.log_output.verticalScrollBar() - scroll.setValue(scroll.maximum()) From 6e7281500ccda0616877010af8c5476b4d8c0538 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 00:31:21 -0700 Subject: [PATCH 05/44] Small changes --- diagnostic_interface/dialog/settings_window.py | 14 ++++++-------- diagnostic_interface/tabs/sunbeam_panel.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/diagnostic_interface/dialog/settings_window.py b/diagnostic_interface/dialog/settings_window.py index 88eb9707..3dcba900 100644 --- a/diagnostic_interface/dialog/settings_window.py +++ b/diagnostic_interface/dialog/settings_window.py @@ -1,11 +1,9 @@ -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSpinBox, QLineEdit, QFormLayout, QFileDialog, QToolButton, \ - QHBoxLayout, QWidget, QMessageBox, QSizePolicy, QLabel, QFrame -from PyQt5.QtCore import QTimer -import os -from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout +from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QSpinBox, QLineEdit, QFormLayout, QFileDialog, + QToolButton, QHBoxLayout, QMessageBox, QFrame, QWidget, QLabel, QVBoxLayout + ) from PyQt5.QtGui import QFontMetrics from PyQt5.QtCore import Qt -import sys +import os class SettingsDialog(QDialog): @@ -53,10 +51,10 @@ def __init__(self, current_interval, current_client_address, current_sunbeam_pat self.browse_button = QToolButton() font = self.browse_button.font() - font.setPointSize(14) # or 18, 24, etc. + font.setPointSize(14) self.browse_button.setFont(font) self.browse_button.setText("📁") - self.browse_button.setFixedWidth(32) # optional to keep size tidy + self.browse_button.setFixedWidth(32) self.browse_button.clicked.connect(self.select_folder) hbox = QHBoxLayout() diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py index 2a68d6cb..916949f5 100644 --- a/diagnostic_interface/tabs/sunbeam_panel.py +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -5,7 +5,6 @@ ) from PyQt5.QtCore import QTimer, Qt import datetime -import sys from diagnostic_interface import settings from data_tools.query import SunbeamClient import os @@ -16,6 +15,7 @@ RESET = "\x1b[0m" +STATUS_POLLING_INTERVAL_MS = 1000 class AnsiLogViewer(QTextEdit): @@ -118,7 +118,7 @@ def _init_ui(self): def _init_timer(self): self.timer = QTimer() self.timer.timeout.connect(self.update_status) - self.timer.setInterval(1000) # poll every 1.00 seconds + self.timer.setInterval(STATUS_POLLING_INTERVAL_MS) def toggle_stack(self): if self.stack_running: From abab9340f889d3f68d78d31c43c876ff2206c3e0 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 00:31:34 -0700 Subject: [PATCH 06/44] Add config to git --- diagnostic_interface/config.py | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 diagnostic_interface/config.py diff --git a/diagnostic_interface/config.py b/diagnostic_interface/config.py new file mode 100644 index 00000000..5e618367 --- /dev/null +++ b/diagnostic_interface/config.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel +from pathlib import Path +import tomli +import tomli_w + + +class Settings(BaseModel): + plot_timer_interval: int + sunbeam_api_url: str + sunbeam_path: str + + +class PersistentSettings: + def __init__(self, path: Path = Path(__file__).parent / "settings.toml"): + self._path = path + self._model = self._load() + + def _load(self) -> Settings: + if self._path.exists(): + with self._path.open("rb") as f: + data = tomli.load(f) + return Settings(**data) + else: + return Settings() + + def save(self): + with self._path.open("wb") as f: + tomli_w.dump(self._model.model_dump(), f) + + def __getattr__(self, item): + return getattr(self._model, item) + + def __setattr__(self, key, value): + if key in {"_path", "_model"}: + return super().__setattr__(key, value) + setattr(self._model, key, value) + self.save() + + +settings = PersistentSettings() From c67a49cfb2753bb701d11773f3af670994ef9b3b Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 00:45:57 -0700 Subject: [PATCH 07/44] Asynchronous requests to update status --- diagnostic_interface/tabs/sunbeam_panel.py | 85 ++++++++++++++-------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py index 916949f5..1c7be96d 100644 --- a/diagnostic_interface/tabs/sunbeam_panel.py +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -3,7 +3,7 @@ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox ) -from PyQt5.QtCore import QTimer, Qt +from PyQt5.QtCore import QTimer, Qt, QRunnable, QObject, pyqtSignal, QThreadPool import datetime from diagnostic_interface import settings from data_tools.query import SunbeamClient @@ -89,10 +89,48 @@ def keyPressEvent(self, event): super().keyPressEvent(event) +class UpdateStatusWorkerSignals(QObject): + services = pyqtSignal(list) # success or failure + + +class UpdateStatusWorker(QRunnable): + def __init__(self, docker_stack): + super().__init__() + self.docker_stack = docker_stack + self.signals = UpdateStatusWorkerSignals() + + def run(self): + try: + result = subprocess.run( + ["docker", "compose", "ps", "--format", "json"], + cwd=settings.sunbeam_path, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=2, + check=True, + ) + + output = result.stdout.strip() + if output.startswith("["): + services = json.loads(output) + else: + services = [json.loads(line) for line in output.splitlines() if line.strip()] + + except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + QTimer.singleShot(0, self.docker_stack.stop_timer) + QTimer.singleShot(0, self.docker_stack.raise_docker_error) + services = [] + + self.signals.services.emit(services) + + class DockerStackTab(QWidget): def __init__(self, ): super().__init__() self.stack_running = False + self._thread_pool = QThreadPool() + self._init_ui() self._init_timer() @@ -117,9 +155,13 @@ def _init_ui(self): def _init_timer(self): self.timer = QTimer() - self.timer.timeout.connect(self.update_status) + self.timer.timeout.connect(self.do_update_status) self.timer.setInterval(STATUS_POLLING_INTERVAL_MS) + def raise_docker_error(self): + QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service stack.\n" + f"Is Docker running and is Sunbeam Path set? \n") + def toggle_stack(self): if self.stack_running: self.stop_stack() @@ -135,7 +177,7 @@ def run(): stderr=subprocess.DEVNULL, check=True, ) - QTimer.singleShot(0, self.update_status) + QTimer.singleShot(0, self.do_update_status) threading.Thread(target=run, daemon=True).start() @@ -147,36 +189,16 @@ def run(): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - QTimer.singleShot(0, self.update_status) + QTimer.singleShot(0, self.do_update_status) threading.Thread(target=run, daemon=True).start() - def update_status(self): - try: - result = subprocess.run( - ["docker", "compose", "ps", "--format", "json"], - cwd=settings.sunbeam_path, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - timeout=0.5, - check=True, - ) - - output = result.stdout.strip() - if output.startswith("["): - services = json.loads(output) - else: - services = [json.loads(line) for line in output.splitlines() if line.strip()] - - except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: - self.timer.stop() - QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service stack.\n" - f"Is Docker running and is Sunbeam Path set? \n" - f"Additional Details: \n{str(e)}") - - services = None + def do_update_status(self): + worker = UpdateStatusWorker(self) + worker.signals.services.connect(self.update_status) + self._thread_pool.start(worker) + def update_status(self, services: list): if not services: self.stack_running = False self.status_label.setText("❌ Status: 0%") @@ -199,9 +221,12 @@ def update_status(self): self.toggle_button.setText("Stop Stack") self.log_output.fetch_ansi_logs(settings.sunbeam_path) + def stop_timer(self): + self.timer.stop() + def set_tab_active(self, active: bool): if active: - QTimer.singleShot(0, self.update_status) + QTimer.singleShot(0, self.do_update_status) self.timer.start() else: self.timer.stop() From 1eb14188a25f80d5ebebb0269993cc6d1b68539f Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:31:17 -0700 Subject: [PATCH 08/44] Initial command viewer --- .../widgets/comnand_output.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 diagnostic_interface/widgets/comnand_output.py diff --git a/diagnostic_interface/widgets/comnand_output.py b/diagnostic_interface/widgets/comnand_output.py new file mode 100644 index 00000000..3eecf9e1 --- /dev/null +++ b/diagnostic_interface/widgets/comnand_output.py @@ -0,0 +1,100 @@ +import re +from PyQt5.QtCore import Qt, pyqtSignal, QProcess, QByteArray +from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton +from ansi2html import Ansi2HTMLConverter + +RESET = "\x1b[0m" + + +class CommandOutputWidget(QTextEdit): + """A QTextEdit that runs a command in a QProcess and renders ANSI output (incl. spinners).""" + # emitted whenever new raw text arrives + _chunk_ready = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.setReadOnly(True) + self.setAcceptRichText(True) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + # ANSI → HTML converter + self._converter = Ansi2HTMLConverter(dark_bg=False, inline=True) + + # Qt process for running the command + self._proc = QProcess(self) + self._proc.setProcessChannelMode(QProcess.MergedChannels) + self._proc.readyReadStandardOutput.connect(self._on_ready_read) + + # connect to our own slot to update UI in main thread + self._chunk_ready.connect(self._append_chunk) + + def start_command(self, program: str, args: list[str]=(), cwd: str=None, env: dict=None): + """Launches the given program+args.""" + self.clear() + if cwd: + self._proc.setWorkingDirectory(cwd) + if env: + qt_env = self._proc.processEnvironment() + for k, v in env.items(): + qt_env.insert(k, v) + self._proc.setProcessEnvironment(qt_env) + + # ensure apps that detect a TTY will emit color + self._proc.start(program, args) + + def stop(self): + """Terminate the process.""" + self._proc.terminate() + + def _on_ready_read(self): + """Read raw bytes from the process and emit decoded text.""" + raw: QByteArray = self._proc.readAllStandardOutput() + text = bytes(raw).decode("utf-8", errors="replace") + self._chunk_ready.emit(text) + + def _append_chunk(self, text: str): + """ + Turn the incoming text into HTML, handling: + - '\r' (carriage-return) to overwrite the last line (for spinners) + - '\n' or '\r\n' → new line + - other text → ANSI→HTML conversion + """ + # Split on any combination of \r and \n, but keep them in the list + parts = re.split(r'(\r\n|\r|\n)', text) + + for part in parts: + if part == "\r": + # remove the last rendered line so we can overwrite it + self._remove_last_line() + elif part in ("\n", "\r\n"): + # explicit newline + self.insertHtml("
") + else: + # regular text: convert ANSI codes → HTML + html = self._converter.convert(RESET + part + RESET, full=False) + # insert without an extra
so that we only break on newline tokens + self.insertHtml(html) + + self._scroll_to_bottom() + + def _remove_last_line(self): + """Delete the last block (line) in the QTextEdit.""" + cursor = self.textCursor() + cursor.movePosition(cursor.End) + cursor.select(cursor.BlockUnderCursor) + cursor.removeSelectedText() + cursor.deletePreviousChar() + + def _scroll_to_bottom(self): + sb = self.verticalScrollBar() + sb.setValue(sb.maximum()) + + # disable wheel scrolling if you like + def wheelEvent(self, ev): + pass + + def keyPressEvent(self, ev): + # disable up/down arrow scrolling + if ev.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): + return + super().keyPressEvent(ev) From a8b01d58303f45405900cfb10bf8eab6a83509e0 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:47:26 -0700 Subject: [PATCH 09/44] Second iteration --- .../widgets/comnand_output.py | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/diagnostic_interface/widgets/comnand_output.py b/diagnostic_interface/widgets/comnand_output.py index 3eecf9e1..2e36518b 100644 --- a/diagnostic_interface/widgets/comnand_output.py +++ b/diagnostic_interface/widgets/comnand_output.py @@ -1,14 +1,15 @@ import re from PyQt5.QtCore import Qt, pyqtSignal, QProcess, QByteArray -from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton +from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton, QMessageBox from ansi2html import Ansi2HTMLConverter +import os +import shlex RESET = "\x1b[0m" class CommandOutputWidget(QTextEdit): """A QTextEdit that runs a command in a QProcess and renders ANSI output (incl. spinners).""" - # emitted whenever new raw text arrives _chunk_ready = pyqtSignal(str) def __init__(self, parent=None): @@ -17,30 +18,49 @@ def __init__(self, parent=None): self.setAcceptRichText(True) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - # ANSI → HTML converter self._converter = Ansi2HTMLConverter(dark_bg=False, inline=True) - - # Qt process for running the command self._proc = QProcess(self) self._proc.setProcessChannelMode(QProcess.MergedChannels) self._proc.readyReadStandardOutput.connect(self._on_ready_read) - - # connect to our own slot to update UI in main thread self._chunk_ready.connect(self._append_chunk) - def start_command(self, program: str, args: list[str]=(), cwd: str=None, env: dict=None): - """Launches the given program+args.""" - self.clear() - if cwd: - self._proc.setWorkingDirectory(cwd) - if env: - qt_env = self._proc.processEnvironment() - for k, v in env.items(): - qt_env.insert(k, v) - self._proc.setProcessEnvironment(qt_env) + def start_in_venv( + self, + project_root: str, + script_path: str, + args: list[str] = None, + venv_subdir: str = "environment", + ): + """ + Activate the venv and run a Python script. + + :param project_root: Path to your project root + :param script_path: Path (relative to project_root) of the .py you want to run + :param args: List of args to pass to the script + :param venv_subdir: Name of the venv folder under project_root (default "venv") + """ + args = args or [] - # ensure apps that detect a TTY will emit color - self._proc.start(program, args) + # full paths + activate = os.path.join(project_root, venv_subdir, "bin", "activate") + script = os.path.join(project_root, script_path) + + # safely shell-quote everything + quoted_activate = shlex.quote(activate) + quoted_script = shlex.quote(script) + quoted_args = " ".join(shlex.quote(a) for a in args) + + # build the bash -lc command + cmd = ( + f"source {quoted_activate} && " + f"exec python {quoted_script}" + + (f" {quoted_args}" if quoted_args else "") + ) + + # set working dir so relative imports, file writes, etc. use project_root + self._proc.setWorkingDirectory(project_root) + # kick off bash -lc "source ... && exec python ..." + self._proc.start("bash", ["-lc", cmd]) def stop(self): """Terminate the process.""" @@ -77,6 +97,26 @@ def _append_chunk(self, text: str): self._scroll_to_bottom() + lines = text.splitlines() + if lines: + last = lines[-1] + if re.search(r"\(y/N\)\s*>\s*$", last): + # strip the trailing prompt marker for nicer display + prompt = re.sub(r"\s*\(y/N\)\s*>\s*$", "(y/N)", last) + self._ask_and_respond(prompt) + + def _ask_and_respond(self, prompt_text: str): + """Show a QMessageBox for the prompt, then write back to the process.""" + answer = QMessageBox.question( + self, + "Confirmation Required", + prompt_text, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + resp = ("y\n" if answer == QMessageBox.Yes else "n\n").encode("utf-8") + self._proc.write(resp) + def _remove_last_line(self): """Delete the last block (line) in the QTextEdit.""" cursor = self.textCursor() From dfa3ecf8ff1f6f0939c0bcd62f9c2a26c3ee2d9a Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:52:52 -0700 Subject: [PATCH 10/44] Update Config with Protocol for type hinting --- diagnostic_interface/config.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/diagnostic_interface/config.py b/diagnostic_interface/config.py index 5e618367..115eac42 100644 --- a/diagnostic_interface/config.py +++ b/diagnostic_interface/config.py @@ -2,12 +2,33 @@ from pathlib import Path import tomli import tomli_w +from typing import Protocol, cast class Settings(BaseModel): plot_timer_interval: int sunbeam_api_url: str sunbeam_path: str + sunlink_path: str + + +class SettingsProtocol(Protocol): + @property + def plot_timer_interval(self) -> int: ... + @property + def sunbeam_api_url(self) -> str: ... + @property + def sunbeam_path(self) -> str: ... + @property + def sunlink_path(self) -> str: ... + @plot_timer_interval.setter + def plot_timer_interval(self, new_interval: int) -> None: ... + @sunbeam_api_url.setter + def sunbeam_api_url(self, new_url: str) -> None: ... + @sunbeam_path.setter + def sunbeam_path(self, new_path: str) -> None: ... + @sunlink_path.setter + def sunlink_path(self, new_path: str) -> None: ... class PersistentSettings: @@ -37,4 +58,4 @@ def __setattr__(self, key, value): self.save() -settings = PersistentSettings() +settings = cast(SettingsProtocol, PersistentSettings()) From 03b06ad772534a8c4a8885e7b902fc714dc16296 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:53:36 -0700 Subject: [PATCH 11/44] Refactor Docker panels --- diagnostic_interface/tabs/docker_panel.py | 154 ++++++++++++++ diagnostic_interface/tabs/sunbeam_panel.py | 225 +-------------------- diagnostic_interface/tabs/sunlink_panel.py | 27 +++ 3 files changed, 192 insertions(+), 214 deletions(-) create mode 100644 diagnostic_interface/tabs/docker_panel.py create mode 100644 diagnostic_interface/tabs/sunlink_panel.py diff --git a/diagnostic_interface/tabs/docker_panel.py b/diagnostic_interface/tabs/docker_panel.py new file mode 100644 index 00000000..73d29ed2 --- /dev/null +++ b/diagnostic_interface/tabs/docker_panel.py @@ -0,0 +1,154 @@ +import threading +import json +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel +from PyQt5.QtCore import QTimer, Qt, QRunnable, QObject, pyqtSignal, QThreadPool +from diagnostic_interface.widgets import DockerLogWidget +import subprocess +from abc import abstractmethod + + +RESET = "\x1b[0m" +STATUS_POLLING_INTERVAL_MS = 1000 +MAX_TEXT_LINES = 50 + + +class UpdateStatusWorkerSignals(QObject): + services = pyqtSignal(list) # success or failure + + +class UpdateStatusWorker(QRunnable): + def __init__(self, docker_stack: "DockerStackTab"): + super().__init__() + self.docker_stack = docker_stack + self.signals = UpdateStatusWorkerSignals() + + def run(self): + try: + result = subprocess.run( + ["docker", "compose", "ps", "--format", "json"], + cwd=self.docker_stack.project_directory, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=2, + check=True, + ) + + output = result.stdout.strip() + if output.startswith("["): + services = json.loads(output) + else: + services = [json.loads(line) for line in output.splitlines() if line.strip()] + + except (subprocess.TimeoutExpired, subprocess.SubprocessError): + QTimer.singleShot(0, self.docker_stack.stop_timer) + QTimer.singleShot(0, self.docker_stack.raise_docker_error) + services = [] + + self.signals.services.emit(services) + + +class DockerStackTab(QWidget): + def __init__(self): + super().__init__() + self.stack_running = False + self._thread_pool = QThreadPool() + + self._init_ui() + self._init_timer() + + def _init_ui(self): + self.status_label = QLabel("Status: Checking...") + self.status_label.setAlignment(Qt.AlignCenter) + + self.toggle_button = QPushButton("Start Stack") + self.toggle_button.clicked.connect(self.toggle_stack) + + self.log_output = DockerLogWidget() + self.log_output.setReadOnly(True) + + layout = QVBoxLayout() + layout.addWidget(self.status_label) + layout.addWidget(self.toggle_button) + layout.addWidget(self.log_output) + self.setLayout(layout) + + @property + @abstractmethod + def project_directory(self): + pass + + def _init_timer(self): + self.timer = QTimer() + self.timer.timeout.connect(self.do_update_status) + self.timer.setInterval(STATUS_POLLING_INTERVAL_MS) + + @abstractmethod + def raise_docker_error(self): + pass + + def toggle_stack(self): + if self.stack_running: + self.stop_stack() + else: + self.start_stack() + + def start_stack(self): + def run(): + subprocess.run( + ["docker", "compose", "up", "-d"], + cwd=self.project_directory, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + QTimer.singleShot(0, self.do_update_status) + + threading.Thread(target=run, daemon=True).start() + + def stop_stack(self): + def run(): + subprocess.run( + ["docker", "compose", "down"], + cwd=self.project_directory, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + QTimer.singleShot(0, self.do_update_status) + + threading.Thread(target=run, daemon=True).start() + + def do_update_status(self): + worker = UpdateStatusWorker(self) + worker.signals.services.connect(self.update_status) + self._thread_pool.start(worker) + + def update_status(self, services: list): + if not services: + self.stack_running = False + self.status_label.setText("❌ Status: 0%") + self.toggle_button.setText("Start Stack") + self.log_output.clear() + return + + states = [s["State"] for s in services] + num_running_containers = len(list(["running" in s for s in states])) + + self.evaluate_readiness(num_running_containers) + + self.toggle_button.setText("Stop Stack") + self.log_output.fetch_ansi_logs(self.project_directory) + + @abstractmethod + def evaluate_readiness(self, num_running_containers: int): + pass + + def stop_timer(self): + self.timer.stop() + + def set_tab_active(self, active: bool): + if active: + QTimer.singleShot(0, self.do_update_status) + self.timer.start() + else: + self.timer.stop() diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py index 1c7be96d..bb0f09f9 100644 --- a/diagnostic_interface/tabs/sunbeam_panel.py +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -1,214 +1,24 @@ -import threading -import json -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox -) -from PyQt5.QtCore import QTimer, Qt, QRunnable, QObject, pyqtSignal, QThreadPool -import datetime +from PyQt5.QtWidgets import QMessageBox from diagnostic_interface import settings +from diagnostic_interface.tabs import DockerStackTab from data_tools.query import SunbeamClient -import os -import pty -import subprocess -from PyQt5.QtWidgets import QTextEdit -from ansi2html import Ansi2HTMLConverter +from abc import abstractmethod -RESET = "\x1b[0m" -STATUS_POLLING_INTERVAL_MS = 1000 - - -class AnsiLogViewer(QTextEdit): +class SunbeamTab(DockerStackTab): def __init__(self): super().__init__() - self.setReadOnly(True) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setAcceptRichText(True) - self.setStyleSheet("QTextEdit { font-family: monospace; }") - self.converter = Ansi2HTMLConverter(dark_bg=False, inline=True) - - self._last_log_time = datetime.datetime.now(datetime.UTC) - - def fetch_ansi_logs(self, project_dir: str): - master_fd, slave_fd = pty.openpty() - since_str = self._last_log_time.isoformat() + "Z" - - proc = subprocess.Popen( - ["docker", "compose", "logs", "--since", since_str], - cwd=project_dir, - stdout=slave_fd, - stderr=subprocess.DEVNULL, - env={**os.environ, "FORCE_COLOR": "1"}, - ) - self._last_log_time = datetime.datetime.utcnow() - - os.close(slave_fd) - - output = b"" - while True: - try: - chunk = os.read(master_fd, 1024) - if not chunk: - break - output += chunk - except OSError: - break - - os.close(master_fd) - ansi_text = output.decode(errors="replace") - - lines = ansi_text.splitlines() - for line in lines: - if '\r' in line: - line = line.split('\r')[-1] - self._remove_last_line() - html = self.converter.convert(RESET + line + RESET, full=False) - self.insertHtml(html + "
") - - self._scroll_to_bottom() - - def _remove_last_line(self): - cursor = self.textCursor() - cursor.movePosition(cursor.End) - cursor.select(cursor.BlockUnderCursor) - cursor.removeSelectedText() - cursor.deletePreviousChar() # remove leftover newline - - def _scroll_to_bottom(self): - scrollbar = self.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - - def wheelEvent(self, event): - # Disable mouse wheel scrolling - pass - - def keyPressEvent(self, event): - # Disable arrow key scrolling - if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): - return - super().keyPressEvent(event) - - -class UpdateStatusWorkerSignals(QObject): - services = pyqtSignal(list) # success or failure - - -class UpdateStatusWorker(QRunnable): - def __init__(self, docker_stack): - super().__init__() - self.docker_stack = docker_stack - self.signals = UpdateStatusWorkerSignals() - - def run(self): - try: - result = subprocess.run( - ["docker", "compose", "ps", "--format", "json"], - cwd=settings.sunbeam_path, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - timeout=2, - check=True, - ) - - output = result.stdout.strip() - if output.startswith("["): - services = json.loads(output) - else: - services = [json.loads(line) for line in output.splitlines() if line.strip()] - - except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: - QTimer.singleShot(0, self.docker_stack.stop_timer) - QTimer.singleShot(0, self.docker_stack.raise_docker_error) - services = [] - - self.signals.services.emit(services) - - -class DockerStackTab(QWidget): - def __init__(self, ): - super().__init__() - self.stack_running = False - self._thread_pool = QThreadPool() - - self._init_ui() - self._init_timer() - - def _init_ui(self): - self.status_label = QLabel("Status: Checking...") - self.status_label.setAlignment(Qt.AlignCenter) - self.toggle_button = QPushButton("Start Stack") - self.toggle_button.clicked.connect(self.toggle_stack) - - self.log_output = AnsiLogViewer() - self.log_output.setReadOnly(True) - self._last_log_tail = "" - self._max_lines = 50 - self._last_log_time = datetime.datetime.now(datetime.UTC) - - layout = QVBoxLayout() - layout.addWidget(self.status_label) - layout.addWidget(self.toggle_button) - layout.addWidget(self.log_output) - self.setLayout(layout) - - def _init_timer(self): - self.timer = QTimer() - self.timer.timeout.connect(self.do_update_status) - self.timer.setInterval(STATUS_POLLING_INTERVAL_MS) + @property + def project_directory(self): + return settings.sunbeam_path def raise_docker_error(self): - QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service stack.\n" - f"Is Docker running and is Sunbeam Path set? \n") - - def toggle_stack(self): - if self.stack_running: - self.stop_stack() - else: - self.start_stack() - - def start_stack(self): - def run(): - subprocess.run( - ["docker", "compose", "up", "-d"], - cwd=settings.sunbeam_path, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - ) - QTimer.singleShot(0, self.do_update_status) - - threading.Thread(target=run, daemon=True).start() - - def stop_stack(self): - def run(): - subprocess.run( - ["docker", "compose", "down"], - cwd=settings.sunbeam_path, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - QTimer.singleShot(0, self.do_update_status) - - threading.Thread(target=run, daemon=True).start() - - def do_update_status(self): - worker = UpdateStatusWorker(self) - worker.signals.services.connect(self.update_status) - self._thread_pool.start(worker) - - def update_status(self, services: list): - if not services: - self.stack_running = False - self.status_label.setText("❌ Status: 0%") - self.toggle_button.setText("Start Stack") - self.log_output.clear() - return - - states = [s["State"] for s in services] - num_running_containers = len(list(["running" in s for s in states])) + QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service " + f"stack.\n Is Docker running and is Sunbeam path correct? \n") + @abstractmethod + def evaluate_readiness(self, num_running_containers): client = SunbeamClient(settings.sunbeam_api_url) if num_running_containers == 6 and client.is_alive(): self.status_label.setText("✅ Status: 100%") @@ -217,16 +27,3 @@ def update_status(self, services: list): else: self.status_label.setText(f"❌ Status: {int(num_running_containers/7. * 100)}%") self.stack_running = True - - self.toggle_button.setText("Stop Stack") - self.log_output.fetch_ansi_logs(settings.sunbeam_path) - - def stop_timer(self): - self.timer.stop() - - def set_tab_active(self, active: bool): - if active: - QTimer.singleShot(0, self.do_update_status) - self.timer.start() - else: - self.timer.stop() diff --git a/diagnostic_interface/tabs/sunlink_panel.py b/diagnostic_interface/tabs/sunlink_panel.py new file mode 100644 index 00000000..aa2504d8 --- /dev/null +++ b/diagnostic_interface/tabs/sunlink_panel.py @@ -0,0 +1,27 @@ +from PyQt5.QtWidgets import QMessageBox +from diagnostic_interface import settings +from diagnostic_interface.tabs import DockerStackTab +from abc import abstractmethod + + +class SunlinkTab(DockerStackTab): + def __init__(self): + super().__init__() + + @property + def project_directory(self): + return settings.sunlink_path + + def raise_docker_error(self): + QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunlink Docker service " + f"stack.\n Is Docker running and is Sunlink path correct? \n") + + @abstractmethod + def evaluate_readiness(self, num_running_containers: int): + if num_running_containers == 4: + self.status_label.setText("✅ Status: 100%") + self.stack_running = True + + else: + self.status_label.setText(f"❌ Status: {int(num_running_containers/4. * 100)}%") + self.stack_running = True From 6c6a14d616d1fe235ec94787d08e2a3c6eb8bae0 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:53:47 -0700 Subject: [PATCH 12/44] Add Sunlink path to settings --- .../dialog/settings_window.py | 140 ++++++++++++------ diagnostic_interface/settings.toml | 1 + 2 files changed, 92 insertions(+), 49 deletions(-) diff --git a/diagnostic_interface/dialog/settings_window.py b/diagnostic_interface/dialog/settings_window.py index 3dcba900..3f782a99 100644 --- a/diagnostic_interface/dialog/settings_window.py +++ b/diagnostic_interface/dialog/settings_window.py @@ -4,10 +4,70 @@ from PyQt5.QtGui import QFontMetrics from PyQt5.QtCore import Qt import os +from typing import Callable + + +class PathFrame(QFrame): + def __init__(self, current_path: str): + super().__init__() + + self._path_label = QLabel() + self._path_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + + # Elide long path + font_metrics = QFontMetrics(self._path_label.font()) + elided = font_metrics.elidedText(current_path, Qt.ElideLeft, 200) + self._path_label.setText(elided) + self._path_label.setToolTip(current_path) + + # QFrame to create a visible box + self.setFrameShape(QFrame.StyledPanel) + self.setFrameShadow(QFrame.Sunken) + self.setLineWidth(2) + self.setLayout(QVBoxLayout()) + self.layout().addWidget(self._path_label) + self.layout().setContentsMargins(2, 2, 2, 2) + + @property + def path_label(self): + return self._path_label + + +class PathSelectionBox(QWidget): + def __init__(self, current_path: str, button_callback: Callable[[], None]): + super().__init__() + + self._frame = PathFrame(current_path) + + self._browse_button = QToolButton() + font = self._browse_button.font() + font.setPointSize(14) + self._browse_button.setFont(font) + self._browse_button.setText("📁") + self._browse_button.setFixedWidth(32) + self._browse_button.clicked.connect(button_callback) + + hbox = QHBoxLayout() + hbox.addWidget(self._frame) + hbox.addWidget(self._browse_button) + hbox.setContentsMargins(0, 0, 0, 0) + + self.setLayout(hbox) + + @property + def frame(self): + return self._frame class SettingsDialog(QDialog): - def __init__(self, current_interval, current_client_address, current_sunbeam_path, parent=None): + def __init__( + self, + current_interval: int, + current_client_address: str, + current_sunbeam_path: str, + current_sunlink_path: str, + parent=None + ): """ :param int current_interval: the current interval that is set on our timer. :param str current_client_address: the URL from where we are currently querying data. @@ -20,51 +80,22 @@ def __init__(self, current_interval, current_client_address, current_sunbeam_pat layout = QFormLayout() # Refresh interval selector - self.interval_spinbox = QSpinBox() - self.interval_spinbox.setValue(current_interval) # convert ms to s - layout.addRow("Plot Refresh Interval:", self.interval_spinbox) + self._interval_spinbox = QSpinBox() + self._interval_spinbox.setValue(current_interval) # convert ms to s + layout.addRow("Plot Refresh Interval:", self._interval_spinbox) # Client selector - self.client_input = QLineEdit() - self.client_input.setText(current_client_address) - layout.addRow("Sunbeam API URL:", self.client_input) - - self.selected_sunbeam_path = current_sunbeam_path - # QLabel for path text - self.sunbeam_path_label = QLabel() - self.sunbeam_path_label.setTextInteractionFlags(Qt.TextSelectableByMouse) - - # Elide long path - font_metrics = QFontMetrics(self.sunbeam_path_label.font()) - elided = font_metrics.elidedText(self.selected_sunbeam_path, Qt.ElideLeft, 200) - self.sunbeam_path_label.setText(elided) - self.sunbeam_path_label.setToolTip(self.selected_sunbeam_path) - - # QFrame to create a visible box - self.frame = QFrame() - self.frame.setFrameShape(QFrame.StyledPanel) - self.frame.setFrameShadow(QFrame.Sunken) - self.frame.setLineWidth(2) - self.frame.setLayout(QVBoxLayout()) - self.frame.layout().addWidget(self.sunbeam_path_label) - self.frame.layout().setContentsMargins(2, 2, 2, 2) - - self.browse_button = QToolButton() - font = self.browse_button.font() - font.setPointSize(14) - self.browse_button.setFont(font) - self.browse_button.setText("📁") - self.browse_button.setFixedWidth(32) - self.browse_button.clicked.connect(self.select_folder) + self._client_input = QLineEdit() + self._client_input.setText(current_client_address) + layout.addRow("Sunbeam API URL:", self._client_input) - hbox = QHBoxLayout() - hbox.addWidget(self.frame) - hbox.addWidget(self.browse_button) - hbox.setContentsMargins(0, 0, 0, 0) + self._selected_sunlink_path = current_sunlink_path + self._sunlink_path_widget = PathSelectionBox(current_sunlink_path, self.select_sunlink_folder) + layout.addRow("Sunlink Path:", self._sunlink_path_widget) - path_widget = QWidget() - path_widget.setLayout(hbox) - layout.addRow("Sunbeam Path:", path_widget) + self._selected_sunbeam_path = current_sunbeam_path + self._sunbeam_path_widget = PathSelectionBox(current_sunbeam_path, self.select_sunbeam_folder) + layout.addRow("Sunbeam Path:", self._sunbeam_path_widget) # OK/Cancel buttons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) @@ -74,17 +105,28 @@ def __init__(self, current_interval, current_client_address, current_sunbeam_pat self.setLayout(layout) - def select_folder(self): + def _select_docker_folder(self, attr_name: str, widget_attr: str) -> None: folder = QFileDialog.getExistingDirectory(self, "Select folder with docker-compose.yaml") if folder: if any(os.path.isfile(os.path.join(folder, f)) for f in ["docker-compose.yaml", "docker-compose.yml"]): - self.selected_sunbeam_path = folder - self.sunbeam_path_label.setText(self.selected_sunbeam_path) - self.sunbeam_path_label.setToolTip(self.selected_sunbeam_path) - + setattr(self, attr_name, folder) + widget = getattr(self, widget_attr) + widget.frame.path_label.setText(folder) + widget.frame.path_label.setToolTip(folder) else: QMessageBox.warning(self, "Warning", "Please select a folder with docker-compose.yaml!") - def get_settings(self): - return self.interval_spinbox.value(), self.client_input.text(), self.selected_sunbeam_path + def select_sunlink_folder(self): + self._select_docker_folder("_selected_sunlink_path", "_sunlink_path_widget") + + def select_sunbeam_folder(self): + self._select_docker_folder("_selected_sunbeam_path", "_sunbeam_path_widget") + + def get_settings(self) -> tuple[int, str, str, str]: + return ( + self._interval_spinbox.value(), + self._client_input.text(), + self._selected_sunbeam_path, + self._selected_sunlink_path + ) diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml index d788f070..a967bdb6 100644 --- a/diagnostic_interface/settings.toml +++ b/diagnostic_interface/settings.toml @@ -1,3 +1,4 @@ plot_timer_interval = 1 sunbeam_api_url = "localhost:8080" sunbeam_path = "/Users/joshuariefman/Solar/sunbeam" +sunlink_path = "/Users/joshuariefman/Solar/sunlink" From 71611e6b8985cda5b2a6cf22d4d365ee51aef98c Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:54:00 -0700 Subject: [PATCH 13/44] Update log widget --- diagnostic_interface/tabs/__init__.py | 10 ++- diagnostic_interface/widgets/__init__.py | 6 +- .../widgets/docker_log_widget.py | 79 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 diagnostic_interface/widgets/docker_log_widget.py diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index 91eaba84..33084997 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,9 +1,15 @@ -from .sunbeam_panel import DockerStackTab +from .docker_panel import DockerStackTab from .plot_tab import PlotTab from ._updatable import UpdatableTab +from .sunbeam_panel import SunbeamTab +from .sunlink_panel import SunlinkTab +from .telemetry_panel import TelemetryTab __all__ = [ "DockerStackTab", "PlotTab", - "UpdatableTab" + "UpdatableTab", + "SunbeamTab", + "SunlinkTab", + "TelemetryTab" ] diff --git a/diagnostic_interface/widgets/__init__.py b/diagnostic_interface/widgets/__init__.py index 351b6ab1..14e5133f 100644 --- a/diagnostic_interface/widgets/__init__.py +++ b/diagnostic_interface/widgets/__init__.py @@ -1,7 +1,11 @@ from .data_select import DataSelect from .timer_widget import TimedWidget +from .docker_log_widget import DockerLogWidget +from .comnand_output import CommandOutputWidget __all__ = [ "DataSelect", - "TimedWidget" + "TimedWidget", + "DockerLogWidget", + "CommandOutputWidget" ] diff --git a/diagnostic_interface/widgets/docker_log_widget.py b/diagnostic_interface/widgets/docker_log_widget.py new file mode 100644 index 00000000..e9dada72 --- /dev/null +++ b/diagnostic_interface/widgets/docker_log_widget.py @@ -0,0 +1,79 @@ +from PyQt5.QtCore import Qt +import datetime +import os +import pty +import subprocess +from PyQt5.QtWidgets import QTextEdit +from ansi2html import Ansi2HTMLConverter + + +RESET = "\x1b[0m" + + +class DockerLogWidget(QTextEdit): + def __init__(self): + super().__init__() + self.setReadOnly(True) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setAcceptRichText(True) + self.converter = Ansi2HTMLConverter(dark_bg=False, inline=True) + + self._last_log_time = datetime.datetime.now(datetime.UTC) + + def fetch_ansi_logs(self, project_dir: str): + master_fd, slave_fd = pty.openpty() + since_str = self._last_log_time.isoformat() + "Z" + + proc = subprocess.Popen( + ["docker", "compose", "logs", "--since", since_str], + cwd=project_dir, + stdout=slave_fd, + stderr=subprocess.DEVNULL, + env={**os.environ, "FORCE_COLOR": "1"}, + ) + self._last_log_time = datetime.datetime.utcnow() + + os.close(slave_fd) + + output = b"" + while True: + try: + chunk = os.read(master_fd, 1024) + if not chunk: + break + output += chunk + except OSError: + break + + os.close(master_fd) + ansi_text = output.decode(errors="replace") + + lines = ansi_text.splitlines() + for line in lines: + if '\r' in line: + line = line.split('\r')[-1] + self._remove_last_line() + html = self.converter.convert(RESET + line + RESET, full=False) + self.insertHtml(html + "
") + + self._scroll_to_bottom() + + def _remove_last_line(self): + cursor = self.textCursor() + cursor.movePosition(cursor.End) + cursor.select(cursor.BlockUnderCursor) + cursor.removeSelectedText() + cursor.deletePreviousChar() + + def _scroll_to_bottom(self): + scrollbar = self.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def wheelEvent(self, event): + pass + + def keyPressEvent(self, event): + # Disable arrow key scrolling + if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): + return + super().keyPressEvent(event) From 2bc6b7298b5c245a18a32760ef914247314c21ef Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Fri, 23 May 2025 23:54:09 -0700 Subject: [PATCH 14/44] Add Telemetry panel --- diagnostic_interface/main_window.py | 29 +++++-- diagnostic_interface/tabs/telemetry_panel.py | 52 +++++++++++++ .../widgets/comnand_output.py | 78 ++++++++----------- 3 files changed, 106 insertions(+), 53 deletions(-) create mode 100644 diagnostic_interface/tabs/telemetry_panel.py diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 5ca530b8..c03e3cfd 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -6,16 +6,14 @@ QWidget, QPushButton, QLabel, - QComboBox, - QFormLayout, QVBoxLayout, QTabWidget, ) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt from data_tools import SunbeamClient -from diagnostic_interface.widgets import TimedWidget, DataSelect -from diagnostic_interface.tabs import DockerStackTab, PlotTab, UpdatableTab +from diagnostic_interface.widgets import DataSelect +from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab from diagnostic_interface.dialog import SettingsDialog from diagnostic_interface import settings @@ -71,8 +69,14 @@ def __init__(self): home_widget.setLayout(layout) self.tabs.addTab(home_widget, "Home") - self.docker_gui = DockerStackTab() - self.tabs.addTab(self.docker_gui, "Sunbeam") + self.sunbeam_gui = SunbeamTab() + self.tabs.addTab(self.sunbeam_gui, "Sunbeam") + + self.sunlink_gui = SunlinkTab() + self.tabs.addTab(self.sunlink_gui, "Sunlink") + + self.telemetry_tab = TelemetryTab() + self.tabs.addTab(self.telemetry_tab, "Telemetry") def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. @@ -109,14 +113,23 @@ def edit_settings(self): current_interval = settings.plot_timer_interval current_client_address = settings.sunbeam_api_url current_sunbeam_path = settings.sunbeam_path + current_sunlink_path = settings.sunlink_path + + dialog = SettingsDialog( + current_interval, + current_client_address, + current_sunbeam_path, + current_sunlink_path, + self + ) - dialog = SettingsDialog(current_interval, current_client_address, current_sunbeam_path, self) if dialog.exec_(): # if user pressed OK - new_plot_interval, new_client_address, sunbeam_path = dialog.get_settings() + new_plot_interval, new_client_address, sunbeam_path, sunlink_path = dialog.get_settings() settings.plot_timer_interval = new_plot_interval settings.sunbeam_api_url = new_client_address settings.sunbeam_path = sunbeam_path + settings.sunlink_path = sunlink_path # Refresh settings self.client = SunbeamClient(settings.sunbeam_api_url) diff --git a/diagnostic_interface/tabs/telemetry_panel.py b/diagnostic_interface/tabs/telemetry_panel.py new file mode 100644 index 00000000..a6bc3e08 --- /dev/null +++ b/diagnostic_interface/tabs/telemetry_panel.py @@ -0,0 +1,52 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel +from PyQt5.QtCore import Qt +from diagnostic_interface.widgets import CommandOutputWidget +from diagnostic_interface import settings + + +class TelemetryTab(QWidget): + def __init__(self): + super().__init__() + self.running = False + + self._init_ui() + + def _init_ui(self): + self.status_label = QLabel("❌ Status: Stopped") + self.status_label.setAlignment(Qt.AlignCenter) + + self.toggle_button = QPushButton("Start Telemetry") + self.toggle_button.clicked.connect(self.toggle_stack) + + self.log_output = CommandOutputWidget() + + layout = QVBoxLayout() + layout.addWidget(self.status_label) + layout.addWidget(self.toggle_button) + layout.addWidget(self.log_output) + + self.setLayout(layout) + + def toggle_stack(self): + if self.running: + self.stop_stack() + else: + self.start_stack() + + def start_stack(self): + self.running = True + self.status_label.setText("✅ Status: Running") + + self.log_output.start_in_venv( + project_root=settings.sunlink_path, + script_path="./link_telemetry.py", + args=["-r", "can", "--debug"] + ) + + self.toggle_button.setText("Stop Telemetry") + + def stop_stack(self): + self.running = False + self.status_label.setText("❌ Status: Stopped") + self.log_output.stop() + self.toggle_button.setText("Start Telemetry") diff --git a/diagnostic_interface/widgets/comnand_output.py b/diagnostic_interface/widgets/comnand_output.py index 2e36518b..a886b7da 100644 --- a/diagnostic_interface/widgets/comnand_output.py +++ b/diagnostic_interface/widgets/comnand_output.py @@ -1,9 +1,10 @@ import re -from PyQt5.QtCore import Qt, pyqtSignal, QProcess, QByteArray -from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton, QMessageBox +from PyQt5.QtCore import Qt, pyqtSignal, QProcess +from PyQt5.QtWidgets import QTextEdit, QMessageBox from ansi2html import Ansi2HTMLConverter import os import shlex +import sys RESET = "\x1b[0m" @@ -32,81 +33,68 @@ def start_in_venv( venv_subdir: str = "environment", ): """ - Activate the venv and run a Python script. - - :param project_root: Path to your project root - :param script_path: Path (relative to project_root) of the .py you want to run - :param args: List of args to pass to the script - :param venv_subdir: Name of the venv folder under project_root (default "venv") + Activate the venv under `project_root/venv_subdir`, then exec: + python {script_path} {args...} + in a PTY so spinners (save/restore ANSI) work. """ args = args or [] - # full paths activate = os.path.join(project_root, venv_subdir, "bin", "activate") script = os.path.join(project_root, script_path) + qa = shlex.quote(activate) + qs = shlex.quote(script) + qargs = " ".join(shlex.quote(a) for a in args) + + # the “inner” command we want to run under PTY + inner = f"source {qa} && exec python {qs}" + (f" {qargs}" if qargs else "") + + if sys.platform.startswith("linux"): + wrapper = f"script -q -e -c {shlex.quote(inner)} /dev/null" + else: + # on macOS (BSD script) there's no -c, so pass the command as args + # -q: quiet, no start/end messages + # -e: return child exit code (BSD supports -e) + # file: /dev/null + # command...: bash -lc inner + wrapper = f"script -q -e /dev/null bash -lc {shlex.quote(inner)}" - # safely shell-quote everything - quoted_activate = shlex.quote(activate) - quoted_script = shlex.quote(script) - quoted_args = " ".join(shlex.quote(a) for a in args) - - # build the bash -lc command - cmd = ( - f"source {quoted_activate} && " - f"exec python {quoted_script}" - + (f" {quoted_args}" if quoted_args else "") - ) - - # set working dir so relative imports, file writes, etc. use project_root self._proc.setWorkingDirectory(project_root) - # kick off bash -lc "source ... && exec python ..." - self._proc.start("bash", ["-lc", cmd]) + self._proc.start("bash", ["-lc", wrapper]) def stop(self): """Terminate the process.""" self._proc.terminate() def _on_ready_read(self): - """Read raw bytes from the process and emit decoded text.""" - raw: QByteArray = self._proc.readAllStandardOutput() + raw = self._proc.readAllStandardOutput() text = bytes(raw).decode("utf-8", errors="replace") self._chunk_ready.emit(text) def _append_chunk(self, text: str): - """ - Turn the incoming text into HTML, handling: - - '\r' (carriage-return) to overwrite the last line (for spinners) - - '\n' or '\r\n' → new line - - other text → ANSI→HTML conversion - """ - # Split on any combination of \r and \n, but keep them in the list - parts = re.split(r'(\r\n|\r|\n)', text) + # 1) Convert ANSI save/restore → '\r' (and drop restore) + text = re.sub(r'\x1b7|\x1b\[s', '\r', text) + text = re.sub(r'\x1b8|\x1b\[u', '', text) + # 2) Split on CR/LF and render ANSI → HTML + parts = re.split(r"(\r\n|\r|\n)", text) for part in parts: if part == "\r": - # remove the last rendered line so we can overwrite it self._remove_last_line() elif part in ("\n", "\r\n"): - # explicit newline self.insertHtml("
") else: - # regular text: convert ANSI codes → HTML html = self._converter.convert(RESET + part + RESET, full=False) - # insert without an extra
so that we only break on newline tokens self.insertHtml(html) self._scroll_to_bottom() + # 3) If the last line is exactly "(y/N) >", prompt the user lines = text.splitlines() - if lines: - last = lines[-1] - if re.search(r"\(y/N\)\s*>\s*$", last): - # strip the trailing prompt marker for nicer display - prompt = re.sub(r"\s*\(y/N\)\s*>\s*$", "(y/N)", last) - self._ask_and_respond(prompt) + if lines and re.search(r"\(y/N\)\s*>\s*$", lines[-1]): + prompt = re.sub(r"\s*\(y/N\)\s*>\s*$", "(y/N)", lines[-1]) + self._ask_and_respond(prompt) def _ask_and_respond(self, prompt_text: str): - """Show a QMessageBox for the prompt, then write back to the process.""" answer = QMessageBox.question( self, "Confirmation Required", From 4c8785f664b4b4f9a184335af3c33c61caeffc6f Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Mon, 26 May 2025 19:14:55 -0700 Subject: [PATCH 15/44] Remove Docker tabs --- diagnostic_interface/main_window.py | 19 ++++++++++--------- diagnostic_interface/start_application.py | 2 ++ diagnostic_interface/tabs/__init__.py | 16 ++++++++-------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index c03e3cfd..435558ef 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -13,7 +13,8 @@ from PyQt5.QtCore import Qt from data_tools import SunbeamClient from diagnostic_interface.widgets import DataSelect -from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab +# from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab +from diagnostic_interface.tabs import PlotTab, UpdatableTab from diagnostic_interface.dialog import SettingsDialog from diagnostic_interface import settings @@ -69,14 +70,14 @@ def __init__(self): home_widget.setLayout(layout) self.tabs.addTab(home_widget, "Home") - self.sunbeam_gui = SunbeamTab() - self.tabs.addTab(self.sunbeam_gui, "Sunbeam") - - self.sunlink_gui = SunlinkTab() - self.tabs.addTab(self.sunlink_gui, "Sunlink") - - self.telemetry_tab = TelemetryTab() - self.tabs.addTab(self.telemetry_tab, "Telemetry") + # self.sunbeam_gui = SunbeamTab() + # self.tabs.addTab(self.sunbeam_gui, "Sunbeam") + # + # self.sunlink_gui = SunlinkTab() + # self.tabs.addTab(self.sunlink_gui, "Sunlink") + # + # self.telemetry_tab = TelemetryTab() + # self.tabs.addTab(self.telemetry_tab, "Telemetry") def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. diff --git a/diagnostic_interface/start_application.py b/diagnostic_interface/start_application.py index d6a5de18..e1a641b9 100644 --- a/diagnostic_interface/start_application.py +++ b/diagnostic_interface/start_application.py @@ -1,4 +1,6 @@ import sys +import os +sys.path.insert(0, os.getcwd()) from PyQt5.QtWidgets import QApplication from main_window import MainWindow import qdarktheme diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index 33084997..9ec23db6 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,15 +1,15 @@ -from .docker_panel import DockerStackTab +# from .docker_panel import DockerStackTab from .plot_tab import PlotTab from ._updatable import UpdatableTab -from .sunbeam_panel import SunbeamTab -from .sunlink_panel import SunlinkTab -from .telemetry_panel import TelemetryTab +# from .sunbeam_panel import SunbeamTab +# from .sunlink_panel import SunlinkTab +# from .telemetry_panel import TelemetryTab __all__ = [ - "DockerStackTab", + # "DockerStackTab", "PlotTab", "UpdatableTab", - "SunbeamTab", - "SunlinkTab", - "TelemetryTab" + # "SunbeamTab", + # "SunlinkTab", + # "TelemetryTab" ] From d8d19d152d5307ad18acf84a69c4bc4d6ee72ab3 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Mon, 26 May 2025 19:17:39 -0700 Subject: [PATCH 16/44] Remove Docker tabs (1) --- diagnostic_interface/widgets/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diagnostic_interface/widgets/__init__.py b/diagnostic_interface/widgets/__init__.py index 14e5133f..13f8e0c6 100644 --- a/diagnostic_interface/widgets/__init__.py +++ b/diagnostic_interface/widgets/__init__.py @@ -1,11 +1,11 @@ from .data_select import DataSelect from .timer_widget import TimedWidget -from .docker_log_widget import DockerLogWidget +# from .docker_log_widget import DockerLogWidget from .comnand_output import CommandOutputWidget __all__ = [ "DataSelect", "TimedWidget", - "DockerLogWidget", + # "DockerLogWidget", "CommandOutputWidget" ] From 362a3d25ab1142a276655f6b344cb06b1d8d6de0 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Mon, 26 May 2025 19:18:17 -0700 Subject: [PATCH 17/44] Remove Docker tabs (2) --- diagnostic_interface/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index ad8b4187..6b631c7c 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -2,7 +2,8 @@ from widgets import TimedWidget, DataSelect from dialog import SettingsDialog from canvas import PlotCanvas, CustomNavigationToolbar -from tabs import DockerStackTab, PlotTab +from tabs import PlotTab +# from tabs import DockerStackTab, PlotTab __all__ = [ "TimedWidget", @@ -11,6 +12,6 @@ "PlotCanvas", "PlotTab", "DataSelect", - "DockerStackTab", + # "DockerStackTab", "settings" ] From 239d1aa0ece066fdad9d849364cfdb0060cdbf5a Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 10:28:07 -0700 Subject: [PATCH 18/44] - --- diagnostic_interface/tabs/copy.py | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 diagnostic_interface/tabs/copy.py diff --git a/diagnostic_interface/tabs/copy.py b/diagnostic_interface/tabs/copy.py new file mode 100644 index 00000000..2b2a2072 --- /dev/null +++ b/diagnostic_interface/tabs/copy.py @@ -0,0 +1,172 @@ +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QMessageBox, + QGroupBox, QHBoxLayout +) +from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer +from poetry.console.commands import self + +from diagnostic_interface import settings +from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas + + +HELP_MESSAGES = { + "VehicleVelocity": "This plot shows velocity over time.\n\n" + "- X-axis: Time\n" + "- Y-axis: Velocity (m/s)\n" + "- Data is sourced from the car's telemetry system.\n", +} + + +class PlotRefreshWorkerSignals(QObject): + finished = pyqtSignal(bool) # success or failure + + +class PlotRefreshWorker(QRunnable): + def __init__(self, plot_canvas, origin, source, event, data_name): + #def __init__(self, plot_canvas): + super().__init__() + self.plot_canvas = plot_canvas + self.origin = origin + self.source = source + self.event = event + self.data_name = data_name + self.signals = PlotRefreshWorkerSignals() + + def run(self): + success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) + self.signals.finished.emit(success) + + +class PlotTab2(QWidget): + close_requested = pyqtSignal(QWidget) + + #def __init__(self, origin : str, source: str, event: str, data_name: str, parent=None): + def __init__(self, origin = "production", source = "power", event = "FSGP_2024_Day_1", data_name1 = "PackPower", data_name2 = "MotorPower", parent = None): + super().__init__(parent) + + self.origin = origin + self.source = source + self.event = event + self.data_name1 = data_name1 + self.data_name2 = data_name2 + + self._thread_pool = QThreadPool() + + # Layout setup + self.layout = QVBoxLayout(self) + self.layout.setSpacing(10) + self.layout.setContentsMargins(30, 30, 30, 30) + + self.plot_canvas1 = PlotCanvas(self) + self.plot_canvas2 = PlotCanvas(self) + + self.toolbar1 = CustomNavigationToolbar(canvas=self.plot_canvas1) + self.toolbar2 = CustomNavigationToolbar(canvas=self.plot_canvas2) + # Buttons + help_button = QPushButton("Help") + help_button.setObjectName("helpButton") + help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.data_name2, self.event)) + + close_button = QPushButton("Close Tab") + close_button.setObjectName("closeButton") + close_button.clicked.connect(self.request_close) + + button_group = QGroupBox("Actions") + button_layout = QHBoxLayout() + button_layout.addWidget(help_button) + button_layout.addWidget(close_button) + button_group.setLayout(button_layout) + + self.layout.addWidget(self.toolbar1) + self.layout.addWidget(self.plot_canvas1) + self.layout.addWidget(self.toolbar2) + self.layout.addWidget(self.plot_canvas2) + + self.layout.addWidget(button_group) + + self.setStyleSheet(""" + QPushButton#helpButton, QPushButton#closeButton { + padding: 6px 12px; + border-radius: 8px; + } + """) + + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh_plot) + +#stuff added: + # self.data_name1 = "PackPower" + # self.data_name2 = "MotorPower" + # + # self.origin1 = "production" + # self.source1 = "power" + # self.event1 = "FSGP_2024_Day_1" + + plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) + plot2 = self.plot_canvas2.query_and_plot(self.origin, self.source, self.event, self.data_name2) + + # if not self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name): + # self.request_close() + + if not (plot1 and plot2): + self.request_close() + + def set_tab_active(self, active: bool) -> None: + if active: + self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) + self.refresh_timer.start() + QTimer.singleShot(0, self.refresh_plot) + + else: + self.refresh_timer.stop() + + def refresh_plot(self): + worker1 = PlotRefreshWorker( + self.plot_canvas1, + #self.plot_canvas2, + self.origin, + self.source, + self.event, + self.data_name1 + ) + + worker1.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker1) + + worker2 = PlotRefreshWorker( + self.plot_canvas2, + # self.plot_canvas2, + self.origin, + self.source, + self.event, + self.data_name2 + ) + worker2.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker2) + + def _on_plot_refresh_finished(self, success: bool): + if not success: + self.request_close() + + def request_close(self): + self.close_requested.emit(self) + + def show_help_message(self, data_name1, data_name2, event): + message1 = HELP_MESSAGES.get(data_name1, "No specific help available for this plot.") + message2 = HELP_MESSAGES.get(data_name2, "No specific help available for this plot.") + QMessageBox.information(self, f"Help: {data_name1}", message1) + QMessageBox.information(self, f"Help: {data_name2}", message2) + + + + + + + + + + + + + + From fa669605e87d5c308cc31c7001d5be139a2f14a2 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 10:39:34 -0700 Subject: [PATCH 19/44] - --- diagnostic_interface/__init__.py | 7 +- diagnostic_interface/main_window.py | 6 +- diagnostic_interface/tabs/__init__.py | 4 +- diagnostic_interface/tabs/plot_tab.py | 213 +++++++++++++++++++++++--- 4 files changed, 206 insertions(+), 24 deletions(-) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index 6b631c7c..4a7eb15b 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -2,7 +2,9 @@ from widgets import TimedWidget, DataSelect from dialog import SettingsDialog from canvas import PlotCanvas, CustomNavigationToolbar -from tabs import PlotTab +from tabs import PlotTab2 +#from .copy import PlotTab2 + # from tabs import DockerStackTab, PlotTab __all__ = [ @@ -10,7 +12,8 @@ "SettingsDialog", "CustomNavigationToolbar", "PlotCanvas", - "PlotTab", + #"PlotTab", + "PlotTab2", "DataSelect", # "DockerStackTab", "settings" diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 435558ef..8804ea9a 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -14,7 +14,9 @@ from data_tools import SunbeamClient from diagnostic_interface.widgets import DataSelect # from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab -from diagnostic_interface.tabs import PlotTab, UpdatableTab +from diagnostic_interface.tabs import PlotTab2, UpdatableTab + + from diagnostic_interface.dialog import SettingsDialog from diagnostic_interface import settings @@ -91,7 +93,7 @@ def create_plot_tab(self): data_name: str = self.data_select_form.selected_data # Creating PlotTab object and adding it to the list of tabs. - plot_tab = PlotTab(origin, source, event, data_name) + plot_tab = PlotTab2() self.tabs.addTab(plot_tab, f"{data_name}") plot_tab.close_requested.connect(self.close_tab) diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index 9ec23db6..5387479f 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,5 +1,5 @@ # from .docker_panel import DockerStackTab -from .plot_tab import PlotTab +from .plot_tab import PlotTab2 from ._updatable import UpdatableTab # from .sunbeam_panel import SunbeamTab # from .sunlink_panel import SunlinkTab @@ -7,7 +7,7 @@ __all__ = [ # "DockerStackTab", - "PlotTab", + "PlotTab2", "UpdatableTab", # "SunbeamTab", # "SunlinkTab", diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index 25e36ff2..baaecf8a 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -1,8 +1,134 @@ +# from PyQt5.QtWidgets import ( +# QWidget, QVBoxLayout, QPushButton, QMessageBox, +# QGroupBox, QHBoxLayout +# ) +# from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer +# +# from diagnostic_interface import settings +# from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas +# +# +# HELP_MESSAGES = { +# "VehicleVelocity": "This plot shows velocity over time.\n\n" +# "- X-axis: Time\n" +# "- Y-axis: Velocity (m/s)\n" +# "- Data is sourced from the car's telemetry system.\n", +# } +# +# +# class PlotRefreshWorkerSignals(QObject): +# finished = pyqtSignal(bool) # success or failure +# +# +# class PlotRefreshWorker(QRunnable): +# def __init__(self, plot_canvas, origin, source, event, data_name): +# super().__init__() +# self.plot_canvas = plot_canvas +# self.origin = origin +# self.source = source +# self.event = event +# self.data_name = data_name +# self.signals = PlotRefreshWorkerSignals() +# +# def run(self): +# success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) +# self.signals.finished.emit(success) +# +# +# class PlotTab(QWidget): +# close_requested = pyqtSignal(QWidget) +# +# def __init__(self, origin: str, source: str, event: str, data_name: str, parent=None): +# super().__init__(parent) +# +# self.origin = origin +# self.source = source +# self.event = event +# self.data_name = data_name +# +# self._thread_pool = QThreadPool() +# +# # Layout setup +# self.layout = QVBoxLayout(self) +# self.layout.setSpacing(10) +# self.layout.setContentsMargins(15, 15, 15, 15) +# +# self.plot_canvas = PlotCanvas(self) +# self.toolbar = CustomNavigationToolbar(canvas=self.plot_canvas) +# +# # Buttons +# help_button = QPushButton("Help") +# help_button.setObjectName("helpButton") +# help_button.clicked.connect(lambda: self.show_help_message(data_name, event)) +# +# close_button = QPushButton("Close Tab") +# close_button.setObjectName("closeButton") +# close_button.clicked.connect(self.request_close) +# +# button_group = QGroupBox("Actions") +# button_layout = QHBoxLayout() +# button_layout.addWidget(help_button) +# button_layout.addWidget(close_button) +# button_group.setLayout(button_layout) +# +# self.layout.addWidget(self.toolbar) +# self.layout.addWidget(self.plot_canvas) +# self.layout.addWidget(button_group) +# +# self.setStyleSheet(""" +# QPushButton#helpButton, QPushButton#closeButton { +# padding: 6px 12px; +# border-radius: 8px; +# } +# """) +# +# self.refresh_timer = QTimer() +# self.refresh_timer.timeout.connect(self.refresh_plot) +# +# if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): +# self.request_close() +# +# def set_tab_active(self, active: bool) -> None: +# if active: +# self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) +# self.refresh_timer.start() +# QTimer.singleShot(0, self.refresh_plot) +# +# else: +# self.refresh_timer.stop() +# +# def refresh_plot(self): +# worker = PlotRefreshWorker( +# self.plot_canvas, +# self.origin, +# self.source, +# self.event, +# self.data_name +# ) +# worker.signals.finished.connect(self._on_plot_refresh_finished) +# self._thread_pool.start(worker) +# +# def _on_plot_refresh_finished(self, success: bool): +# if not success: +# self.request_close() +# +# def request_close(self): +# self.close_requested.emit(self) +# +# def show_help_message(self, data_name, event): +# message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") +# QMessageBox.information(self, f"Help: {data_name}", message) + + + + + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QMessageBox, QGroupBox, QHBoxLayout ) from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer +#from poetry.console.commands import self from diagnostic_interface import settings from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas @@ -22,6 +148,7 @@ class PlotRefreshWorkerSignals(QObject): class PlotRefreshWorker(QRunnable): def __init__(self, plot_canvas, origin, source, event, data_name): + #def __init__(self, plot_canvas): super().__init__() self.plot_canvas = plot_canvas self.origin = origin @@ -35,31 +162,35 @@ def run(self): self.signals.finished.emit(success) -class PlotTab(QWidget): +class PlotTab2(QWidget): close_requested = pyqtSignal(QWidget) - def __init__(self, origin: str, source: str, event: str, data_name: str, parent=None): + #def __init__(self, origin : str, source: str, event: str, data_name: str, parent=None): + def __init__(self, origin = "production", source = "power", event = "FSGP_2024_Day_1", data_name1 = "PackPower", data_name2 = "MotorPower", parent = None): super().__init__(parent) self.origin = origin self.source = source self.event = event - self.data_name = data_name + self.data_name1 = data_name1 + self.data_name2 = data_name2 self._thread_pool = QThreadPool() # Layout setup self.layout = QVBoxLayout(self) self.layout.setSpacing(10) - self.layout.setContentsMargins(15, 15, 15, 15) + self.layout.setContentsMargins(30, 30, 30, 30) - self.plot_canvas = PlotCanvas(self) - self.toolbar = CustomNavigationToolbar(canvas=self.plot_canvas) + self.plot_canvas1 = PlotCanvas(self) + self.plot_canvas2 = PlotCanvas(self) + self.toolbar1 = CustomNavigationToolbar(canvas=self.plot_canvas1) + self.toolbar2 = CustomNavigationToolbar(canvas=self.plot_canvas2) # Buttons help_button = QPushButton("Help") help_button.setObjectName("helpButton") - help_button.clicked.connect(lambda: self.show_help_message(data_name, event)) + help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.data_name2, self.event)) close_button = QPushButton("Close Tab") close_button.setObjectName("closeButton") @@ -71,8 +202,11 @@ def __init__(self, origin: str, source: str, event: str, data_name: str, parent= button_layout.addWidget(close_button) button_group.setLayout(button_layout) - self.layout.addWidget(self.toolbar) - self.layout.addWidget(self.plot_canvas) + self.layout.addWidget(self.toolbar1) + self.layout.addWidget(self.plot_canvas1) + self.layout.addWidget(self.toolbar2) + self.layout.addWidget(self.plot_canvas2) + self.layout.addWidget(button_group) self.setStyleSheet(""" @@ -85,7 +219,21 @@ def __init__(self, origin: str, source: str, event: str, data_name: str, parent= self.refresh_timer = QTimer() self.refresh_timer.timeout.connect(self.refresh_plot) - if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): +#stuff added: + # self.data_name1 = "PackPower" + # self.data_name2 = "MotorPower" + # + # self.origin1 = "production" + # self.source1 = "power" + # self.event1 = "FSGP_2024_Day_1" + + plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) + plot2 = self.plot_canvas2.query_and_plot(self.origin, self.source, self.event, self.data_name2) + + # if not self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name): + # self.request_close() + + if not (plot1 and plot2): self.request_close() def set_tab_active(self, active: bool) -> None: @@ -98,15 +246,28 @@ def set_tab_active(self, active: bool) -> None: self.refresh_timer.stop() def refresh_plot(self): - worker = PlotRefreshWorker( - self.plot_canvas, + worker1 = PlotRefreshWorker( + self.plot_canvas1, + #self.plot_canvas2, + self.origin, + self.source, + self.event, + self.data_name1 + ) + + worker1.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker1) + + worker2 = PlotRefreshWorker( + self.plot_canvas2, + # self.plot_canvas2, self.origin, self.source, self.event, - self.data_name + self.data_name2 ) - worker.signals.finished.connect(self._on_plot_refresh_finished) - self._thread_pool.start(worker) + worker2.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker2) def _on_plot_refresh_finished(self, success: bool): if not success: @@ -115,6 +276,22 @@ def _on_plot_refresh_finished(self, success: bool): def request_close(self): self.close_requested.emit(self) - def show_help_message(self, data_name, event): - message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") - QMessageBox.information(self, f"Help: {data_name}", message) + def show_help_message(self, data_name1, data_name2, event): + message1 = HELP_MESSAGES.get(data_name1, "No specific help available for this plot.") + message2 = HELP_MESSAGES.get(data_name2, "No specific help available for this plot.") + QMessageBox.information(self, f"Help: {data_name1}", message1) + QMessageBox.information(self, f"Help: {data_name2}", message2) + + + + + + + + + + + + + + From bf7cdcfcd1d56bcc81d3558cae2bd9f8f2b59f67 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 11:16:57 -0700 Subject: [PATCH 20/44] - --- diagnostic_interface/settings.toml | 2 +- diagnostic_interface/tabs/plot_tab.py | 162 +++----------------------- 2 files changed, 18 insertions(+), 146 deletions(-) diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml index a967bdb6..f6f44e0d 100644 --- a/diagnostic_interface/settings.toml +++ b/diagnostic_interface/settings.toml @@ -1,4 +1,4 @@ plot_timer_interval = 1 -sunbeam_api_url = "localhost:8080" +sunbeam_api_url = "api.sunbeam.ubcsolar.com" sunbeam_path = "/Users/joshuariefman/Solar/sunbeam" sunlink_path = "/Users/joshuariefman/Solar/sunlink" diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index baaecf8a..beb44c8d 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -1,128 +1,3 @@ -# from PyQt5.QtWidgets import ( -# QWidget, QVBoxLayout, QPushButton, QMessageBox, -# QGroupBox, QHBoxLayout -# ) -# from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer -# -# from diagnostic_interface import settings -# from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas -# -# -# HELP_MESSAGES = { -# "VehicleVelocity": "This plot shows velocity over time.\n\n" -# "- X-axis: Time\n" -# "- Y-axis: Velocity (m/s)\n" -# "- Data is sourced from the car's telemetry system.\n", -# } -# -# -# class PlotRefreshWorkerSignals(QObject): -# finished = pyqtSignal(bool) # success or failure -# -# -# class PlotRefreshWorker(QRunnable): -# def __init__(self, plot_canvas, origin, source, event, data_name): -# super().__init__() -# self.plot_canvas = plot_canvas -# self.origin = origin -# self.source = source -# self.event = event -# self.data_name = data_name -# self.signals = PlotRefreshWorkerSignals() -# -# def run(self): -# success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) -# self.signals.finished.emit(success) -# -# -# class PlotTab(QWidget): -# close_requested = pyqtSignal(QWidget) -# -# def __init__(self, origin: str, source: str, event: str, data_name: str, parent=None): -# super().__init__(parent) -# -# self.origin = origin -# self.source = source -# self.event = event -# self.data_name = data_name -# -# self._thread_pool = QThreadPool() -# -# # Layout setup -# self.layout = QVBoxLayout(self) -# self.layout.setSpacing(10) -# self.layout.setContentsMargins(15, 15, 15, 15) -# -# self.plot_canvas = PlotCanvas(self) -# self.toolbar = CustomNavigationToolbar(canvas=self.plot_canvas) -# -# # Buttons -# help_button = QPushButton("Help") -# help_button.setObjectName("helpButton") -# help_button.clicked.connect(lambda: self.show_help_message(data_name, event)) -# -# close_button = QPushButton("Close Tab") -# close_button.setObjectName("closeButton") -# close_button.clicked.connect(self.request_close) -# -# button_group = QGroupBox("Actions") -# button_layout = QHBoxLayout() -# button_layout.addWidget(help_button) -# button_layout.addWidget(close_button) -# button_group.setLayout(button_layout) -# -# self.layout.addWidget(self.toolbar) -# self.layout.addWidget(self.plot_canvas) -# self.layout.addWidget(button_group) -# -# self.setStyleSheet(""" -# QPushButton#helpButton, QPushButton#closeButton { -# padding: 6px 12px; -# border-radius: 8px; -# } -# """) -# -# self.refresh_timer = QTimer() -# self.refresh_timer.timeout.connect(self.refresh_plot) -# -# if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): -# self.request_close() -# -# def set_tab_active(self, active: bool) -> None: -# if active: -# self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) -# self.refresh_timer.start() -# QTimer.singleShot(0, self.refresh_plot) -# -# else: -# self.refresh_timer.stop() -# -# def refresh_plot(self): -# worker = PlotRefreshWorker( -# self.plot_canvas, -# self.origin, -# self.source, -# self.event, -# self.data_name -# ) -# worker.signals.finished.connect(self._on_plot_refresh_finished) -# self._thread_pool.start(worker) -# -# def _on_plot_refresh_finished(self, success: bool): -# if not success: -# self.request_close() -# -# def request_close(self): -# self.close_requested.emit(self) -# -# def show_help_message(self, data_name, event): -# message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") -# QMessageBox.information(self, f"Help: {data_name}", message) - - - - - from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QMessageBox, QGroupBox, QHBoxLayout @@ -165,15 +40,16 @@ def run(self): class PlotTab2(QWidget): close_requested = pyqtSignal(QWidget) - #def __init__(self, origin : str, source: str, event: str, data_name: str, parent=None): + #def __init__(self, origin : str, source: str, event: str, data_name1: str, data_name2: str, parent=None): def __init__(self, origin = "production", source = "power", event = "FSGP_2024_Day_1", data_name1 = "PackPower", data_name2 = "MotorPower", parent = None): super().__init__(parent) + # + # self.origin = origin + # self.source = source + # self.event = event + # self.data_name1 = data_name1 + # self.data_name2 = data_name2 - self.origin = origin - self.source = source - self.event = event - self.data_name1 = data_name1 - self.data_name2 = data_name2 self._thread_pool = QThreadPool() @@ -190,7 +66,7 @@ def __init__(self, origin = "production", source = "power", event = "FSGP_2024_D # Buttons help_button = QPushButton("Help") help_button.setObjectName("helpButton") - help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.data_name2, self.event)) + help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.event)) close_button = QPushButton("Close Tab") close_button.setObjectName("closeButton") @@ -220,15 +96,12 @@ def __init__(self, origin = "production", source = "power", event = "FSGP_2024_D self.refresh_timer.timeout.connect(self.refresh_plot) #stuff added: - # self.data_name1 = "PackPower" - # self.data_name2 = "MotorPower" - # - # self.origin1 = "production" - # self.source1 = "power" - # self.event1 = "FSGP_2024_Day_1" - plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) - plot2 = self.plot_canvas2.query_and_plot(self.origin, self.source, self.event, self.data_name2) + + #plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) + plot1 = self.plot_canvas1.query_and_plot("production", "power", "FSGP_2024_Day_1", "MotorPower") + + plot2 = self.plot_canvas2.query_and_plot("production", "power","FSGP_2024_Day_1", "PackPower") # if not self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name): # self.request_close() @@ -248,11 +121,12 @@ def set_tab_active(self, active: bool) -> None: def refresh_plot(self): worker1 = PlotRefreshWorker( self.plot_canvas1, - #self.plot_canvas2, self.origin, self.source, self.event, self.data_name1 + #"MotorPower" + ) worker1.signals.finished.connect(self._on_plot_refresh_finished) @@ -260,11 +134,11 @@ def refresh_plot(self): worker2 = PlotRefreshWorker( self.plot_canvas2, - # self.plot_canvas2, self.origin, self.source, self.event, self.data_name2 + #"PackPower" ) worker2.signals.finished.connect(self._on_plot_refresh_finished) self._thread_pool.start(worker2) @@ -276,11 +150,9 @@ def _on_plot_refresh_finished(self, success: bool): def request_close(self): self.close_requested.emit(self) - def show_help_message(self, data_name1, data_name2, event): + def show_help_message(self, data_name1): message1 = HELP_MESSAGES.get(data_name1, "No specific help available for this plot.") - message2 = HELP_MESSAGES.get(data_name2, "No specific help available for this plot.") QMessageBox.information(self, f"Help: {data_name1}", message1) - QMessageBox.information(self, f"Help: {data_name2}", message2) From 4092ea8efde1c1861ebbc98c619c651f6850ddd6 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 11:40:42 -0700 Subject: [PATCH 21/44] - --- diagnostic_interface/tabs/plot_tab.py | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index beb44c8d..88145afc 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -37,6 +37,97 @@ def run(self): self.signals.finished.emit(success) +class PlotTab(QWidget): + close_requested = pyqtSignal(QWidget) + + def __init__(self, origin: str, source: str, event: str, data_name: str, parent=None): + super().__init__(parent) + + self.origin = origin + self.source = source + self.event = event + self.data_name = data_name + + self._thread_pool = QThreadPool() + + # Layout setup + self.layout = QVBoxLayout(self) + self.layout.setSpacing(10) + self.layout.setContentsMargins(15, 15, 15, 15) + + self.plot_canvas = PlotCanvas(self) + self.toolbar = CustomNavigationToolbar(canvas=self.plot_canvas) + + # Buttons + help_button = QPushButton("Help") + help_button.setObjectName("helpButton") + help_button.clicked.connect(lambda: self.show_help_message(data_name, event)) + + close_button = QPushButton("Close Tab") + close_button.setObjectName("closeButton") + close_button.clicked.connect(self.request_close) + + button_group = QGroupBox("Actions") + button_layout = QHBoxLayout() + button_layout.addWidget(help_button) + button_layout.addWidget(close_button) + button_group.setLayout(button_layout) + + self.layout.addWidget(self.toolbar) + self.layout.addWidget(self.plot_canvas) + self.layout.addWidget(button_group) + + self.setStyleSheet(""" + QPushButton#helpButton, QPushButton#closeButton { + padding: 6px 12px; + border-radius: 8px; + } + """) + + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh_plot) + + if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): + self.request_close() + + def set_tab_active(self, active: bool) -> None: + if active: + self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) + self.refresh_timer.start() + QTimer.singleShot(0, self.refresh_plot) + + else: + self.refresh_timer.stop() + + def refresh_plot(self): + worker = PlotRefreshWorker( + self.plot_canvas, + self.origin, + self.source, + self.event, + self.data_name + ) + worker.signals.finished.connect(self._on_plot_refresh_finished) + self._thread_pool.start(worker) + + def _on_plot_refresh_finished(self, success: bool): + if not success: + self.request_close() + + def request_close(self): + self.close_requested.emit(self) + + def show_help_message(self, data_name, event): + message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") + QMessageBox.information(self, f"Help: {data_name}", message) + + + + + + + + class PlotTab2(QWidget): close_requested = pyqtSignal(QWidget) From cb0e6e14021c3425faa9cee34be85fbc6306fcb5 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 12:12:21 -0700 Subject: [PATCH 22/44] - --- diagnostic_interface/tabs/plot_tab.py | 43 ++++++++++++++------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index 88145afc..d1926cc2 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -36,7 +36,6 @@ def run(self): success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) self.signals.finished.emit(success) - class PlotTab(QWidget): close_requested = pyqtSignal(QWidget) @@ -124,10 +123,6 @@ def show_help_message(self, data_name, event): - - - - class PlotTab2(QWidget): close_requested = pyqtSignal(QWidget) @@ -210,27 +205,33 @@ def set_tab_active(self, active: bool) -> None: self.refresh_timer.stop() def refresh_plot(self): - worker1 = PlotRefreshWorker( - self.plot_canvas1, - self.origin, - self.source, - self.event, - self.data_name1 - #"MotorPower" + # worker1 = PlotRefreshWorker( + # self.plot_canvas1, + # self.origin, + # self.source, + # self.event, + # self.data_name1 + # #"MotorPower" + # + # ) - ) + worker1 = PlotRefreshWorker(self.plot_canvas1, "production", "power", "FSGP_2024_Day_1", "MotorPower") worker1.signals.finished.connect(self._on_plot_refresh_finished) self._thread_pool.start(worker1) + # + # worker2 = PlotRefreshWorker( + # self.plot_canvas2, + # self.origin, + # self.source, + # self.event, + # self.data_name2 + # #"PackPower" + # ) + + + worker2 = PlotRefreshWorker(self.plot_canvas2, "production", "power", "FSGP_2024_Day_1", "MotorPower") - worker2 = PlotRefreshWorker( - self.plot_canvas2, - self.origin, - self.source, - self.event, - self.data_name2 - #"PackPower" - ) worker2.signals.finished.connect(self._on_plot_refresh_finished) self._thread_pool.start(worker2) From 17cfc7a9f49d7e442813db8cfd0e625547c8b9f8 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 12:12:53 -0700 Subject: [PATCH 23/44] - --- diagnostic_interface/main_window.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 8804ea9a..8f3ccfa5 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -1,4 +1,4 @@ -import pathlib +\import pathlib from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( @@ -14,7 +14,7 @@ from data_tools import SunbeamClient from diagnostic_interface.widgets import DataSelect # from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab -from diagnostic_interface.tabs import PlotTab2, UpdatableTab +from diagnostic_interface.tabs import PlotTab2, PlotTab, UpdatableTab from diagnostic_interface.dialog import SettingsDialog @@ -30,6 +30,7 @@ class MainWindow(QMainWindow): + def __init__(self): super().__init__() self.setWindowTitle(WINDOW_TITLE) @@ -62,6 +63,9 @@ def __init__(self): # Button to load the plot submit_button = QPushButton("Load Data") submit_button.clicked.connect(self.create_plot_tab) + + + layout.addWidget(submit_button) #Settings button @@ -81,23 +85,35 @@ def __init__(self): # self.telemetry_tab = TelemetryTab() # self.tabs.addTab(self.telemetry_tab, "Telemetry") + def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. This method contains a connection to the request_close method of the PlotTab class to receive the signal to close a tab.""" + plot_tab2 = PlotTab2() + self.tabs.addTab(plot_tab2, f"packpower_and_motorpower") + # Getting the values that we will query. origin: str = self.data_select_form.selected_origin source: str = self.data_select_form.selected_source event: str = self.data_select_form.selected_event data_name: str = self.data_select_form.selected_data + + # Creating PlotTab object and adding it to the list of tabs. - plot_tab = PlotTab2() + + + plot_tab = PlotTab(origin, source, event, data_name) self.tabs.addTab(plot_tab, f"{data_name}") + plot_tab.close_requested.connect(self.close_tab) + + + def close_tab(self, widget) -> None: """ Closes the current tab. @@ -143,3 +159,4 @@ def on_tab_changed(self, index: int): widget = self.tabs.widget(i) if isinstance(widget, UpdatableTab): widget.set_tab_active(i == index) + From 379e6d14f853e2045ee388b6b2ea6383eed83afd Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 12:13:59 -0700 Subject: [PATCH 24/44] - --- diagnostic_interface/__init__.py | 4 ++-- diagnostic_interface/tabs/__init__.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index 4a7eb15b..005d9f70 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -2,7 +2,7 @@ from widgets import TimedWidget, DataSelect from dialog import SettingsDialog from canvas import PlotCanvas, CustomNavigationToolbar -from tabs import PlotTab2 +from tabs import PlotTab2, PlotTab #from .copy import PlotTab2 # from tabs import DockerStackTab, PlotTab @@ -12,7 +12,7 @@ "SettingsDialog", "CustomNavigationToolbar", "PlotCanvas", - #"PlotTab", + "PlotTab", "PlotTab2", "DataSelect", # "DockerStackTab", diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index 5387479f..295806c0 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,5 +1,6 @@ # from .docker_panel import DockerStackTab from .plot_tab import PlotTab2 +from .plot_tab import PlotTab from ._updatable import UpdatableTab # from .sunbeam_panel import SunbeamTab # from .sunlink_panel import SunlinkTab @@ -7,6 +8,7 @@ __all__ = [ # "DockerStackTab", + "PlotTab", "PlotTab2", "UpdatableTab", # "SunbeamTab", From ec9ec66c260d01fd1dd18d48ac2d69669d696edc Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 12:14:17 -0700 Subject: [PATCH 25/44] - --- diagnostic_interface/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 8f3ccfa5..9c98e49e 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -1,4 +1,4 @@ -\import pathlib +import pathlib from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( From ae6f8bc17453b6238afe11b6b0f3806a3d6b2c12 Mon Sep 17 00:00:00 2001 From: sanar Date: Sat, 31 May 2025 16:21:35 -0700 Subject: [PATCH 26/44] - --- diagnostic_interface/canvas/plot_canvas.py | 89 ++++++++++++++++++++++ diagnostic_interface/tabs/plot_tab.py | 20 +---- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/diagnostic_interface/canvas/plot_canvas.py b/diagnostic_interface/canvas/plot_canvas.py index 7cbbd1e1..6c2f4987 100644 --- a/diagnostic_interface/canvas/plot_canvas.py +++ b/diagnostic_interface/canvas/plot_canvas.py @@ -91,3 +91,92 @@ def save_data_to_csv(self): }) df.to_csv(file_name, index=False) print(f"Data saved to {file_name}") + + + + + + +#temp: + + +class PlotCanvas2(FigureCanvas): + def __init__(self, parent=None): + self.fig, self.ax = plt.subplots(figsize = (width, height)) + super().__init__(self.fig) + self.setParent(parent) + + self.current_data = None + self.current_data_name = "" + self.current_event = "" + self.current_origin = "" + self.current_source = "" + + self.line = None + + def query_and_plot(self, origin, source, event, data_name): + try: + data = self.query_data(origin, source, event, data_name) + if not isinstance(data, TimeSeries): + raise TypeError("Expected TimeSeries.") + + self.current_data = data + self.current_data_name = data_name + self.current_event = event + self.current_origin = origin + self.current_source = source + + if self.line is None: + self.line, = self.ax.plot(data.datetime_x_axis, data, linewidth=1) + self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.ax.set_xlabel("Time", fontsize=10) + self.ax.set_ylabel(data_name, fontsize=10) + + # Improve datetime formatting + self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + self.ax.xaxis.set_major_locator(mdates.HourLocator()) + self.fig.autofmt_xdate() + + else: + # Only update data + self.line.set_xdata(data.datetime_x_axis) + self.line.set_ydata(data) + + self.ax.relim() + self.ax.autoscale_view(scalex=True, scaley=True) + self.fig.tight_layout() + self.draw() + + return True + + except Exception as e: + QMessageBox.critical(None, "Plotting Error", f"Error fetching or plotting data:\n{str(e)}") + return False + + def query_data(self, origin, source, event, data_name): + client = SunbeamClient(settings.sunbeam_api_url) + file = client.get_file(origin, event, source, data_name) + result = file.unwrap() + return result.values if hasattr(result, "values") else result.data + + def save_data_to_csv(self): + if self.current_data is None: + print("No data available to save.") + return + + options = QFileDialog.Options() + file_name, _ = QFileDialog.getSaveFileName( + None, + "Save Data", + f"{self.current_data_name}_{self.current_event}_{self.current_origin}_{self.current_source}.csv", + "CSV Files (*.csv);;All Files (*)", + options=options, + ) + + if file_name: + df = pd.DataFrame({ + "Time": self.current_data.datetime_x_axis, + f"{self.current_data_name}": self.current_data, + }) + df.to_csv(file_name, index=False) + print(f"Data saved to {file_name}") diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index d1926cc2..e464f4f1 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -6,7 +6,7 @@ #from poetry.console.commands import self from diagnostic_interface import settings -from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas +from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas, PlotCanvas2 HELP_MESSAGES = { @@ -205,29 +205,11 @@ def set_tab_active(self, active: bool) -> None: self.refresh_timer.stop() def refresh_plot(self): - # worker1 = PlotRefreshWorker( - # self.plot_canvas1, - # self.origin, - # self.source, - # self.event, - # self.data_name1 - # #"MotorPower" - # - # ) worker1 = PlotRefreshWorker(self.plot_canvas1, "production", "power", "FSGP_2024_Day_1", "MotorPower") worker1.signals.finished.connect(self._on_plot_refresh_finished) self._thread_pool.start(worker1) - # - # worker2 = PlotRefreshWorker( - # self.plot_canvas2, - # self.origin, - # self.source, - # self.event, - # self.data_name2 - # #"PackPower" - # ) worker2 = PlotRefreshWorker(self.plot_canvas2, "production", "power", "FSGP_2024_Day_1", "MotorPower") From 9d5adb8b3fa0288dc4e01d596811c4c3402aa2f1 Mon Sep 17 00:00:00 2001 From: sanar Date: Fri, 5 Sep 2025 15:08:38 -0700 Subject: [PATCH 27/44] - --- simulation/config/models/_car.py | 29 ++++++++++++---- simulation/model/Model.py | 40 +++++++++++----------- simulation/model/ModelBuilder.py | 58 +++++++++++++++++++++++--------- simulation/model/Simulation.py | 21 +++++++++++- 4 files changed, 105 insertions(+), 43 deletions(-) diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index bbd08961..98f5800b 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -59,13 +59,11 @@ class BatteryModelConfig(BatteryConfig): """ R_0_data: list[float] - R_P: float - C_P: float + R_P_data: list[float] + C_P_data: list[float] Q_total: float SOC_data: list[float] Uoc_data: list[float] - max_current_capacity: float - max_energy_capacity: float class BasicBatteryConfig(BatteryConfig): @@ -86,7 +84,9 @@ class MotorConfig(Config): Configuration object describing the motor of a vehicle. """ - model_config = ConfigDict(frozen=True) + model_config = ConfigDict(frozen=True, subclass_field="motor_type") + + motor_type: str road_friction: float # Road friction coefficient, dimensionless tire_radius: float # Tire radius, in m @@ -94,8 +94,24 @@ class MotorConfig(Config): drag_coefficient: float # Drag coefficient, dimensionless +class BasicMotorConfig(MotorConfig): + pass + +class AdvancedMotorConfig(MotorConfig): + cornering_coefficient: float + + +class AeroshellConfig(Config): + """ + Configuration object describing the aerodynamics forces (specifically drag and downforce) of a vehicle. + """ + model_config = ConfigDict(frozen=True) #drag vs down + + class RegenConfig(Config): """ +class RegenConfig(Config): + Configuration object describing the regenerative braking systems of a vehicle. """ @@ -115,5 +131,6 @@ class CarConfig(Config): battery_config: BatteryConfig motor_config: MotorConfig regen_config: RegenConfig + aeroshell_config: AeroshellConfig - name: str + name: str \ No newline at end of file diff --git a/simulation/model/Model.py b/simulation/model/Model.py index 1f8cdc2e..3e29c68b 100644 --- a/simulation/model/Model.py +++ b/simulation/model/Model.py @@ -9,10 +9,13 @@ from simulation.race import Race, reshape_speed_array, get_granularity_reduced_boolean from simulation.config import SimulationReturnType +from micro_strategy import OptimizedSpeedsPath + from physics.models.arrays import BaseArray from physics.models.battery import BaseBattery from physics.models.lvs import BaseLVS from physics.models.motor import BaseMotor +from physics.models.aeroshell.aeroshell import Aeroshell from physics.models.regen import BaseRegen from physics.environment.gis import BaseGIS from physics.environment.meteorology import BaseMeteorology @@ -21,7 +24,7 @@ class Model: """ A `Model` is a comprehensive model of a solar-powered vehicle's components within a fully qualified environment, - ready for simulation given an input of driving speeds for the vehicle to perform. + ready for simulation given an input of driving speeds_directory for the vehicle to perform. """ def __init__( @@ -34,6 +37,7 @@ def __init__( array: BaseArray, battery: BaseBattery, motor: BaseMotor, + aeroshell: Aeroshell, regen: BaseRegen, lvs: BaseLVS, gis: BaseGIS, @@ -41,6 +45,7 @@ def __init__( max_acceleration: float, max_deceleration: float, start_time: int, + num_laps: int ): """ Instantiate a `Model`. @@ -53,6 +58,7 @@ def __init__( :param BaseArray array: An instance of `BaseArray` representing the solar array to be simulated. :param BaseBattery battery: An instance of `BaseBattery` representing the batter pack to be simulated. :param BaseMotor motor: An instance of `BaseMotor` representing the motor to be simulated. + :param Aeroshell aeroshell: An instance of the Aeroshell class that represents the drag and down force calculations :param BaseRegen regen: An instance of `BaseRegen` representing the regenerative braking system to be simulated. :param BaseLVS lvs: An instance of `BaseLVS` representing the low-voltage systems to be simulated. :param BaseGIS gis: An instance of `BaseGIS` which characterizes geographical information about the simulation. @@ -60,6 +66,7 @@ def __init__( :param float max_acceleration: Maximum allowed acceleration of the car in km/h. :param float max_deceleration: Maximum allowed deceleration of the car in km/h. :param int start_time: The initial time, in seconds since midnight of the first day, of the simulation. + :param int num_laps: The number of laps that we are simulating/optimizing """ self.return_type = return_type self.race = race @@ -68,6 +75,7 @@ def __init__( self.simulation_dt = simulation_dt self.solar_array = array self.motor = motor + self.aeroshell = aeroshell self.regen = regen self.battery = battery self.gis = gis @@ -76,8 +84,9 @@ def __init__( self.max_acceleration = max_acceleration self.max_deceleration = max_deceleration self.start_time = start_time + self.num_laps = num_laps - self.time_of_initialization = self.meteorology.last_updated_time # Real Time + self.time_of_initialization = race.date.timestamp() + self.start_time # Real Time self.simulation_duration = race.race_duration - self.start_time @@ -101,14 +110,14 @@ def run_model( ): """ - Given an array of driving speeds, simulate the model by running calculations sequentially for the entire + Given an array of driving speeds_directory, simulate the model by running calculations sequentially for the entire simulation duration. Returns either time taken, distance travelled, or void. This function is mostly a wrapper around `run_simulation_calculations`, which is where the magic happens, that deals with processing the driving - speeds array as well as plotting and handling the calculation results. + speeds_directory array as well as plotting and handling the calculation results. Note: if the speed remains constant throughout this update, knowing the starting time, the cumulative distance at every time can be known. @@ -120,7 +129,7 @@ def run_model( Note 2: currently, the simulation can only be run for times during which weather data is available - :param np.ndarray speed: Array that specifies the solar car's driving speed at each time step. + :param np.ndarray speed: Array that specifies the solar car's avg driving speed per lap. :param bool plot_results: Set to True to plot the results of the simulation. :param bool verbose: Boolean to control logging and debugging behaviour. :param tuple[float] plot_portion: A tuple containing the beginning and end of the portion of the array we'd @@ -133,23 +142,14 @@ def run_model( Reduces verbosity when true. """ - - if speed is None: - speed = np.array([30] * self.get_driving_time_divisions()) - - assert len(speed) == self.get_driving_time_divisions(), ( - "Input driving speeds array must have length equal to " - "get_driving_time_divisions()! Current length is " - f"{len(speed)} and length of " - f"{self.get_driving_time_divisions()} is needed!" - ) + track_speeds = np.load(OptimizedSpeedsPath) # ----- Reshape speed array ----- speed_kmh = reshape_speed_array( self.race, speed, - self.speed_dt, self.start_time, + self.gis, self.simulation_dt, self.max_acceleration, self.max_deceleration, @@ -157,11 +157,10 @@ def run_model( # ----- Preserve raw speed ----- raw_speed = speed_kmh.copy() - # speed_kmh = core.constrain_speeds(self.speed_limits.astype(float), speed_kmh, self.simulation_dt) # ------ Run calculations and get result and modified speed array ------- self._simulation = Simulation(self) - self._simulation.run_simulation_calculations(speed_kmh) + self._simulation.run_simulation_calculations(speed_kmh, track_speeds) results = self.get_results( [ @@ -184,7 +183,6 @@ def run_model( ) # ----- Plotting ----- - if plot_results: results_arrays = self.get_results( [ @@ -204,7 +202,7 @@ def run_model( "SOC (%)", "Delta energy (J)", "Solar irradiance (W/m^2)", - "Wind speeds (km/h)", + "Wind speeds_directory (km/h)", "Elevation (m)", "Raw SOC (%)", "Raw Speed (km/h)", @@ -329,4 +327,4 @@ def get_driving_time_divisions(self) -> int: ) .sum() .astype(int) - ) + ) \ No newline at end of file diff --git a/simulation/model/ModelBuilder.py b/simulation/model/ModelBuilder.py index 8a35914f..f148f9e3 100644 --- a/simulation/model/ModelBuilder.py +++ b/simulation/model/ModelBuilder.py @@ -25,9 +25,10 @@ ) from physics.models.arrays import BaseArray, BasicArray -from physics.models.battery import BaseBattery, BasicBattery, BatteryModel +from physics.models.battery import BaseBattery, BasicBattery, EquivalentCircuitBatteryModel, BatteryModelConfig from physics.models.lvs import BaseLVS, BasicLVS -from physics.models.motor import BaseMotor, BasicMotor +from physics.models.motor import BaseMotor, BasicMotor, AdvancedMotor +from physics.models.aeroshell.aeroshell import Aeroshell from physics.models.regen import BaseRegen, BasicRegen from physics.environment.gis import BaseGIS, GIS from physics.environment.meteorology import ( @@ -36,7 +37,6 @@ CloudedMeteorology, ) - load_dotenv() @@ -71,6 +71,7 @@ def __init__(self, cache: Cache = None): self.gis: Optional[BaseGIS] = None self.meteorology: Optional[BaseMeteorology] = None self.motor: Optional[BaseMotor] = None + self.aeroshell: Optional[Aeroshell] = None self.battery: Optional[BaseBattery] = None self.regen: Optional[BaseRegen] = None self.max_acceleration: Optional[float] = None @@ -90,6 +91,7 @@ def __init__(self, cache: Cache = None): self.current_coord: Optional[NDArray] = None self.initial_battery_charge: Optional[float] = None self.start_time: Optional[int] = None + self.num_laps: Optional[int] = None # Hyperparameters self.simulation_period: Optional[int] = None @@ -269,6 +271,10 @@ def _set_route_data(self): route: Route = self._cache.get(route_data_path) + if hasattr(competition_config, "tiling"): + if route.tiling != competition_config.tiling: + raise KeyError + # Generate new route data data except KeyError: match competition_config.competition_type: @@ -305,7 +311,7 @@ def _set_route_data(self): def _set_weather_data(self): environment_config = self._environment_config - environment_hash = ModelBuilder._truncate_hash(hash(environment_config)) + environment_hash = ModelBuilder._truncate_hash(hash(environment_config) + hash(environment_config.weather_query_config)) weather_data_path = WeatherPath / environment_hash weather_query_config = environment_config.weather_query_config @@ -395,23 +401,43 @@ def compile(self): self.battery = BasicBattery(self.initial_battery_charge, **arguments) case "BatteryModel": - self.battery = BatteryModel( - self._car_config.battery_config, self.initial_battery_charge + battery_config = BatteryModelConfig( + self._car_config.battery_config.R_0_data, + self._car_config.battery_config.SOC_data, + self._car_config.battery_config.R_P_data, + self._car_config.battery_config.C_P_data, + self._car_config.battery_config.Uoc_data, + self._car_config.battery_config.Q_total ) + # battery_config = BatteryModelConfig(**self._car_config.battery_config.model_dump()) - self.motor = BasicMotor( - vehicle_mass=self._car_config.vehicle_config.vehicle_mass, - **self._car_config.motor_config.model_dump(), - ) + self.battery = EquivalentCircuitBatteryModel(battery_config, self.initial_battery_charge) + + match self._car_config.motor_config.motor_type: + case "BasicMotor": + self.motor = BasicMotor( + vehicle_mass=self._car_config.vehicle_config.vehicle_mass, + **self._car_config.motor_config.model_dump(), + ) + + case "AdvancedMotor": + self.motor = AdvancedMotor( + vehicle_mass=self._car_config.vehicle_config.vehicle_mass, + **self._car_config.motor_config.model_dump() + ) + + match self._car_config.aeroshell_config: + case "Aeroshell": + self.aeroshell = Aeroshell() self.regen = BasicRegen(self._car_config.vehicle_config.vehicle_mass) - tiling = self.route_data.tiling + self.num_laps = self.route_data.tiling route_data = { - "path": np.tile(self.route_data.coords, (tiling, 1)), + "path": np.tile(self.route_data.coords, (self.num_laps, 1)), "num_unique_coords": len(self.route_data.coords), - "time_zones": np.tile(self.route_data.path_time_zones, tiling), - "elevations": np.tile(self.route_data.path_elevations, tiling), + "time_zones": np.tile(self.route_data.path_time_zones, self.num_laps), + "elevations": np.tile(self.route_data.path_elevations, self.num_laps), } self.gis = GIS(route_data, self.origin_coord, self.current_coord) @@ -454,6 +480,7 @@ def get(self) -> Model: array=self.array, battery=self.battery, motor=self.motor, + aeroshell = self.aeroshell, lvs=self.lvs, regen=self.regen, gis=self.gis, @@ -461,4 +488,5 @@ def get(self) -> Model: max_acceleration=self.max_acceleration, max_deceleration=self.max_deceleration, start_time=self.start_time, - ) + num_laps = self.num_laps + ) \ No newline at end of file diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index d9386097..d89e63b1 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -51,6 +51,8 @@ def __init__(self, model): self.tick_array = None self.time_zones = None self.distances = None + self.drag_force = None + self.down_force = None self.cumulative_distances = None self.closest_gis_indices = None self.closest_weather_indices = None @@ -73,6 +75,7 @@ def __init__(self, model): self.time_in_motion = None self.final_soc = None self.map_data_indices = None + self.wind_attack_angles = None def run_simulation_calculations(self, speed_kmh: NDArray) -> None: """ @@ -170,6 +173,15 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.gis_vehicle_bearings, self.absolute_wind_speeds, self.wind_directions ) + #with calculated wind_speeds, we can not calculate drag and down forces in order to pass into motor model calculations + self.drag_force = self.model.aeroshell.calculate_drag_force(self.wind_speeds, self.wind_attack_angles, self.speed_kmh, + "drag") + + self.down_force = self.model.aeroshell.calculate_down_force(self.wind_speeds, self.wind_attack_angles, self.speed_kmh, + "down") + + + # Get an array of solar irradiance at every coordinate and time self.solar_irradiances = self.model.meteorology.calculate_solar_irradiances( self.model.route_coords[self.closest_gis_indices], @@ -178,6 +190,8 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.gis_route_elevations_at_each_tick, ) + + # TLDR: we have now obtained solar irradiances, wind speeds, and gradients at each tick # ----- Energy Calculations ----- @@ -187,7 +201,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: ) self.motor_consumed_energy = self.model.motor.calculate_energy_in( - self.speed_kmh, self.gradients, self.wind_speeds, self.model.simulation_dt + self.speed_kmh, self.gradients, self.drag_force, self.down_force, self.model.simulation_dt ) self.array_produced_energy = self.model.solar_array.calculate_produced_energy( @@ -332,6 +346,11 @@ def get_results( "map_data_indices": self.map_data_indices, "path_coordinates": self.model.gis.get_path(), "raw_soc": self.raw_soc, + "drag_force": self.drag_force, + "down_force": self.down_force, + "wind_attack_angles": self.wind_attack_angles, + + } if "default" in requested_properties or requested_properties == "default": From 7236454dce3da6645203f71e8ab380cd73d1d27b Mon Sep 17 00:00:00 2001 From: sanar Date: Tue, 23 Sep 2025 19:43:00 -0700 Subject: [PATCH 28/44] - --- diagnostic_interface/__init__.py | 4 ++-- diagnostic_interface/main_window.py | 2 +- diagnostic_interface/tabs/__init__.py | 4 ++-- diagnostic_interface/tabs/plot_tab.py | 3 ++- simulation/config/models/_car.py | 9 +++++--- simulation/model/Model.py | 2 +- simulation/model/ModelBuilder.py | 30 +++++++++++++-------------- simulation/model/Simulation.py | 24 +++++++-------------- 8 files changed, 36 insertions(+), 42 deletions(-) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index 005d9f70..cf3cd8b3 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -2,7 +2,7 @@ from widgets import TimedWidget, DataSelect from dialog import SettingsDialog from canvas import PlotCanvas, CustomNavigationToolbar -from tabs import PlotTab2, PlotTab +from tabs import PlotTab #from .copy import PlotTab2 # from tabs import DockerStackTab, PlotTab @@ -13,7 +13,7 @@ "CustomNavigationToolbar", "PlotCanvas", "PlotTab", - "PlotTab2", + #"PlotTab2", "DataSelect", # "DockerStackTab", "settings" diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 9c98e49e..3b088d84 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -14,7 +14,7 @@ from data_tools import SunbeamClient from diagnostic_interface.widgets import DataSelect # from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab -from diagnostic_interface.tabs import PlotTab2, PlotTab, UpdatableTab +from diagnostic_interface.tabs import PlotTab, UpdatableTab from diagnostic_interface.dialog import SettingsDialog diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index 295806c0..fd2171c1 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,5 +1,5 @@ # from .docker_panel import DockerStackTab -from .plot_tab import PlotTab2 +#from .plot_tab import PlotTab2 from .plot_tab import PlotTab from ._updatable import UpdatableTab # from .sunbeam_panel import SunbeamTab @@ -9,7 +9,7 @@ __all__ = [ # "DockerStackTab", "PlotTab", - "PlotTab2", + #"PlotTab2", "UpdatableTab", # "SunbeamTab", # "SunlinkTab", diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index e464f4f1..ce7bf999 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -6,7 +6,8 @@ #from poetry.console.commands import self from diagnostic_interface import settings -from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas, PlotCanvas2 +from diagnostic_interface.canvas import (CustomNavigationToolbar, PlotCanvas) +#PlotCanvas2) HELP_MESSAGES = { diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index 98f5800b..fbfb8609 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -97,6 +97,7 @@ class MotorConfig(Config): class BasicMotorConfig(MotorConfig): pass + class AdvancedMotorConfig(MotorConfig): cornering_coefficient: float @@ -105,8 +106,10 @@ class AeroshellConfig(Config): """ Configuration object describing the aerodynamics forces (specifically drag and downforce) of a vehicle. """ - model_config = ConfigDict(frozen=True) #drag vs down - + model_config = ConfigDict(frozen=True) + drag_lookup: dict[float:float] #lookup table that corresponds angles to drag force, computed by a CFD + down_lookup: dict[float:float] #lookup table that corresponds angles to down force, computed by a CFD + wind_reference_speed: float # a reference wind speed in order to scale aerodynamic force calculations class RegenConfig(Config): """ @@ -133,4 +136,4 @@ class CarConfig(Config): regen_config: RegenConfig aeroshell_config: AeroshellConfig - name: str \ No newline at end of file + name: str diff --git a/simulation/model/Model.py b/simulation/model/Model.py index 3e29c68b..7a92c3b2 100644 --- a/simulation/model/Model.py +++ b/simulation/model/Model.py @@ -15,7 +15,7 @@ from physics.models.battery import BaseBattery from physics.models.lvs import BaseLVS from physics.models.motor import BaseMotor -from physics.models.aeroshell.aeroshell import Aeroshell +from physics.models.aeroshell import Aeroshell from physics.models.regen import BaseRegen from physics.environment.gis import BaseGIS from physics.environment.meteorology import BaseMeteorology diff --git a/simulation/model/ModelBuilder.py b/simulation/model/ModelBuilder.py index f148f9e3..51e4e850 100644 --- a/simulation/model/ModelBuilder.py +++ b/simulation/model/ModelBuilder.py @@ -28,7 +28,7 @@ from physics.models.battery import BaseBattery, BasicBattery, EquivalentCircuitBatteryModel, BatteryModelConfig from physics.models.lvs import BaseLVS, BasicLVS from physics.models.motor import BaseMotor, BasicMotor, AdvancedMotor -from physics.models.aeroshell.aeroshell import Aeroshell +from physics.models.aeroshell import Aeroshell from physics.models.regen import BaseRegen, BasicRegen from physics.environment.gis import BaseGIS, GIS from physics.environment.meteorology import ( @@ -47,7 +47,6 @@ class ModelBuilder: This class follows the fluid builder pattern, such that its methods return itself allowing for operations like, >>> ModelBuilder().set_environment_config(environment_config).set_initial_conditions(initial_conditions).compile() - Once all configuration has been set, `ModelBuilder` can be compiled, and then a `Model` acquired from it with `get`. """ @@ -123,11 +122,11 @@ def set_hyperparameters(self, hyperparameters: SimulationHyperparametersConfig): return self def set_environment_config( - self, - environment_config: EnvironmentConfig, - rebuild_weather_cache: bool = False, - rebuild_competition_cache: bool = False, - rebuild_route_cache: bool = False, + self, + environment_config: EnvironmentConfig, + rebuild_weather_cache: bool = False, + rebuild_competition_cache: bool = False, + rebuild_route_cache: bool = False, ): """ Set the environment configuration of the `Model` to be built. @@ -305,13 +304,14 @@ def _set_route_data(self): self.origin_coord = route.coords[0] self.dest_coord = route.coords[-1] self.waypoints = route.coords[ - 1:-1 - ] # Get all coords between first and last coordinate + 1:-1 + ] # Get all coords between first and last coordinate def _set_weather_data(self): environment_config = self._environment_config - environment_hash = ModelBuilder._truncate_hash(hash(environment_config) + hash(environment_config.weather_query_config)) + environment_hash = ModelBuilder._truncate_hash( + hash(environment_config) + hash(environment_config.weather_query_config)) weather_data_path = WeatherPath / environment_hash weather_query_config = environment_config.weather_query_config @@ -426,9 +426,7 @@ def compile(self): **self._car_config.motor_config.model_dump() ) - match self._car_config.aeroshell_config: - case "Aeroshell": - self.aeroshell = Aeroshell() + self.aeroshell = Aeroshell(**self._car_config.aeroshell_config.model_dump()) self.regen = BasicRegen(self._car_config.vehicle_config.vehicle_mass) @@ -480,7 +478,7 @@ def get(self) -> Model: array=self.array, battery=self.battery, motor=self.motor, - aeroshell = self.aeroshell, + aeroshell=self.aeroshell, lvs=self.lvs, regen=self.regen, gis=self.gis, @@ -488,5 +486,5 @@ def get(self) -> Model: max_acceleration=self.max_acceleration, max_deceleration=self.max_deceleration, start_time=self.start_time, - num_laps = self.num_laps - ) \ No newline at end of file + num_laps=self.num_laps + ) diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index d89e63b1..89f16888 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -132,7 +132,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.max_route_distance = self.cumulative_distances[-1] self.route_length = ( - self.max_route_distance / 1000.0 + self.max_route_distance / 1000.0 ) # store the route length in kilometers # Array of elevations at every route point @@ -173,14 +173,9 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.gis_vehicle_bearings, self.absolute_wind_speeds, self.wind_directions ) - #with calculated wind_speeds, we can not calculate drag and down forces in order to pass into motor model calculations - self.drag_force = self.model.aeroshell.calculate_drag_force(self.wind_speeds, self.wind_attack_angles, self.speed_kmh, - "drag") - - self.down_force = self.model.aeroshell.calculate_down_force(self.wind_speeds, self.wind_attack_angles, self.speed_kmh, - "down") - - + # with calculated wind_speeds, we can now calculate (aerodynamic) drag and down forces in order to pass into motor model calculations + self.drag_force = self.model.aeroshell.calculate_drag(self.wind_speeds, self.wind_attack_angles, self.speed_kmh/3.6) + self.down_force = self.model.aeroshell.calculate_down(self.wind_speeds, self.wind_attack_angles, self.speed_kmh/3.6) # Get an array of solar irradiance at every coordinate and time self.solar_irradiances = self.model.meteorology.calculate_solar_irradiances( @@ -190,8 +185,6 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.gis_route_elevations_at_each_tick, ) - - # TLDR: we have now obtained solar irradiances, wind speeds, and gradients at each tick # ----- Energy Calculations ----- @@ -212,8 +205,8 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.speed_kmh, self.gis_route_elevations_at_each_tick, 0.0, 10000.0 ) - self.not_charge = self.model.race.charging_boolean[self.model.start_time :] - self.not_race = self.model.race.driving_boolean[self.model.start_time :] + self.not_charge = self.model.race.charging_boolean[self.model.start_time:] + self.not_race = self.model.race.driving_boolean[self.model.start_time:] if self.model.simulation_dt != 1: self.not_charge = self.not_charge[:: self.model.simulation_dt] @@ -257,7 +250,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: # self.speed_kmh = np.logical_and(self.not_charge, self.state_of_charge) * self.speed_kmh self.time_in_motion = ( - np.logical_and(self.tick_array, self.speed_kmh) * self.model.simulation_dt + np.logical_and(self.tick_array, self.speed_kmh) * self.model.simulation_dt ) self.final_soc = self.state_of_charge[-1] * 100 + 0.0 @@ -282,7 +275,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.calculations_have_happened = True def get_results( - self, requested_properties: Union[list, str] + self, requested_properties: Union[list, str] ) -> Union[list, np.ndarray, float]: """ @@ -350,7 +343,6 @@ def get_results( "down_force": self.down_force, "wind_attack_angles": self.wind_attack_angles, - } if "default" in requested_properties or requested_properties == "default": From 55a1874c41a283a17755d2c27935c47e0dd35cef Mon Sep 17 00:00:00 2001 From: sanar Date: Tue, 30 Sep 2025 19:28:01 -0700 Subject: [PATCH 29/44] - --- diagnostic_interface/canvas/custom_toolbar.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/diagnostic_interface/canvas/custom_toolbar.py b/diagnostic_interface/canvas/custom_toolbar.py index 2a93ca1f..e59e8c4a 100644 --- a/diagnostic_interface/canvas/custom_toolbar.py +++ b/diagnostic_interface/canvas/custom_toolbar.py @@ -3,8 +3,6 @@ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -from PyQt5.QtCore import QSize - class CustomNavigationToolbar(NavigationToolbar): """Custom toolbar with tooltips for each button.""" @@ -23,16 +21,6 @@ def __init__(self, canvas, parent=None): self.save_data_action.triggered.connect(self.canvas.save_data_to_csv) self.addAction(self.save_data_action) - - - - #stuff added: - - - self.setIconSize(QSize(24, 24)) - self.setFixedHeight(25) - - # Define tooltips for each standard tool in the toolbar tooltips = { "Home": "Reset view.", From 66d74efa55bfe5839b8da65a7cb80f8e0a7d1296 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Tue, 30 Sep 2025 19:29:42 -0700 Subject: [PATCH 30/44] Undo changes to diagnostic_interface --- diagnostic_interface/__init__.py | 20 +- diagnostic_interface/canvas/__init__.py | 10 +- diagnostic_interface/canvas/custom_toolbar.py | 12 + diagnostic_interface/canvas/plot_canvas.py | 238 ++++++++--- diagnostic_interface/config.py | 403 ++++++++++++++++-- diagnostic_interface/dialog/__init__.py | 4 +- .../dialog/settings_window.py | 18 +- diagnostic_interface/main_window.py | 103 ++--- diagnostic_interface/settings.toml | 6 +- diagnostic_interface/start_application.py | 12 +- diagnostic_interface/tabs/__init__.py | 31 +- diagnostic_interface/tabs/docker_panel.py | 52 ++- diagnostic_interface/tabs/plot_tab.py | 162 ++----- diagnostic_interface/tabs/sunbeam_panel.py | 36 +- diagnostic_interface/tabs/sunlink_panel.py | 33 +- diagnostic_interface/tabs/telemetry_panel.py | 35 +- diagnostic_interface/widgets/__init__.py | 17 +- diagnostic_interface/widgets/data_select.py | 6 +- .../widgets/docker_log_widget.py | 181 ++++++-- 19 files changed, 983 insertions(+), 396 deletions(-) diff --git a/diagnostic_interface/__init__.py b/diagnostic_interface/__init__.py index cf3cd8b3..8db1cf45 100644 --- a/diagnostic_interface/__init__.py +++ b/diagnostic_interface/__init__.py @@ -1,11 +1,8 @@ -from .config import settings -from widgets import TimedWidget, DataSelect -from dialog import SettingsDialog -from canvas import PlotCanvas, CustomNavigationToolbar -from tabs import PlotTab -#from .copy import PlotTab2 - -# from tabs import DockerStackTab, PlotTab +from .config import settings, command_settings, coords +from .widgets import TimedWidget, DataSelect +from .dialog import SettingsDialog +from .canvas import PlotCanvas, CustomNavigationToolbar +from .tabs import DockerStackTab, PlotTab __all__ = [ "TimedWidget", @@ -13,8 +10,9 @@ "CustomNavigationToolbar", "PlotCanvas", "PlotTab", - #"PlotTab2", "DataSelect", - # "DockerStackTab", - "settings" + "DockerStackTab", + "settings", + "command_settings", + "coords" ] diff --git a/diagnostic_interface/canvas/__init__.py b/diagnostic_interface/canvas/__init__.py index 00bf973c..28c49d43 100644 --- a/diagnostic_interface/canvas/__init__.py +++ b/diagnostic_interface/canvas/__init__.py @@ -1,7 +1,13 @@ from .custom_toolbar import CustomNavigationToolbar -from .plot_canvas import PlotCanvas +from .plot_canvas import PlotCanvas, PlotCanvas2, IntegralPlot +#from .plot_canvas import PlotCanvas +#from .soc_canvas import SocCanvas +from .realtime_canvas import RealtimeCanvas __all__ = [ "CustomNavigationToolbar", - "PlotCanvas" + "PlotCanvas", "PlotCanvas2", "IntegralPlot", + #"SocCanvas" + "PlotCanvas", + "RealtimeCanvas" ] diff --git a/diagnostic_interface/canvas/custom_toolbar.py b/diagnostic_interface/canvas/custom_toolbar.py index e59e8c4a..2a93ca1f 100644 --- a/diagnostic_interface/canvas/custom_toolbar.py +++ b/diagnostic_interface/canvas/custom_toolbar.py @@ -3,6 +3,8 @@ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from PyQt5.QtCore import QSize + class CustomNavigationToolbar(NavigationToolbar): """Custom toolbar with tooltips for each button.""" @@ -21,6 +23,16 @@ def __init__(self, canvas, parent=None): self.save_data_action.triggered.connect(self.canvas.save_data_to_csv) self.addAction(self.save_data_action) + + + + #stuff added: + + + self.setIconSize(QSize(24, 24)) + self.setFixedHeight(25) + + # Define tooltips for each standard tool in the toolbar tooltips = { "Home": "Reset view.", diff --git a/diagnostic_interface/canvas/plot_canvas.py b/diagnostic_interface/canvas/plot_canvas.py index 6c2f4987..97570e38 100644 --- a/diagnostic_interface/canvas/plot_canvas.py +++ b/diagnostic_interface/canvas/plot_canvas.py @@ -4,14 +4,102 @@ from PyQt5.QtWidgets import QMessageBox, QFileDialog from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from data_tools import SunbeamClient, TimeSeries +from scipy.integrate import cumulative_trapezoid as trapz from diagnostic_interface import settings +from typing import Optional +import mplcursors +from dateutil import tz -# Use a light-theme friendly style plt.style.use("seaborn-v0_8-darkgrid") -# plt.style.use("fivethirtyeight") +local_tz = tz.tzlocal() class PlotCanvas(FigureCanvas): + def __init__(self, parent=None): + self.fig, self.ax = plt.subplots() + super().__init__(self.fig) + self.setParent(parent) + + self.current_data: Optional[TimeSeries] = None + + self.line = None + + def fetch_data(self, origin: str, event: str, source: str, data_name: str) -> TimeSeries: + client = SunbeamClient(settings.sunbeam_api_url) + file = client.get_file(origin, event, source, data_name) + result = file.unwrap() + + data = result.values if hasattr(result, "values") else result.data + self.current_data = data + + return data + + def plot(self, ts: TimeSeries, title: str, y_label: str) -> None: + x = ts.datetime_x_axis + y = ts + + if self.line is None: + self.line, = self.ax.plot(x, y, linewidth=1) + self.ax.set_title(title) + self.ax.set_xlabel("Time") + self.ax.set_ylabel(y_label) + + locator = mdates.AutoDateLocator() + formatter = mdates.DateFormatter("%H:%M", tz=local_tz) + + self.ax.xaxis.set_major_formatter(formatter) + self.ax.xaxis.set_major_locator(locator) + + self.figure.autofmt_xdate() + + else: + self.line.set_xdata(x) + self.line.set_ydata(y) + + self.ax.relim() + self.ax.autoscale_view(scalex=True, scaley=True) + self.draw() + + cursor = mplcursors.cursor(self.line, hover=True) + + @cursor.connect("add") + def _(sel): + x, y = sel.target # x is a float (matplotlib date), y is the y-value + dt = mdates.num2date(x, tz=local_tz) + sel.annotation.set_text( + f"{y:.2f} {ts.units} at {dt.strftime('%H:%M')}" + ) + # optional: tweak annotation style + bbox = sel.annotation.get_bbox_patch() + bbox.set_facecolor("white") + bbox.set_edgecolor("black") + bbox.set_alpha(0.8) + bbox.set_boxstyle("round,pad=0.3") + + def save_data_to_csv(self): + if self.current_data is None: + print("No data available to save.") + return + + options = QFileDialog.Options() + file_name, _ = QFileDialog.getSaveFileName( + None, + "Save Data", + f"{self.current_data_name}_{self.current_event}_{self.current_origin}_{self.current_source}.csv", + "CSV Files (*.csv);;All Files (*)", + options=options, + ) + + if file_name: + df = pd.DataFrame({ + "Time": self.current_data.datetime_x_axis, + f"{self.current_data_name}": self.current_data, + }) + df.to_csv(file_name, index=False) + print(f"Data saved to {file_name}") + + +class PlotCanvas2(FigureCanvas): def __init__(self, parent=None): self.fig, self.ax = plt.subplots() super().__init__(self.fig) @@ -23,47 +111,79 @@ def __init__(self, parent=None): self.current_origin = "" self.current_source = "" - self.line = None + self.line1 = None + self.line2 = None - def query_and_plot(self, origin, source, event, data_name): + def plot(self, data, data2): try: - data = self.query_data(origin, source, event, data_name) - if not isinstance(data, TimeSeries): - raise TypeError("Expected TimeSeries.") self.current_data = data - self.current_data_name = data_name - self.current_event = event - self.current_origin = origin - self.current_source = source - - if self.line is None: - self.line, = self.ax.plot(data.datetime_x_axis, data, linewidth=1) - self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.current_data2 = data2 + + if self.line1 is None and self.line2 is None: + self.line1, = self.ax.plot(data.datetime_x_axis, data, linewidth=1, color='red') + + self.ax2 = self.ax.twinx() + self.line2, = self.ax2.plot(data2.datetime_x_axis, data2, linewidth=1) + + #self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.ax.set_title("Wind Speed and Precipitation", fontsize=12) self.ax.set_xlabel("Time", fontsize=10) - self.ax.set_ylabel(data_name, fontsize=10) + self.ax.set_ylabel("WindSpeed10m", fontsize=10) + self.ax2.set_ylabel("PrecipitationRate", fontsize=10) - # Improve datetime formatting - self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) - self.ax.xaxis.set_major_locator(mdates.HourLocator()) - self.fig.autofmt_xdate() + self.ax.legend([self.line1, self.line2], ["Wind Speed", "Precipitation Rate"]) + + locator = mdates.AutoDateLocator() + formatter = mdates.DateFormatter("%H:%M", tz=local_tz) + + self.ax.xaxis.set_major_formatter(formatter) + self.ax.xaxis.set_major_locator(locator) else: # Only update data - self.line.set_xdata(data.datetime_x_axis) - self.line.set_ydata(data) + self.line1.set_xdata(data.datetime_x_axis) + self.line2.set_xdata(data2.datetime_x_axis) + self.line1.set_ydata(data) + self.line2.set_ydata(data2) self.ax.relim() self.ax.autoscale_view(scalex=True, scaley=True) + + self.ax2.relim() + self.ax2.autoscale_view(scalex=True, scaley=True) + self.fig.tight_layout() self.draw() + cursor = mplcursors.cursor([self.line1, self.line2], hover=True) + + @cursor.connect("add") + def _(sel): + x, y = sel.target # x is a float (matplotlib date), y is the y-value + dt = mdates.num2date(x, tz=local_tz) + sel.annotation.set_text( + f"{y:.2f} at {dt.strftime('%H:%M')}" + ) + # optional: tweak annotation style + bbox = sel.annotation.get_bbox_patch() + bbox.set_facecolor("white") + bbox.set_edgecolor("black") + bbox.set_alpha(0.8) + bbox.set_boxstyle("round,pad=0.3") + return True except Exception as e: QMessageBox.critical(None, "Plotting Error", f"Error fetching or plotting data:\n{str(e)}") return False + def fetch_data(self): + data = self.query_data(settings.realtime_pipeline, "weather", settings.realtime_event, "WindSpeed10m") + data2 = self.query_data(settings.realtime_pipeline, "weather", settings.realtime_event, "PrecipitationRate") + + return data, data2 + def query_data(self, origin, source, event, data_name): client = SunbeamClient(settings.sunbeam_api_url) file = client.get_file(origin, event, source, data_name) @@ -93,60 +213,71 @@ def save_data_to_csv(self): print(f"Data saved to {file_name}") - - - - -#temp: - - -class PlotCanvas2(FigureCanvas): +class IntegralPlot(FigureCanvas): def __init__(self, parent=None): - self.fig, self.ax = plt.subplots(figsize = (width, height)) + self.fig, self.ax = plt.subplots() super().__init__(self.fig) self.setParent(parent) self.current_data = None - self.current_data_name = "" - self.current_event = "" - self.current_origin = "" - self.current_source = "" + self.line1 = None - self.line = None + def plot(self, data, plot_title, ylabel): - def query_and_plot(self, origin, source, event, data_name): try: - data = self.query_data(origin, source, event, data_name) + integral_values = trapz(data, x=data.x_axis, initial=0) + if not isinstance(data, TimeSeries): raise TypeError("Expected TimeSeries.") - self.current_data = data - self.current_data_name = data_name - self.current_event = event - self.current_origin = origin - self.current_source = source - - if self.line is None: - self.line, = self.ax.plot(data.datetime_x_axis, data, linewidth=1) - self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.current_data = None + self.line1 = None + + if self.line1 is None: + + self.line1, = self.ax.plot(data.datetime_x_axis, integral_values, linewidth=1, color='red') + #add the comma so that its not just a normal 2d plot + + #self.ax.set_title(f"{data_name} - {event}", fontsize=12) + self.ax.set_title(plot_title, fontsize=12) self.ax.set_xlabel("Time", fontsize=10) - self.ax.set_ylabel(data_name, fontsize=10) + self.ax.set_ylabel(ylabel, fontsize=10) # Improve datetime formatting - self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) - self.ax.xaxis.set_major_locator(mdates.HourLocator()) - self.fig.autofmt_xdate() + locator = mdates.AutoDateLocator() + formatter = mdates.DateFormatter("%H:%M", tz=local_tz) + + self.ax.xaxis.set_major_formatter(formatter) + self.ax.xaxis.set_major_locator(locator) else: # Only update data - self.line.set_xdata(data.datetime_x_axis) - self.line.set_ydata(data) + self.line1.set_xdata(data.datetime_x_axis) + + self.line1.set_ydata(integral_values) self.ax.relim() self.ax.autoscale_view(scalex=True, scaley=True) + self.fig.tight_layout() self.draw() + cursor = mplcursors.cursor(self.line1, hover=True) + + @cursor.connect("add") + def _(sel): + x, y = sel.target # x is a float (matplotlib date), y is the y-value + dt = mdates.num2date(x, tz=local_tz) + sel.annotation.set_text( + f"{y/1e6:.2f} MJ/m^2 at {dt.strftime('%H:%M')}" + ) + # optional: tweak annotation style + bbox = sel.annotation.get_bbox_patch() + bbox.set_facecolor("white") + bbox.set_edgecolor("black") + bbox.set_alpha(0.8) + bbox.set_boxstyle("round,pad=0.3") + return True except Exception as e: @@ -169,6 +300,7 @@ def save_data_to_csv(self): None, "Save Data", f"{self.current_data_name}_{self.current_event}_{self.current_origin}_{self.current_source}.csv", + "CSV Files (*.csv);;All Files (*)", options=options, ) diff --git a/diagnostic_interface/config.py b/diagnostic_interface/config.py index 115eac42..e3cb508e 100644 --- a/diagnostic_interface/config.py +++ b/diagnostic_interface/config.py @@ -1,61 +1,374 @@ -from pydantic import BaseModel +from __future__ import annotations from pathlib import Path +from typing import Any, Generic, Type, TypeVar + import tomli import tomli_w -from typing import Protocol, cast +from pydantic import BaseModel, ConfigDict, Field +T = TypeVar("T", bound=BaseModel) -class Settings(BaseModel): - plot_timer_interval: int - sunbeam_api_url: str - sunbeam_path: str - sunlink_path: str - - -class SettingsProtocol(Protocol): - @property - def plot_timer_interval(self) -> int: ... - @property - def sunbeam_api_url(self) -> str: ... - @property - def sunbeam_path(self) -> str: ... - @property - def sunlink_path(self) -> str: ... - @plot_timer_interval.setter - def plot_timer_interval(self, new_interval: int) -> None: ... - @sunbeam_api_url.setter - def sunbeam_api_url(self, new_url: str) -> None: ... - @sunbeam_path.setter - def sunbeam_path(self, new_path: str) -> None: ... - @sunlink_path.setter - def sunlink_path(self, new_path: str) -> None: ... - - -class PersistentSettings: - def __init__(self, path: Path = Path(__file__).parent / "settings.toml"): + +class PersistentConfig(Generic[T]): + """ + Generic wrapper around a Pydantic BaseModel that + loads from / saves to a TOML file, with atomic writes + and auto-saving on attribute mutation. + """ + + def __init__(self, model_cls: Type[T], path: Path): + self._model_cls = model_cls self._path = path - self._model = self._load() + self._model: T = self._load() - def _load(self) -> Settings: + def _load(self) -> T: if self._path.exists(): with self._path.open("rb") as f: data = tomli.load(f) - return Settings(**data) + + return self._model_cls(**data) + + # if doesn't already, exist, use default + return self._model_cls() + + def save(self) -> None: + # atomic write: write to .tmp then replace + tmp = self._path.with_suffix(self._path.suffix + ".tmp") + tomli_w.dump(self._model.model_dump(), tmp.open("wb")) + tmp.replace(self._path) + + def __getattr__(self, name: str) -> Any: + # proxy all unknown attrs to the model + return getattr(self._model, name) + + def __setattr__(self, name: str, value: Any) -> None: + # internal attributes bypass proxy + if name in {"_model", "_model_cls", "_path"}: + super().__setattr__(name, value) + return + + # if model has the field, set/save; else normal setattr + if name in self._model.model_fields: + setattr(self._model, name, value) + self.save() else: - return Settings() + super().__setattr__(name, value) + + +class Settings(BaseModel): + model_config = ConfigDict(extra="ignore") + + plot_timer_interval: int = Field(5, gt=0, description="seconds between plot refresh") + sunbeam_api_url: str = Field(..., description="Base URL for Sunbeam API") + sunbeam_path: str = Field(..., description="Filesystem path to Sunbeam project") + sunlink_path: str = Field(..., description="Filesystem path to Sunlink project") + realtime_event: str = Field(..., description="Realtime event for realtime-data visualization") + realtime_pipeline: str = Field(..., description="Realtime pipeline for realtime-data visualization") + + +class CommandSettings(BaseModel): + model_config = ConfigDict(extra="ignore") - def save(self): - with self._path.open("wb") as f: - tomli_w.dump(self._model.model_dump(), f) + sunbeam_up_cmd: str = Field(..., description="docker-compose up command for Sunbeam") + sunbeam_down_cmd: str = Field(..., description="docker-compose down command for Sunbeam") + sunlink_up_cmd: str = Field(..., description="docker-compose up command for Sunlink") + sunlink_down_cmd: str = Field(..., description="docker-compose down command for Sunlink") + telemetry_enable_cmd: str = Field(..., description="command to enable the telemetry link") - def __getattr__(self, item): - return getattr(self._model, item) - def __setattr__(self, key, value): - if key in {"_path", "_model"}: - return super().__setattr__(key, value) - setattr(self._model, key, value) - self.save() +BASE = Path(__file__).parent +settings = PersistentConfig(Settings, BASE / "settings.toml") +command_settings = PersistentConfig(CommandSettings, BASE / "commands.toml") -settings = cast(SettingsProtocol, PersistentSettings()) +coords = [ + [36.99932082, -86.37230251], + [36.99940337, -86.37214797], + [36.99948814, -86.37199725], + [36.99958754, -86.37189043], + [36.99970528, -86.37178738], + [36.99982625, -86.37169183], + [36.99997073, -86.37159181], + [37.00011471, -86.3714889], + [37.00027357, -86.37138907], + [37.0003852, -86.37123775], + [37.00042033, -86.37099884], + [37.00038811, -86.3707825], + [37.00031195, -86.37062837], + [37.00026491, -86.37041498], + [37.00030254, -86.37020247], + [37.00038472, -86.3700261], + [37.0004699, -86.36984594], + [37.00056383, -86.36964375], + [37.00064015, -86.3694857], + [37.00070993, -86.36934481], + [37.0008098, -86.36912411], + [37.00090662, -86.36891836], + [37.00098578, -86.36874579], + [37.00107373, -86.36854755], + [37.0011529, -86.36837867], + [37.00122817, -86.3682181], + [37.00133071, -86.36801267], + [37.00143614, -86.36779264], + [37.00152389, -86.3675912], + [37.00160574, -86.36740819], + [37.00167596, -86.36725066], + [37.00175285, -86.36709064], + [37.00183166, -86.36691875], + [37.00192538, -86.36670617], + [37.00200136, -86.36653034], + [37.00208623, -86.36635086], + [37.00215644, -86.36619701], + [37.00222549, -86.36603626], + [37.00229839, -86.3658645], + [37.00237732, -86.36569622], + [37.00245038, -86.36553914], + [37.00252912, -86.36537128], + [37.00259904, -86.36521818], + [37.00266755, -86.36507091], + [37.00274639, -86.36490341], + [37.00283342, -86.36471029], + [37.00291248, -86.36454704], + [37.00298517, -86.36439075], + [37.0030636, -86.36423803], + [37.00313338, -86.36408574], + [37.00320937, -86.36393701], + [37.00330797, -86.36377724], + [37.00343811, -86.36368662], + [37.00357758, -86.36365019], + [37.00372489, -86.36360692], + [37.00388711, -86.36356354], + [37.00405472, -86.36352621], + [37.00423763, -86.36348621], + [37.00437129, -86.36338074], + [37.00448184, -86.36323899], + [37.00457515, -86.36307953], + [37.0047012, -86.36286956], + [37.00486024, -86.36273924], + [37.00505061, -86.36270756], + [37.00527945, -86.36272947], + [37.00548802, -86.36263566], + [37.00565341, -86.36245496], + [37.00573513, -86.3621647], + [37.00568611, -86.36182869], + [37.00548782, -86.36157939], + [37.0052881, -86.36149696], + [37.00511652, -86.36149669], + [37.0049746, -86.36158761], + [37.00485989, -86.3616726], + [37.00469955, -86.3617696], + [37.00451492, -86.36178471], + [37.00435852, -86.36173599], + [37.00419576, -86.36162316], + [37.00409127, -86.36146652], + [37.00404463, -86.3612301], + [37.00407232, -86.36097832], + [37.00415922, -86.36078578], + [37.00426711, -86.36066944], + [37.00439509, -86.36060407], + [37.00452844, -86.36057503], + [37.00466778, -86.36054604], + [37.004833, -86.36050987], + [37.00499495, -86.36047743], + [37.00514229, -86.36044484], + [37.00524902, -86.36041601], + [37.00541074, -86.36037856], + [37.00558676, -86.36034575], + [37.00578957, -86.36038183], + [37.00596102, -86.36045445], + [37.00607154, -86.36065091], + [37.0061651, -86.36090615], + [37.00626234, -86.3611639], + [37.00637312, -86.36147776], + [37.00642557, -86.36179897], + [37.00644459, -86.36216748], + [37.00637929, -86.36250433], + [37.00629836, -86.36273906], + [37.00622229, -86.36291634], + [37.00611158, -86.36309997], + [37.00600738, -86.36323379], + [37.00589169, -86.3633461], + [37.00576471, -86.36344689], + [37.00564905, -86.36353398], + [37.00551878, -86.36359932], + [37.00537978, -86.36365448], + [37.00525188, -86.36370605], + [37.0051236, -86.36376501], + [37.00497727, -86.36382802], + [37.00484855, -86.36391978], + [37.00471266, -86.36403361], + [37.0045783, -86.36422396], + [37.00447907, -86.36439954], + [37.00435695, -86.36453807], + [37.00424813, -86.36465022], + [37.00411061, -86.36480228], + [37.00397977, -86.36492984], + [37.0038682, -86.36504946], + [37.00376295, -86.36516126], + [37.00365436, -86.36527329], + [37.00354759, -86.36539144], + [37.00341675, -86.36552332], + [37.00329206, -86.3656715], + [37.00316095, -86.36581567], + [37.00303256, -86.36594441], + [37.00291203, -86.36607157], + [37.00278014, -86.36622802], + [37.00269333, -86.36641303], + [37.00260606, -86.36659437], + [37.00252845, -86.36675984], + [37.00243487, -86.36695271], + [37.00234795, -86.36716654], + [37.00226102, -86.36735999], + [37.00218046, -86.36754144], + [37.00210026, -86.36771854], + [37.00202358, -86.36787901], + [37.00194406, -86.36806693], + [37.00185835, -86.36825428], + [37.00177578, -86.36843604], + [37.00169902, -86.36860048], + [37.0016732, -86.3687928], + [37.001679, -86.3689974], + [37.00168488, -86.36919051], + [37.00169122, -86.36938766], + [37.00169469, -86.3695562], + [37.00170118, -86.36974238], + [37.00171055, -86.36993991], + [37.00167198, -86.37010864], + [37.00158558, -86.37021273], + [37.00148934, -86.37030898], + [37.00139295, -86.37041757], + [37.00129011, -86.37051816], + [37.00120286, -86.37061913], + [37.00112253, -86.37073184], + [37.00106144, -86.37085264], + [37.00099678, -86.37100197], + [37.00092255, -86.37116696], + [37.00083249, -86.37137665], + [37.00073506, -86.37159438], + [37.00065426, -86.37177611], + [37.00057398, -86.37196168], + [37.00060097, -86.37214635], + [37.00074272, -86.37221467], + [37.0009059, -86.37222018], + [37.00107657, -86.37218846], + [37.00122503, -86.37211976], + [37.00133803, -86.37204677], + [37.00145467, -86.37192099], + [37.00156457, -86.37178347], + [37.00167114, -86.37162599], + [37.00176434, -86.37145299], + [37.00183833, -86.37126821], + [37.00188646, -86.37112396], + [37.0019441, -86.37095574], + [37.00197296, -86.370787], + [37.00202945, -86.37067751], + [37.00209668, -86.37048098], + [37.00218305, -86.37031655], + [37.00228558, -86.37016005], + [37.00238818, -86.37002764], + [37.00249377, -86.36991909], + [37.00259639, -86.36983868], + [37.00270864, -86.36976266], + [37.00281411, -86.3697033], + [37.00292264, -86.36964769], + [37.00303847, -86.369599], + [37.00316942, -86.36952703], + [37.00332578, -86.36939126], + [37.00342743, -86.36921198], + [37.00346211, -86.36901195], + [37.00343231, -86.36879509], + [37.00336736, -86.36861758], + [37.00327983, -86.36847602], + [37.00316589, -86.36828932], + [37.00305696, -86.36810259], + [37.00296937, -86.36793662], + [37.00293802, -86.36773416], + [37.00295792, -86.36753614], + [37.00301643, -86.36739089], + [37.00307192, -86.36724611], + [37.00312788, -86.36711056], + [37.00320856, -86.36698638], + [37.00331523, -86.36684361], + [37.00340519, -86.36672019], + [37.00350264, -86.36658791], + [37.00361135, -86.36649982], + [37.00374245, -86.36645177], + [37.00387034, -86.3664758], + [37.00395668, -86.36655587], + [37.00402368, -86.36670753], + [37.00407807, -86.36686779], + [37.00412607, -86.36701603], + [37.00418686, -86.36718831], + [37.00427315, -86.36742386], + [37.00436296, -86.3676927], + [37.00442144, -86.36790543], + [37.00445858, -86.36815496], + [37.00447715, -86.36847342], + [37.00444541, -86.36882644], + [37.00434238, -86.36911386], + [37.00425654, -86.36927291], + [37.00418289, -86.36939303], + [37.00410293, -86.36950912], + [37.00399731, -86.36962489], + [37.00390147, -86.36973254], + [37.00379293, -86.36984054], + [37.00369701, -86.36994104], + [37.00359474, -86.37004514], + [37.0034838, -86.3701598], + [37.00338477, -86.37024376], + [37.00327609, -86.37032788], + [37.00318982, -86.37040789], + [37.00306205, -86.37049992], + [37.00294389, -86.37059192], + [37.00283177, -86.37070006], + [37.00272312, -86.37083897], + [37.00264003, -86.37094293], + [37.00255683, -86.37103895], + [37.00249914, -86.37115127], + [37.00243497, -86.37126771], + [37.00236746, -86.3713964], + [37.00230001, -86.37152106], + [37.00223561, -86.37164596], + [37.00217132, -86.37175865], + [37.0021135, -86.37187932], + [37.00204601, -86.37200003], + [37.00196791, -86.37211434], + [37.00188761, -86.3722312], + [37.00180726, -86.37233202], + [37.00173651, -86.37241676], + [37.00165284, -86.37250555], + [37.00155067, -86.3726109], + [37.00144762, -86.37271211], + [37.00136704, -86.37279282], + [37.00126713, -86.37288565], + [37.00115123, -86.37299929], + [37.00103518, -86.37310829], + [37.0009191, -86.37321738], + [37.00082561, -86.37329809], + [37.00072863, -86.37339546], + [37.00061364, -86.37350099], + [37.00051698, -86.37360214], + [37.00042667, -86.37368311], + [37.00032338, -86.37378434], + [37.00022342, -86.37387665], + [37.00012018, -86.3739737], + [37.00002051, -86.37405916], + [36.99991404, -86.37415222], + [36.99981076, -86.37423248], + [36.99970432, -86.37430523], + [36.99958169, -86.37432571], + [36.99946928, -86.37430399], + [36.99934964, -86.37428072], + [36.99922365, -86.37425686], + [36.99908793, -86.37422888], + [36.99899069, -86.37413638], + [36.99896465, -86.37397888], + [36.9989808, -86.37380904], + [36.99900664, -86.37362303], + [36.99904589, -86.37342887], + [36.9990885, -86.37323096], + [36.99912743, -86.3730292], + [36.99916641, -86.37284378], + [36.99921127, -86.37261758], + [36.99924349, -86.37245211], +] diff --git a/diagnostic_interface/dialog/__init__.py b/diagnostic_interface/dialog/__init__.py index 9cd68346..43e4f598 100644 --- a/diagnostic_interface/dialog/__init__.py +++ b/diagnostic_interface/dialog/__init__.py @@ -1,5 +1,7 @@ from .settings_window import SettingsDialog +from .command_dialog import TextEditDialog __all__ = [ - "SettingsDialog" + "SettingsDialog", + "TextEditDialog" ] diff --git a/diagnostic_interface/dialog/settings_window.py b/diagnostic_interface/dialog/settings_window.py index 3f782a99..b7419c1e 100644 --- a/diagnostic_interface/dialog/settings_window.py +++ b/diagnostic_interface/dialog/settings_window.py @@ -66,6 +66,8 @@ def __init__( current_client_address: str, current_sunbeam_path: str, current_sunlink_path: str, + current_realtime_event: str, + current_realtime_pipeline: str, parent=None ): """ @@ -89,6 +91,16 @@ def __init__( self._client_input.setText(current_client_address) layout.addRow("Sunbeam API URL:", self._client_input) + # Event selector + self._event_input = QLineEdit() + self._event_input.setText(current_realtime_event) + layout.addRow("Realtime Event:", self._event_input) + + # Pipeline selector + self._pipeline_selector = QLineEdit() + self._pipeline_selector.setText(current_realtime_pipeline) + layout.addRow("Realtime Pipeline:", self._pipeline_selector) + self._selected_sunlink_path = current_sunlink_path self._sunlink_path_widget = PathSelectionBox(current_sunlink_path, self.select_sunlink_folder) layout.addRow("Sunlink Path:", self._sunlink_path_widget) @@ -123,10 +135,12 @@ def select_sunlink_folder(self): def select_sunbeam_folder(self): self._select_docker_folder("_selected_sunbeam_path", "_sunbeam_path_widget") - def get_settings(self) -> tuple[int, str, str, str]: + def get_settings(self) -> tuple[int, str, str, str, str, str]: return ( self._interval_spinbox.value(), self._client_input.text(), self._selected_sunbeam_path, - self._selected_sunlink_path + self._selected_sunlink_path, + self._event_input.text(), + self._pipeline_selector.text(), ) diff --git a/diagnostic_interface/main_window.py b/diagnostic_interface/main_window.py index 3b088d84..175fb6e3 100644 --- a/diagnostic_interface/main_window.py +++ b/diagnostic_interface/main_window.py @@ -1,45 +1,34 @@ -import pathlib - -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import ( - QMainWindow, - QWidget, - QPushButton, - QLabel, - QVBoxLayout, - QTabWidget, -) -from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt -from data_tools import SunbeamClient -from diagnostic_interface.widgets import DataSelect -# from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, PlotTab, UpdatableTab, TelemetryTab -from diagnostic_interface.tabs import PlotTab, UpdatableTab - - +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QVBoxLayout from diagnostic_interface.dialog import SettingsDialog from diagnostic_interface import settings +from diagnostic_interface.tabs import PlotTab, UpdatableTab +import pathlib + +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QPushButton, QTabWidget +from data_tools import SunbeamClient +from diagnostic_interface.widgets import DataSelect, SplashOverlay # Interface aesthetic parameters WINDOW_TITLE = "Diagnostic Interface" -X_COORD = 100 # Sets the x-coord where the interface will be created -Y_COORD = 100 # Sets the y-coord where the interface will be created -WIDTH = 800 # Sizing of window -HEIGHT = 600 # Size of window class MainWindow(QMainWindow): - def __init__(self): super().__init__() self.setWindowTitle(WINDOW_TITLE) - self.setGeometry(X_COORD, Y_COORD, WIDTH, HEIGHT) + + from diagnostic_interface.tabs import SunbeamTab, SunlinkTab, TelemetryTab, SOCTab, PowerTab, WeatherTab, SpeedTab, ArrayTab + self.tabs = QTabWidget() self.tabs.currentChanged.connect(self.on_tab_changed) self.setCentralWidget(self.tabs) home_widget = QWidget() + layout = QVBoxLayout() self.client = SunbeamClient(settings.sunbeam_api_url) @@ -63,9 +52,6 @@ def __init__(self): # Button to load the plot submit_button = QPushButton("Load Data") submit_button.clicked.connect(self.create_plot_tab) - - - layout.addWidget(submit_button) #Settings button @@ -76,44 +62,53 @@ def __init__(self): home_widget.setLayout(layout) self.tabs.addTab(home_widget, "Home") - # self.sunbeam_gui = SunbeamTab() - # self.tabs.addTab(self.sunbeam_gui, "Sunbeam") - # - # self.sunlink_gui = SunlinkTab() - # self.tabs.addTab(self.sunlink_gui, "Sunlink") - # - # self.telemetry_tab = TelemetryTab() - # self.tabs.addTab(self.telemetry_tab, "Telemetry") + self.sunbeam_gui = SunbeamTab() + self.tabs.addTab(self.sunbeam_gui, "Sunbeam") + + self.sunlink_gui = SunlinkTab() + self.tabs.addTab(self.sunlink_gui, "Sunlink") + + self.telemetry_tab = TelemetryTab() + self.tabs.addTab(self.telemetry_tab, "Telemetry") + + self.soc_tab = SOCTab() + self.tabs.addTab(self.soc_tab, "SOC") + + self.power_tab = PowerTab() + self.tabs.addTab(self.power_tab, "Power") + self.weather_tab = WeatherTab() + self.tabs.addTab(self.weather_tab, "Weather") + + self.speed_tab = SpeedTab() + self.tabs.addTab(self.speed_tab, "Speed") + + self.array_tab = ArrayTab() + self.tabs.addTab(self.array_tab, "Array") + + pix = QPixmap("Solar_Sun.png").scaled(200, 200, Qt.KeepAspectRatio) + self._splash = SplashOverlay(self, pix, interval=20) + + def finishSplash(self): + self._splash.hide() def create_plot_tab(self): """Creates a PlotTab object. This object contains plots and the toolbar to interact with them. This method contains a connection to the request_close method of the PlotTab class to receive the signal to close a tab.""" - plot_tab2 = PlotTab2() - self.tabs.addTab(plot_tab2, f"packpower_and_motorpower") - # Getting the values that we will query. origin: str = self.data_select_form.selected_origin source: str = self.data_select_form.selected_source event: str = self.data_select_form.selected_event data_name: str = self.data_select_form.selected_data - - # Creating PlotTab object and adding it to the list of tabs. - - plot_tab = PlotTab(origin, source, event, data_name) self.tabs.addTab(plot_tab, f"{data_name}") - plot_tab.close_requested.connect(self.close_tab) - - - def close_tab(self, widget) -> None: """ Closes the current tab. @@ -133,22 +128,35 @@ def edit_settings(self): current_client_address = settings.sunbeam_api_url current_sunbeam_path = settings.sunbeam_path current_sunlink_path = settings.sunlink_path + current_realtime_event = settings.realtime_event + current_realtime_pipeline = settings.realtime_pipeline dialog = SettingsDialog( current_interval, current_client_address, current_sunbeam_path, current_sunlink_path, + current_realtime_event, + current_realtime_pipeline, self ) if dialog.exec_(): # if user pressed OK - new_plot_interval, new_client_address, sunbeam_path, sunlink_path = dialog.get_settings() + ( + new_plot_interval, + new_client_address, + sunbeam_path, + sunlink_path, + realtime_event, + realtime_pipeline + ) = dialog.get_settings() settings.plot_timer_interval = new_plot_interval settings.sunbeam_api_url = new_client_address settings.sunbeam_path = sunbeam_path settings.sunlink_path = sunlink_path + settings.realtime_event = realtime_event + settings.realtime_pipeline = realtime_pipeline # Refresh settings self.client = SunbeamClient(settings.sunbeam_api_url) @@ -159,4 +167,3 @@ def on_tab_changed(self, index: int): widget = self.tabs.widget(i) if isinstance(widget, UpdatableTab): widget.set_tab_active(i == index) - diff --git a/diagnostic_interface/settings.toml b/diagnostic_interface/settings.toml index f6f44e0d..1dd576c4 100644 --- a/diagnostic_interface/settings.toml +++ b/diagnostic_interface/settings.toml @@ -1,4 +1,6 @@ -plot_timer_interval = 1 -sunbeam_api_url = "api.sunbeam.ubcsolar.com" +plot_timer_interval = 10 +sunbeam_api_url = "localhost:8080" sunbeam_path = "/Users/joshuariefman/Solar/sunbeam" sunlink_path = "/Users/joshuariefman/Solar/sunlink" +realtime_event = "June_8th_Testing" +realtime_pipeline = "track_test" diff --git a/diagnostic_interface/start_application.py b/diagnostic_interface/start_application.py index e1a641b9..d8ed256b 100644 --- a/diagnostic_interface/start_application.py +++ b/diagnostic_interface/start_application.py @@ -1,17 +1,25 @@ import sys import os sys.path.insert(0, os.getcwd()) + from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QTimer from main_window import MainWindow -import qdarktheme +from qt_material import apply_stylesheet if __name__ == "__main__": app = QApplication(sys.argv) # PyQt Theme - app.setStyleSheet(qdarktheme.load_stylesheet("light")) + apply_stylesheet(app, theme='light_blue.xml', invert_secondary=True) window = MainWindow() + window.showFullScreen() window.show() + + window._splash.showMessage("Loading...") + QTimer.singleShot(1500, lambda: window._splash.showMessage("Loading...")) + QTimer.singleShot(2000, window.finishSplash) + sys.exit(app.exec_()) diff --git a/diagnostic_interface/tabs/__init__.py b/diagnostic_interface/tabs/__init__.py index fd2171c1..c864032c 100644 --- a/diagnostic_interface/tabs/__init__.py +++ b/diagnostic_interface/tabs/__init__.py @@ -1,17 +1,30 @@ # from .docker_panel import DockerStackTab -#from .plot_tab import PlotTab2 +#from diagnostic_interface import PlotTab + +#from .plot_tab import WeatherTab +#from .weather_tab import WeatherTab from .plot_tab import PlotTab +from .docker_panel import DockerStackTab +from .weather_tab import WeatherTab from ._updatable import UpdatableTab -# from .sunbeam_panel import SunbeamTab -# from .sunlink_panel import SunlinkTab -# from .telemetry_panel import TelemetryTab +from .power_panel import PowerTab +from .sunbeam_panel import SunbeamTab +from .sunlink_panel import SunlinkTab +from .telemetry_panel import TelemetryTab +from .soc_tab import SOCTab +from .speed_tab import SpeedTab +from .array_tab import ArrayTab __all__ = [ - # "DockerStackTab", + "DockerStackTab", "PlotTab", - #"PlotTab2", + "WeatherTab", "UpdatableTab", - # "SunbeamTab", - # "SunlinkTab", - # "TelemetryTab" + "SunbeamTab", + "SunlinkTab", + "TelemetryTab", + "PowerTab", + "SOCTab", + "SpeedTab", + "ArrayTab" ] diff --git a/diagnostic_interface/tabs/docker_panel.py b/diagnostic_interface/tabs/docker_panel.py index 73d29ed2..577f253b 100644 --- a/diagnostic_interface/tabs/docker_panel.py +++ b/diagnostic_interface/tabs/docker_panel.py @@ -1,5 +1,6 @@ import threading import json + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel from PyQt5.QtCore import QTimer, Qt, QRunnable, QObject, pyqtSignal, QThreadPool from diagnostic_interface.widgets import DockerLogWidget @@ -17,21 +18,23 @@ class UpdateStatusWorkerSignals(QObject): class UpdateStatusWorker(QRunnable): - def __init__(self, docker_stack: "DockerStackTab"): + def __init__(self, docker_stack: "DockerStackTab", cmd: list[str]): super().__init__() self.docker_stack = docker_stack + self.cmd = cmd self.signals = UpdateStatusWorkerSignals() def run(self): try: result = subprocess.run( - ["docker", "compose", "ps", "--format", "json"], + self.cmd, # ["docker", "compose", "ps", "--format", "json"] cwd=self.docker_stack.project_directory, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=2, check=True, + shell=True, ) output = result.stdout.strip() @@ -40,7 +43,7 @@ def run(self): else: services = [json.loads(line) for line in output.splitlines() if line.strip()] - except (subprocess.TimeoutExpired, subprocess.SubprocessError): + except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError): QTimer.singleShot(0, self.docker_stack.stop_timer) QTimer.singleShot(0, self.docker_stack.raise_docker_error) services = [] @@ -64,14 +67,14 @@ def _init_ui(self): self.toggle_button = QPushButton("Start Stack") self.toggle_button.clicked.connect(self.toggle_stack) - self.log_output = DockerLogWidget() + self.log_output = DockerLogWidget(self.start_stack_command) self.log_output.setReadOnly(True) - layout = QVBoxLayout() - layout.addWidget(self.status_label) - layout.addWidget(self.toggle_button) - layout.addWidget(self.log_output) - self.setLayout(layout) + self.layout = QVBoxLayout() + self.layout.addWidget(self.status_label) + self.layout.addWidget(self.toggle_button) + self.layout.addWidget(self.log_output) + self.setLayout(self.layout) @property @abstractmethod @@ -93,33 +96,54 @@ def toggle_stack(self): else: self.start_stack() + @staticmethod + @abstractmethod + def start_stack_command(): + raise NotImplementedError + + @staticmethod + @abstractmethod + def stop_stack_command(): + raise NotImplementedError + def start_stack(self): def run(): + cmd = self.start_stack_command() subprocess.run( - ["docker", "compose", "up", "-d"], + cmd, cwd=self.project_directory, + shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True, ) QTimer.singleShot(0, self.do_update_status) - + print(self.start_stack_command()) threading.Thread(target=run, daemon=True).start() def stop_stack(self): def run(): + cmd = self.stop_stack_command() subprocess.run( - ["docker", "compose", "down"], + cmd, cwd=self.project_directory, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + shell=True ) QTimer.singleShot(0, self.do_update_status) threading.Thread(target=run, daemon=True).start() + def get_docker_cmd(self): + cmd: str = self.start_stack_command() + + docker_cmd = cmd.replace("up -d", "ps --format json") + + return docker_cmd + def do_update_status(self): - worker = UpdateStatusWorker(self) + worker = UpdateStatusWorker(self, self.get_docker_cmd()) worker.signals.services.connect(self.update_status) self._thread_pool.start(worker) @@ -137,7 +161,7 @@ def update_status(self, services: list): self.evaluate_readiness(num_running_containers) self.toggle_button.setText("Stop Stack") - self.log_output.fetch_ansi_logs(self.project_directory) + self.log_output.fetch_logs(self.project_directory) @abstractmethod def evaluate_readiness(self, num_running_containers: int): diff --git a/diagnostic_interface/tabs/plot_tab.py b/diagnostic_interface/tabs/plot_tab.py index ce7bf999..b6dc105c 100644 --- a/diagnostic_interface/tabs/plot_tab.py +++ b/diagnostic_interface/tabs/plot_tab.py @@ -2,12 +2,11 @@ QWidget, QVBoxLayout, QPushButton, QMessageBox, QGroupBox, QHBoxLayout ) -from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer -#from poetry.console.commands import self +from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer, pyqtSlot from diagnostic_interface import settings -from diagnostic_interface.canvas import (CustomNavigationToolbar, PlotCanvas) -#PlotCanvas2) +from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas +from data_tools.collections import TimeSeries HELP_MESSAGES = { @@ -17,14 +16,16 @@ "- Data is sourced from the car's telemetry system.\n", } +EVENT = "FSGP_2024_Day_1" + class PlotRefreshWorkerSignals(QObject): - finished = pyqtSignal(bool) # success or failure + data_ready = pyqtSignal(object) # emits the TimeSeries + error = pyqtSignal(str) # emits an error message class PlotRefreshWorker(QRunnable): - def __init__(self, plot_canvas, origin, source, event, data_name): - #def __init__(self, plot_canvas): + def __init__(self, plot_canvas: PlotCanvas, origin, source, event, data_name): super().__init__() self.plot_canvas = plot_canvas self.origin = origin @@ -34,8 +35,12 @@ def __init__(self, plot_canvas, origin, source, event, data_name): self.signals = PlotRefreshWorkerSignals() def run(self): - success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) - self.signals.finished.emit(success) + try: + data = self.plot_canvas.fetch_data(self.origin, self.event, self.source, self.data_name) + self.signals.data_ready.emit(data) + except Exception as e: + self.signals.error.emit(str(e)) + class PlotTab(QWidget): close_requested = pyqtSignal(QWidget) @@ -87,8 +92,7 @@ def __init__(self, origin: str, source: str, event: str, data_name: str, parent= self.refresh_timer = QTimer() self.refresh_timer.timeout.connect(self.refresh_plot) - if not self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name): - self.request_close() + QTimer.singleShot(0, self.refresh_plot) def set_tab_active(self, active: bool) -> None: if active: @@ -107,12 +111,18 @@ def refresh_plot(self): self.event, self.data_name ) - worker.signals.finished.connect(self._on_plot_refresh_finished) + worker.signals.data_ready.connect(self._on_data_ready) + worker.signals.error.connect(self._on_data_error) self._thread_pool.start(worker) - def _on_plot_refresh_finished(self, success: bool): - if not success: - self.request_close() + @pyqtSlot(object) + def _on_data_ready(self, data: TimeSeries): + self.plot_canvas.plot(data, f"{self.data_name}", data.units) + + @pyqtSlot(str) + def _on_data_error(self, msg): + QMessageBox.critical(self, "Plot Error", msg) + self.request_close() def request_close(self): self.close_requested.emit(self) @@ -120,125 +130,3 @@ def request_close(self): def show_help_message(self, data_name, event): message = HELP_MESSAGES.get(data_name, "No specific help available for this plot.") QMessageBox.information(self, f"Help: {data_name}", message) - - - - -class PlotTab2(QWidget): - close_requested = pyqtSignal(QWidget) - - #def __init__(self, origin : str, source: str, event: str, data_name1: str, data_name2: str, parent=None): - def __init__(self, origin = "production", source = "power", event = "FSGP_2024_Day_1", data_name1 = "PackPower", data_name2 = "MotorPower", parent = None): - super().__init__(parent) - # - # self.origin = origin - # self.source = source - # self.event = event - # self.data_name1 = data_name1 - # self.data_name2 = data_name2 - - - self._thread_pool = QThreadPool() - - # Layout setup - self.layout = QVBoxLayout(self) - self.layout.setSpacing(10) - self.layout.setContentsMargins(30, 30, 30, 30) - - self.plot_canvas1 = PlotCanvas(self) - self.plot_canvas2 = PlotCanvas(self) - - self.toolbar1 = CustomNavigationToolbar(canvas=self.plot_canvas1) - self.toolbar2 = CustomNavigationToolbar(canvas=self.plot_canvas2) - # Buttons - help_button = QPushButton("Help") - help_button.setObjectName("helpButton") - help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.event)) - - close_button = QPushButton("Close Tab") - close_button.setObjectName("closeButton") - close_button.clicked.connect(self.request_close) - - button_group = QGroupBox("Actions") - button_layout = QHBoxLayout() - button_layout.addWidget(help_button) - button_layout.addWidget(close_button) - button_group.setLayout(button_layout) - - self.layout.addWidget(self.toolbar1) - self.layout.addWidget(self.plot_canvas1) - self.layout.addWidget(self.toolbar2) - self.layout.addWidget(self.plot_canvas2) - - self.layout.addWidget(button_group) - - self.setStyleSheet(""" - QPushButton#helpButton, QPushButton#closeButton { - padding: 6px 12px; - border-radius: 8px; - } - """) - - self.refresh_timer = QTimer() - self.refresh_timer.timeout.connect(self.refresh_plot) - -#stuff added: - - - #plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) - plot1 = self.plot_canvas1.query_and_plot("production", "power", "FSGP_2024_Day_1", "MotorPower") - - plot2 = self.plot_canvas2.query_and_plot("production", "power","FSGP_2024_Day_1", "PackPower") - - # if not self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name): - # self.request_close() - - if not (plot1 and plot2): - self.request_close() - - def set_tab_active(self, active: bool) -> None: - if active: - self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) - self.refresh_timer.start() - QTimer.singleShot(0, self.refresh_plot) - - else: - self.refresh_timer.stop() - - def refresh_plot(self): - - worker1 = PlotRefreshWorker(self.plot_canvas1, "production", "power", "FSGP_2024_Day_1", "MotorPower") - - worker1.signals.finished.connect(self._on_plot_refresh_finished) - self._thread_pool.start(worker1) - - - worker2 = PlotRefreshWorker(self.plot_canvas2, "production", "power", "FSGP_2024_Day_1", "MotorPower") - - worker2.signals.finished.connect(self._on_plot_refresh_finished) - self._thread_pool.start(worker2) - - def _on_plot_refresh_finished(self, success: bool): - if not success: - self.request_close() - - def request_close(self): - self.close_requested.emit(self) - - def show_help_message(self, data_name1): - message1 = HELP_MESSAGES.get(data_name1, "No specific help available for this plot.") - QMessageBox.information(self, f"Help: {data_name1}", message1) - - - - - - - - - - - - - - diff --git a/diagnostic_interface/tabs/sunbeam_panel.py b/diagnostic_interface/tabs/sunbeam_panel.py index bb0f09f9..167c4172 100644 --- a/diagnostic_interface/tabs/sunbeam_panel.py +++ b/diagnostic_interface/tabs/sunbeam_panel.py @@ -1,29 +1,55 @@ -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QMessageBox, QPushButton, QDialog from diagnostic_interface import settings from diagnostic_interface.tabs import DockerStackTab +from diagnostic_interface.dialog import TextEditDialog from data_tools.query import SunbeamClient from abc import abstractmethod +from diagnostic_interface.config import command_settings class SunbeamTab(DockerStackTab): def __init__(self): super().__init__() + edit_btn = QPushButton("Edit Commands", self) + edit_btn.clicked.connect(self.open_edit_dialog) + + # self.layout.addWidget(self.label) + self.layout.addWidget(edit_btn) + + @staticmethod + def open_edit_dialog(): + dlg = TextEditDialog( + [command_settings.sunbeam_up_cmd, command_settings.sunbeam_down_cmd], + ["Sunbeam Up", "Sunbeam Down"] + ) + + if dlg.exec_() == QDialog.Accepted: + command_settings.sunbeam_up_cmd, command_settings.sunbeam_down_cmd = dlg.getText() + + @staticmethod + def start_stack_command(): + return command_settings.sunbeam_up_cmd + + @staticmethod + def stop_stack_command(): + return command_settings.sunbeam_down_cmd + @property def project_directory(self): return settings.sunbeam_path def raise_docker_error(self): - QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunbeam Docker service " - f"stack.\n Is Docker running and is Sunbeam path correct? \n") + QMessageBox.critical(None, "Docker Error", "Did not get a response from the Sunbeam Docker service " + "stack.\n Is Docker running and is Sunbeam path correct? \n") @abstractmethod def evaluate_readiness(self, num_running_containers): client = SunbeamClient(settings.sunbeam_api_url) - if num_running_containers == 6 and client.is_alive(): + if num_running_containers == 5 and client.is_alive(): self.status_label.setText("✅ Status: 100%") self.stack_running = True else: - self.status_label.setText(f"❌ Status: {int(num_running_containers/7. * 100)}%") + self.status_label.setText(f"❌ Status: {int(num_running_containers/6. * 100)}%") self.stack_running = True diff --git a/diagnostic_interface/tabs/sunlink_panel.py b/diagnostic_interface/tabs/sunlink_panel.py index aa2504d8..0f44ef8e 100644 --- a/diagnostic_interface/tabs/sunlink_panel.py +++ b/diagnostic_interface/tabs/sunlink_panel.py @@ -1,20 +1,45 @@ -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QMessageBox, QPushButton, QDialog from diagnostic_interface import settings from diagnostic_interface.tabs import DockerStackTab from abc import abstractmethod +from diagnostic_interface.dialog import TextEditDialog +from diagnostic_interface.config import command_settings class SunlinkTab(DockerStackTab): def __init__(self): super().__init__() + edit_btn = QPushButton("Edit Commands", self) + edit_btn.clicked.connect(self.open_edit_dialog) + + self.layout.addWidget(edit_btn) + + @staticmethod + def open_edit_dialog(): + dlg = TextEditDialog( + [command_settings.sunlink_up_cmd, command_settings.sunlink_down_cmd], + ["Sunlink Up", "Sunlink Down"] + ) + + if dlg.exec_() == QDialog.Accepted: + command_settings.sunlink_up_cmd, command_settings.sunlink_down_cmd = dlg.getText() + + @staticmethod + def start_stack_command(): + return command_settings.sunlink_up_cmd + + @staticmethod + def stop_stack_command(): + return command_settings.sunlink_down_cmd + @property def project_directory(self): return settings.sunlink_path def raise_docker_error(self): - QMessageBox.critical(None, "Docker Error", f"Did not get a response from the Sunlink Docker service " - f"stack.\n Is Docker running and is Sunlink path correct? \n") + QMessageBox.critical(None, "Docker Error", "Did not get a response from the Sunlink Docker service " + "stack.\n Is Docker running and is Sunlink path correct? \n") @abstractmethod def evaluate_readiness(self, num_running_containers: int): @@ -23,5 +48,5 @@ def evaluate_readiness(self, num_running_containers: int): self.stack_running = True else: - self.status_label.setText(f"❌ Status: {int(num_running_containers/4. * 100)}%") + self.status_label.setText(f"❌ Status: {int(num_running_containers / 4. * 100)}%") self.stack_running = True diff --git a/diagnostic_interface/tabs/telemetry_panel.py b/diagnostic_interface/tabs/telemetry_panel.py index a6bc3e08..ef8d8698 100644 --- a/diagnostic_interface/tabs/telemetry_panel.py +++ b/diagnostic_interface/tabs/telemetry_panel.py @@ -1,7 +1,8 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QDialog from PyQt5.QtCore import Qt from diagnostic_interface.widgets import CommandOutputWidget -from diagnostic_interface import settings +from diagnostic_interface.dialog import TextEditDialog +from diagnostic_interface.config import command_settings class TelemetryTab(QWidget): @@ -25,8 +26,24 @@ def _init_ui(self): layout.addWidget(self.toggle_button) layout.addWidget(self.log_output) + edit_btn = QPushButton("Edit Commands", self) + edit_btn.clicked.connect(self.open_edit_dialog) + + layout.addWidget(edit_btn) + self.setLayout(layout) + @staticmethod + def start_stack_command() -> str: + return command_settings.sunlink_up_cmd + + @staticmethod + def open_edit_dialog(): + dlg = TextEditDialog([command_settings.telemetry_enable_cmd], ["Telemetry Link"]) + + if dlg.exec_() == QDialog.Accepted: + command_settings.telemetry_enable_cmd, = dlg.getText() + def toggle_stack(self): if self.running: self.stop_stack() @@ -37,11 +54,9 @@ def start_stack(self): self.running = True self.status_label.setText("✅ Status: Running") - self.log_output.start_in_venv( - project_root=settings.sunlink_path, - script_path="./link_telemetry.py", - args=["-r", "can", "--debug"] - ) + cmd = command_settings.telemetry_enable_cmd + + self.log_output.start_cmd(cmd=cmd) self.toggle_button.setText("Stop Telemetry") @@ -50,3 +65,9 @@ def stop_stack(self): self.status_label.setText("❌ Status: Stopped") self.log_output.stop() self.toggle_button.setText("Start Telemetry") + + def set_tab_active(self, active: bool): + if active: + self.log_output.activate() + else: + self.log_output.deactivate() diff --git a/diagnostic_interface/widgets/__init__.py b/diagnostic_interface/widgets/__init__.py index 13f8e0c6..dadaecc1 100644 --- a/diagnostic_interface/widgets/__init__.py +++ b/diagnostic_interface/widgets/__init__.py @@ -1,11 +1,20 @@ from .data_select import DataSelect from .timer_widget import TimedWidget -# from .docker_log_widget import DockerLogWidget -from .comnand_output import CommandOutputWidget +from .docker_log_widget import DockerLogWidget +from .command_output import CommandOutputWidget +from .map_widget import FoliumMapWidget +from .realtime_map_widget import RealtimeMapWidget +from .timed_map_plot import TimedMapPlot +from .splash_overlay import SplashOverlay __all__ = [ "DataSelect", "TimedWidget", - # "DockerLogWidget", - "CommandOutputWidget" + "DockerLogWidget", + "CommandOutputWidget", + "FoliumMapWidget", + "CommandOutputWidget", + "RealtimeMapWidget", + "TimedMapPlot", + "SplashOverlay" ] diff --git a/diagnostic_interface/widgets/data_select.py b/diagnostic_interface/widgets/data_select.py index e4d25020..7480e294 100644 --- a/diagnostic_interface/widgets/data_select.py +++ b/diagnostic_interface/widgets/data_select.py @@ -1,5 +1,5 @@ -from PyQt5.QtWidgets import QComboBox, QFormLayout, QMessageBox +from PyQt5.QtWidgets import QComboBox, QFormLayout from data_tools import SunbeamClient from diagnostic_interface import settings from requests import exceptions as requests_exceptions @@ -132,8 +132,8 @@ def filter_data(self) -> tuple[list, list, list, list]: return available_origins, available_sources, available_events, available_data - except requests_exceptions.Timeout as e: - QMessageBox.critical(None, "Plotting Error", f"Error fetching Sunbeam files:\n{str(e)}") + except requests_exceptions.Timeout: + # QMessageBox.critical(None, "Plotting Error", f"Error fetching Sunbeam files:\n{str(e)}") return [], [], [], [] @staticmethod diff --git a/diagnostic_interface/widgets/docker_log_widget.py b/diagnostic_interface/widgets/docker_log_widget.py index e9dada72..6b7683d3 100644 --- a/diagnostic_interface/widgets/docker_log_widget.py +++ b/diagnostic_interface/widgets/docker_log_widget.py @@ -1,79 +1,166 @@ +import subprocess +from collections.abc import Callable from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTextEdit import datetime import os -import pty -import subprocess -from PyQt5.QtWidgets import QTextEdit -from ansi2html import Ansi2HTMLConverter -RESET = "\x1b[0m" +class DockerLogWidget(QTextEdit): + """ + A QTextEdit that synchronously fetches Docker logs in batches (via subprocess.run), + prunes old lines, and handles carriage-return “spinner” overwrites. + - `start_stack_command: Callable[[], str]` should return a single, fully-formed + command string (e.g. "docker-compose -f docker-compose.yml up -d"). We will + replace "up -d" with "logs --since Z" each time we fetch. + - `max_lines` is the total number of text lines to keep before pruning older lines. -class DockerLogWidget(QTextEdit): - def __init__(self): + NOTE: Because we use `subprocess.run(..., shell=True)` directly, **fetch_logs() will + block the GUI thread until the external command completes**. If you need non-blocking + behavior, move `fetch_logs()` into a worker thread. + """ + + def __init__(self, start_stack_command: Callable[[], str], max_lines: int = 1000): super().__init__() self.setReadOnly(True) + # We only insert plain text, so rich text is disabled. + self.setAcceptRichText(False) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setAcceptRichText(True) - self.converter = Ansi2HTMLConverter(dark_bg=False, inline=True) - - self._last_log_time = datetime.datetime.now(datetime.UTC) - - def fetch_ansi_logs(self, project_dir: str): - master_fd, slave_fd = pty.openpty() - since_str = self._last_log_time.isoformat() + "Z" - - proc = subprocess.Popen( - ["docker", "compose", "logs", "--since", since_str], - cwd=project_dir, - stdout=slave_fd, - stderr=subprocess.DEVNULL, - env={**os.environ, "FORCE_COLOR": "1"}, - ) + + self.max_lines = max_lines + self._last_log_time = datetime.datetime.utcnow() + self._start_stack_command = start_stack_command + + def _build_logs_command_str(self, since_iso: str) -> str: + """ + 1) Call start_stack_command() to get the base `up -d` invocation (single string). + 2) Replace "up -d" with "logs --since Z". + e.g. base = "docker-compose -f docker-compose.yml up -d" + since_iso = "2025-05-30T16:00:00", we return + "docker-compose -f docker-compose.yml logs --since 2025-05-30T16:00:00Z" + """ + base_cmd = self._start_stack_command() + if not isinstance(base_cmd, str) or not base_cmd.strip(): + raise ValueError("start_stack_command() must return a non-empty command string.") + return base_cmd.replace("up -d", f"logs --since {since_iso}Z") + + def fetch_logs(self, project_dir: str): + """ + Fetch new logs since the last timestamp. This method *blocks* until + the `docker logs --since ` process exits, then updates the widget. + """ + # 1) Compute ISO‐formatted "since" timestamp (UTC) + since_iso = self._last_log_time.replace(microsecond=0).isoformat() self._last_log_time = datetime.datetime.utcnow() - os.close(slave_fd) - - output = b"" - while True: - try: - chunk = os.read(master_fd, 1024) - if not chunk: - break - output += chunk - except OSError: - break - - os.close(master_fd) - ansi_text = output.decode(errors="replace") - - lines = ansi_text.splitlines() - for line in lines: - if '\r' in line: - line = line.split('\r')[-1] + # 2) Build the Docker logs command string + docker_cmd_str = self._build_logs_command_str(since_iso) + if not docker_cmd_str.strip(): + return # nothing to do if the string is empty + + # 3) Ensure no ANSI colors are emitted + env = os.environ.copy() + env["FORCE_COLOR"] = "0" + + # 4) Decide which shell wrapper to use + if os.name == "nt": + # On Windows: wrap in cmd /C "<...>" + shell_cmd = f"{docker_cmd_str}" + else: + # On Unix/macOS: wrap in bash -lc "<...>" + # We do NOT shlex.quote(docker_cmd_str) here, because we want the entire + # string passed verbatim within the quotes. + shell_cmd = f'bash -lc "{docker_cmd_str}"' + + # 5) Run synchronously (this will block until completion) + try: + completed = subprocess.run( + shell_cmd, + shell=True, + cwd=project_dir, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + errors="replace", + ) + except Exception as e: + # If the call fails entirely (e.g. command not found), append the error and return. + self._append_text(f"[Error running logs command]: {e}\n") + return + + # 6) Process stdout lines + stdout_text = completed.stdout or "" + lines = stdout_text.splitlines() + + # 7) Insert lines into the QTextEdit (handling '\r' overwrites) + cursor = self.textCursor() + cursor.beginEditBlock() + self.setUpdatesEnabled(False) + + for raw_line in lines: + # If carriage-return is present, remove everything up to the last '\r', + # then delete the previous line in the widget before inserting the new text. + if "\r" in raw_line: + new_line = raw_line.rsplit("\r", 1)[-1] self._remove_last_line() - html = self.converter.convert(RESET + line + RESET, full=False) - self.insertHtml(html + "
") + else: + new_line = raw_line + + cursor.insertText(new_line + "\n") + + cursor.endEditBlock() + self.setUpdatesEnabled(True) + + # 8) Prune old lines if we've exceeded max_lines + self._prune_old_lines() + # 9) Scroll to bottom so the newest logs are visible self._scroll_to_bottom() + def _prune_old_lines(self): + """Delete the oldest blocks if blockCount() exceeds max_lines.""" + doc = self.document() + overflow = doc.blockCount() - self.max_lines + if overflow > 0: + block_to_keep = doc.findBlockByNumber(overflow) + cur = self.textCursor() + cur.setPosition(0) + cur.setPosition(block_to_keep.position(), cur.KeepAnchor) + cur.removeSelectedText() + def _remove_last_line(self): + """ + Delete the last QTextBlock (line) in the widget. Used for spinner overwrites. + """ cursor = self.textCursor() cursor.movePosition(cursor.End) cursor.select(cursor.BlockUnderCursor) cursor.removeSelectedText() + # After removing text of that block, remove the trailing newline (if any) cursor.deletePreviousChar() def _scroll_to_bottom(self): - scrollbar = self.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) + sb = self.verticalScrollBar() + sb.setValue(sb.maximum()) + # Prevent the user from scrolling with wheel or arrow keys: def wheelEvent(self, event): pass def keyPressEvent(self, event): - # Disable arrow key scrolling if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return super().keyPressEvent(event) + + def _append_text(self, text: str): + """ + Helper to insert arbitrary text (e.g. error messages) at the bottom, + then prune & scroll appropriately. + """ + cursor = self.textCursor() + cursor.movePosition(cursor.End) + cursor.insertText(text) + self._prune_old_lines() + self._scroll_to_bottom() From a208241a0d28c00b4c4a3596c5d0710ab81a478b Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Tue, 30 Sep 2025 19:35:53 -0700 Subject: [PATCH 31/44] Undo changes to pyproject and poetry --- poetry.lock | 1222 +++++++++++++++++++++--------------------------- pyproject.toml | 68 ++- 2 files changed, 589 insertions(+), 701 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4301a62b..a228f822 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -11,35 +11,17 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "ansi2html" -version = "1.9.2" -description = "Convert text with ANSI color codes to HTML or to LaTeX" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5"}, - {file = "ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5"}, -] - -[package.extras] -docs = ["mkdocs", "mkdocs-material", "mkdocs-material-extensions", "mkdocstrings", "mkdocstrings-python", "pymdown-extensions"] -test = ["pytest", "pytest-cov"] - [[package]] name = "anytree" -version = "2.12.1" +version = "2.13.0" description = "Powerful and Lightweight Python Tree Data Structure with various plugins" optional = false -python-versions = ">=3.7.2,<4" +python-versions = "<4.0,>=3.9.2" files = [ - {file = "anytree-2.12.1-py3-none-any.whl", hash = "sha256:5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0"}, - {file = "anytree-2.12.1.tar.gz", hash = "sha256:244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830"}, + {file = "anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff"}, + {file = "anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714"}, ] -[package.dependencies] -six = "*" - [[package]] name = "backports-tarfile" version = "1.2.0" @@ -74,24 +56,25 @@ scipy = ">=1.0.0,<2.0.0" [[package]] name = "bokeh" -version = "3.6.3" +version = "3.7.2" description = "Interactive plots and applications in the browser from Python" optional = false python-versions = ">=3.10" files = [ - {file = "bokeh-3.6.3-py3-none-any.whl", hash = "sha256:1c219e2afe1405e6ada212071ac3bee91c95acfd1aa6d620eb6f61a751407747"}, - {file = "bokeh-3.6.3.tar.gz", hash = "sha256:9b81d6a9ea62e75a04a1a9d9f931942016890beec9ab5d129a2a4432cf595c0a"}, + {file = "bokeh-3.7.2-py3-none-any.whl", hash = "sha256:efd9172a90cc233c1c21ef4813d58a8a6f97ee63c8e2f1b4f2389a64fcef0722"}, + {file = "bokeh-3.7.2.tar.gz", hash = "sha256:80c21885cec276431acd4db92f831c71eb999ea995470ce777e0c577b0cfc1d8"}, ] [package.dependencies] contourpy = ">=1.2" Jinja2 = ">=2.9" +narwhals = ">=1.13" numpy = ">=1.16" packaging = ">=16.8" pandas = ">=1.2" pillow = ">=7.1.0" PyYAML = ">=3.10" -tornado = ">=6.2" +tornado = {version = ">=6.2", markers = "sys_platform != \"emscripten\""} xyzservices = ">=2021.09.1" [[package]] @@ -332,65 +315,68 @@ files = [ [[package]] name = "contourpy" -version = "1.3.1" +version = "1.3.2" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.10" files = [ - {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, - {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, - {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, - {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, - {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, - {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, - {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, - {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, - {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, - {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, - {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, - {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, - {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, - {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, - {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, - {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, - {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, - {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, - {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, - {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, - {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, - {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, - {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, - {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, - {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, - {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, - {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, - {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, - {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, - {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, - {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, + {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, + {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2"}, + {file = "contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0"}, + {file = "contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd"}, + {file = "contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f"}, + {file = "contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e"}, + {file = "contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912"}, + {file = "contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef"}, + {file = "contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f"}, + {file = "contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd"}, + {file = "contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1"}, + {file = "contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5"}, + {file = "contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54"}, ] [package.dependencies] @@ -399,52 +385,54 @@ numpy = ">=1.23" [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] [[package]] name = "cryptography" -version = "44.0.2" +version = "44.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" files = [ - {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, - {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, - {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, - {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, - {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, - {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, - {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, - {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, - {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, - {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, - {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, + {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, + {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, + {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, + {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, + {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, + {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, + {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, ] [package.dependencies] @@ -457,7 +445,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -553,61 +541,61 @@ testing = ["pytest"] [[package]] name = "fonttools" -version = "4.56.0" +version = "4.57.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, - {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, - {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311"}, - {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc"}, - {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f"}, - {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086"}, - {file = "fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786"}, - {file = "fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685"}, - {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df"}, - {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"}, - {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c"}, - {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049"}, - {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62"}, - {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0"}, - {file = "fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b"}, - {file = "fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05"}, - {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9"}, - {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f"}, - {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2"}, - {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563"}, - {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a"}, - {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28"}, - {file = "fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c"}, - {file = "fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba"}, - {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692"}, - {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0"}, - {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1"}, - {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea"}, - {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3"}, - {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278"}, - {file = "fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f"}, - {file = "fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6"}, - {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71"}, - {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2"}, - {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771"}, - {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc"}, - {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba"}, - {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4"}, - {file = "fonttools-4.56.0-cp38-cp38-win32.whl", hash = "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35"}, - {file = "fonttools-4.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31"}, - {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5"}, - {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c"}, - {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10"}, - {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76"}, - {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29"}, - {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8"}, - {file = "fonttools-4.56.0-cp39-cp39-win32.whl", hash = "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40"}, - {file = "fonttools-4.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793"}, - {file = "fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14"}, - {file = "fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4"}, + {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41"}, + {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02"}, + {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e"}, + {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab"}, + {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1"}, + {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f"}, + {file = "fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec"}, + {file = "fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db"}, + {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3871349303bdec958360eedb619169a779956503ffb4543bb3e6211e09b647c4"}, + {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c59375e85126b15a90fcba3443eaac58f3073ba091f02410eaa286da9ad80ed8"}, + {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967b65232e104f4b0f6370a62eb33089e00024f2ce143aecbf9755649421c683"}, + {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39acf68abdfc74e19de7485f8f7396fa4d2418efea239b7061d6ed6a2510c746"}, + {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d077f909f2343daf4495ba22bb0e23b62886e8ec7c109ee8234bdbd678cf344"}, + {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46370ac47a1e91895d40e9ad48effbe8e9d9db1a4b80888095bc00e7beaa042f"}, + {file = "fonttools-4.57.0-cp311-cp311-win32.whl", hash = "sha256:ca2aed95855506b7ae94e8f1f6217b7673c929e4f4f1217bcaa236253055cb36"}, + {file = "fonttools-4.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:17168a4670bbe3775f3f3f72d23ee786bd965395381dfbb70111e25e81505b9d"}, + {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31"}, + {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92"}, + {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888"}, + {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6"}, + {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98"}, + {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8"}, + {file = "fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac"}, + {file = "fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9"}, + {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef"}, + {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c"}, + {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72"}, + {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817"}, + {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9"}, + {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13"}, + {file = "fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199"}, + {file = "fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3"}, + {file = "fonttools-4.57.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9d57b4e23ebbe985125d3f0cabbf286efa191ab60bbadb9326091050d88e8213"}, + {file = "fonttools-4.57.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:579ba873d7f2a96f78b2e11028f7472146ae181cae0e4d814a37a09e93d5c5cc"}, + {file = "fonttools-4.57.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3e1ec10c29bae0ea826b61f265ec5c858c5ba2ce2e69a71a62f285cf8e4595"}, + {file = "fonttools-4.57.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1968f2a2003c97c4ce6308dc2498d5fd4364ad309900930aa5a503c9851aec8"}, + {file = "fonttools-4.57.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:aff40f8ac6763d05c2c8f6d240c6dac4bb92640a86d9b0c3f3fff4404f34095c"}, + {file = "fonttools-4.57.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d07f1b64008e39fceae7aa99e38df8385d7d24a474a8c9872645c4397b674481"}, + {file = "fonttools-4.57.0-cp38-cp38-win32.whl", hash = "sha256:51d8482e96b28fb28aa8e50b5706f3cee06de85cbe2dce80dbd1917ae22ec5a6"}, + {file = "fonttools-4.57.0-cp38-cp38-win_amd64.whl", hash = "sha256:03290e818782e7edb159474144fca11e36a8ed6663d1fcbd5268eb550594fd8e"}, + {file = "fonttools-4.57.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7339e6a3283e4b0ade99cade51e97cde3d54cd6d1c3744459e886b66d630c8b3"}, + {file = "fonttools-4.57.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05efceb2cb5f6ec92a4180fcb7a64aa8d3385fd49cfbbe459350229d1974f0b1"}, + {file = "fonttools-4.57.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a97bb05eb24637714a04dee85bdf0ad1941df64fe3b802ee4ac1c284a5f97b7c"}, + {file = "fonttools-4.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541cb48191a19ceb1a2a4b90c1fcebd22a1ff7491010d3cf840dd3a68aebd654"}, + {file = "fonttools-4.57.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cdef9a056c222d0479a1fdb721430f9efd68268014c54e8166133d2643cb05d9"}, + {file = "fonttools-4.57.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3cf97236b192a50a4bf200dc5ba405aa78d4f537a2c6e4c624bb60466d5b03bd"}, + {file = "fonttools-4.57.0-cp39-cp39-win32.whl", hash = "sha256:e952c684274a7714b3160f57ec1d78309f955c6335c04433f07d36c5eb27b1f9"}, + {file = "fonttools-4.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2a722c0e4bfd9966a11ff55c895c817158fcce1b2b6700205a376403b546ad9"}, + {file = "fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f"}, + {file = "fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de"}, ] [package.extras] @@ -624,6 +612,40 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +[[package]] +name = "geographiclib" +version = "2.0" +description = "The geodesic routines from GeographicLib" +optional = false +python-versions = ">=3.7" +files = [ + {file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"}, + {file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"}, +] + +[[package]] +name = "geopy" +version = "2.4.1" +description = "Python Geocoding Toolbox" +optional = false +python-versions = ">=3.7" +files = [ + {file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"}, + {file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"}, +] + +[package.dependencies] +geographiclib = ">=1.52,<3" + +[package.extras] +aiohttp = ["aiohttp"] +dev = ["coverage", "flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"] +dev-docs = ["readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"] +dev-lint = ["flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)"] +dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<=4.3.2)"] +requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"] +timezone = ["pytz"] + [[package]] name = "gitdb" version = "4.0.12" @@ -658,24 +680,24 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", [[package]] name = "google-api-core" -version = "2.24.1" +version = "2.24.2" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, - {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" proto-plus = [ - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, ] -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] @@ -685,31 +707,31 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.163.0" +version = "2.169.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.163.0-py2.py3-none-any.whl", hash = "sha256:080e8bc0669cb4c1fb8efb8da2f5b91a2625d8f0e7796cfad978f33f7016c6c4"}, - {file = "google_api_python_client-2.163.0.tar.gz", hash = "sha256:88dee87553a2d82176e2224648bf89272d536c8f04dcdda37ef0a71473886dd7"}, + {file = "google_api_python_client-2.169.0-py3-none-any.whl", hash = "sha256:dae3e882dc0e6f28e60cf09c1f13fedfd881db84f824dd418aa9e44def2fe00d"}, + {file = "google_api_python_client-2.169.0.tar.gz", hash = "sha256:0585bb97bd5f5bf3ed8d4bf624593e4c5a14d06c811d1952b07a1f94b4d12c51"}, ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" +httplib2 = ">=0.19.0,<1.0.0" uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.38.0" +version = "2.40.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, - {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, + {file = "google_auth-2.40.0-py2.py3-none-any.whl", hash = "sha256:dc3a5078acb1043c3e43685c22d628afe40af8559cf561de388e0c939280fcc8"}, + {file = "google_auth-2.40.0.tar.gz", hash = "sha256:c277cf39f7c192d8540eb6331c08b5a0796e8041af8343ae73dd6b269732ca6c"}, ] [package.dependencies] @@ -718,12 +740,14 @@ pyasn1-modules = ">=0.2.1" rsa = ">=3.1.4,<5" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] [[package]] name = "google-auth-httplib2" @@ -742,13 +766,13 @@ httplib2 = ">=0.19.0" [[package]] name = "google-auth-oauthlib" -version = "1.2.1" +version = "1.2.2" description = "Google Authentication Library" optional = false python-versions = ">=3.6" files = [ - {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, - {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, + {file = "google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2"}, + {file = "google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684"}, ] [package.dependencies] @@ -760,101 +784,83 @@ tool = ["click (>=6.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.69.1" +version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis_common_protos-1.69.1-py2.py3-none-any.whl", hash = "sha256:4077f27a6900d5946ee5a369fab9c8ded4c0ef1c6e880458ea2f70c14f7b70d5"}, - {file = "googleapis_common_protos-1.69.1.tar.gz", hash = "sha256:e20d2d8dda87da6fe7340afbbdf4f0bcb4c8fae7e6cadf55926c31f946b0b9b1"}, + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.1" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, - {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, - {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, - {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, - {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, - {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, - {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, - {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, - {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, - {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, - {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, - {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, - {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, - {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, - {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, - {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, - {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, + {file = "greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1a40a17e2c7348f5eee5d8e1b4fa6a937f0587eba89411885a36a8e1fc29bd2"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5193135b3a8d0017cb438de0d49e92bf2f6c1c770331d24aa7500866f4db4017"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639a94d001fe874675b553f28a9d44faed90f9864dc57ba0afef3f8d76a18b04"}, + {file = "greenlet-3.2.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fe303381e7e909e42fb23e191fc69659910909fdcd056b92f6473f80ef18543"}, + {file = "greenlet-3.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:72c9b668454e816b5ece25daac1a42c94d1c116d5401399a11b77ce8d883110c"}, + {file = "greenlet-3.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6079ae990bbf944cf66bea64a09dcb56085815630955109ffa98984810d71565"}, + {file = "greenlet-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e63cd2035f49376a23611fbb1643f78f8246e9d4dfd607534ec81b175ce582c2"}, + {file = "greenlet-3.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aa30066fd6862e1153eaae9b51b449a6356dcdb505169647f69e6ce315b9468b"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0f3a0a67786facf3b907a25db80efe74310f9d63cc30869e49c79ee3fcef7e"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64a4d0052de53ab3ad83ba86de5ada6aeea8f099b4e6c9ccce70fb29bc02c6a2"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852ef432919830022f71a040ff7ba3f25ceb9fe8f3ab784befd747856ee58530"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4818116e75a0dd52cdcf40ca4b419e8ce5cb6669630cb4f13a6c384307c9543f"}, + {file = "greenlet-3.2.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9afa05fe6557bce1642d8131f87ae9462e2a8e8c46f7ed7929360616088a3975"}, + {file = "greenlet-3.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5c12f0d17a88664757e81a6e3fc7c2452568cf460a2f8fb44f90536b2614000b"}, + {file = "greenlet-3.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dbb4e1aa2000852937dd8f4357fb73e3911da426df8ca9b8df5db231922da474"}, + {file = "greenlet-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:cb5ee928ce5fedf9a4b0ccdc547f7887136c4af6109d8f2fe8e00f90c0db47f5"}, + {file = "greenlet-3.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0ba2811509a30e5f943be048895a983a8daf0b9aa0ac0ead526dfb5d987d80ea"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4245246e72352b150a1588d43ddc8ab5e306bef924c26571aafafa5d1aaae4e8"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7abc0545d8e880779f0c7ce665a1afc3f72f0ca0d5815e2b006cafc4c1cc5840"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dcc6d604a6575c6225ac0da39df9335cc0c6ac50725063fa90f104f3dbdb2c9"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2273586879affca2d1f414709bb1f61f0770adcabf9eda8ef48fd90b36f15d12"}, + {file = "greenlet-3.2.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff38c869ed30fff07f1452d9a204ece1ec6d3c0870e0ba6e478ce7c1515acf22"}, + {file = "greenlet-3.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e934591a7a4084fa10ee5ef50eb9d2ac8c4075d5c9cf91128116b5dca49d43b1"}, + {file = "greenlet-3.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:063bcf7f8ee28eb91e7f7a8148c65a43b73fbdc0064ab693e024b5a940070145"}, + {file = "greenlet-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7132e024ebeeeabbe661cf8878aac5d2e643975c4feae833142592ec2f03263d"}, + {file = "greenlet-3.2.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e1967882f0c42eaf42282a87579685c8673c51153b845fde1ee81be720ae27ac"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77ae69032a95640a5fe8c857ec7bee569a0997e809570f4c92048691ce4b437"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3227c6ec1149d4520bc99edac3b9bc8358d0034825f3ca7572165cb502d8f29a"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ddda0197c5b46eedb5628d33dad034c455ae77708c7bf192686e760e26d6a0c"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de62b542e5dcf0b6116c310dec17b82bb06ef2ceb696156ff7bf74a7a498d982"}, + {file = "greenlet-3.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07a0c01010df42f1f058b3973decc69c4d82e036a951c3deaf89ab114054c07"}, + {file = "greenlet-3.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2530bfb0abcd451ea81068e6d0a1aac6dabf3f4c23c8bd8e2a8f579c2dd60d95"}, + {file = "greenlet-3.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c472adfca310f849903295c351d297559462067f618944ce2650a1878b84123"}, + {file = "greenlet-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:24a496479bc8bd01c39aa6516a43c717b4cee7196573c47b1f8e1011f7c12495"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175d583f7d5ee57845591fc30d852b75b144eb44b05f38b67966ed6df05c8526"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ecc9d33ca9428e4536ea53e79d781792cee114d2fa2695b173092bdbd8cd6d5"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f56382ac4df3860ebed8ed838f268f03ddf4e459b954415534130062b16bc32"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc45a7189c91c0f89aaf9d69da428ce8301b0fd66c914a499199cfb0c28420fc"}, + {file = "greenlet-3.2.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51a2f49da08cff79ee42eb22f1658a2aed60c72792f0a0a95f5f0ca6d101b1fb"}, + {file = "greenlet-3.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:0c68bbc639359493420282d2f34fa114e992a8724481d700da0b10d10a7611b8"}, + {file = "greenlet-3.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:e775176b5c203a1fa4be19f91da00fd3bff536868b77b237da3f4daa5971ae5d"}, + {file = "greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189"}, + {file = "greenlet-3.2.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:17964c246d4f6e1327edd95e2008988a8995ae3a7732be2f9fc1efed1f1cdf8c"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b4ec7f65f0e4a1500ac475c9343f6cc022b2363ebfb6e94f416085e40dea15"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b38d53cf268da963869aa25a6e4cc84c1c69afc1ae3391738b2603d110749d01"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a7490f74e8aabc5f29256765a99577ffde979920a2db1f3676d265a3adba41"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4339b202ac20a89ccd5bde0663b4d00dc62dd25cb3fb14f7f3034dec1b0d9ece"}, + {file = "greenlet-3.2.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a750f1046994b9e038b45ae237d68153c29a3a783075211fb1414a180c8324b"}, + {file = "greenlet-3.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:374ffebaa5fbd10919cd599e5cf8ee18bae70c11f9d61e73db79826c8c93d6f9"}, + {file = "greenlet-3.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b89e5d44f55372efc6072f59ced5ed1efb7b44213dab5ad7e0caba0232c6545"}, + {file = "greenlet-3.2.1-cp39-cp39-win32.whl", hash = "sha256:b7503d6b8bbdac6bbacf5a8c094f18eab7553481a1830975799042f26c9e101b"}, + {file = "greenlet-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:e98328b8b8f160925d6b1c5b1879d8e64f6bd8cf11472b7127d579da575b77d9"}, + {file = "greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7"}, ] [package.extras] @@ -863,48 +869,48 @@ test = ["objgraph", "psutil"] [[package]] name = "h3" -version = "4.2.1" +version = "4.2.2" description = "Uber's hierarchical hexagonal geospatial indexing system" optional = false python-versions = ">=3.8" files = [ - {file = "h3-4.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8e4785358c2c68a3d5acf3bec074b0125378ad80b35e961b0afd5a4e7edbc62f"}, - {file = "h3-4.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:08481cc13b26ff40cb5e6d72d6c4a34e007bb31d3ca7f6ff462b20bf16b2d7da"}, - {file = "h3-4.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbeab692bb62e92434772f7243095e097c83326f116d8c933f484096a7c64b38"}, - {file = "h3-4.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f4f1feef8e95bdce9cbefbf08808399f2180ef30485fa94796a4f21222e65de"}, - {file = "h3-4.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f903fc9433b0c840015b5b9b11b9b4e5c515890fce0889a4c481069466e29b7c"}, - {file = "h3-4.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:126393d674df1a1e05c816e88ce554a6c989d93c3bdd0b9f7468d30c41828ee0"}, - {file = "h3-4.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c5aff0b8de63a760d8ec162c3c959d0418321c54b84193472cafb1560ab540"}, - {file = "h3-4.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9800d0a00a2231d896d6a47e8046d16175928cd947b6a2d7b64d0176770ee4f1"}, - {file = "h3-4.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec751665b2be8ff92d5b62637341b338875cfd2457789abffa0aa694c2dfb11c"}, - {file = "h3-4.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e2ea658437002a0ac6c0c08846f44cfcba380a57596d3376233f0dbdd7e6e0"}, - {file = "h3-4.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9bdebc0fd8877176bb6fd0ab4d1cb13d7e676619715ee2c37bd2c8de70a2ca8b"}, - {file = "h3-4.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1da01a46c69476b58b68279eb9b6b74751941b6bf7a03b5ba54e1fd36297d46c"}, - {file = "h3-4.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2b0c3ee94714799467fb26152dbd0309e7c5aa1a020518dace8fd3ddbe4d1da7"}, - {file = "h3-4.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:292ca62f7474a88fc961bf30d08fccb6ec3270e5a479494e2262b9c11c713394"}, - {file = "h3-4.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a8eb8e8e6a4b18466208dbb72386dd695eb9534c13deea6b0f37708a3f771b9"}, - {file = "h3-4.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b326683200e7a8845a260b2798509bf537875966fbda86bab049e858860f069d"}, - {file = "h3-4.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37869289e0208cfab30bc82fc356c48a2e99d5eaeaca7f0630bd5eb755db01db"}, - {file = "h3-4.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8e9d7a99a6e0a485f115ebbe293036a873348eb803c667b886deb1f6fac8850"}, - {file = "h3-4.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4c4da955ad72a5162fadda01d81a944d3a02a0737e9d59c9f574cf71f30555e"}, - {file = "h3-4.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e636e759aa73a3c5adb50a62735dc66f7a8517ee26091563e1030bc99fdf7d4"}, - {file = "h3-4.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9af2122b4c137d4867f6092478a0d7f37c9fd1c9f920f3fb4dff9dd9197bcd3e"}, - {file = "h3-4.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:232e2d157a5512cc0c7a44fea775766a66b00aba18ea38b7093ba75eb50f8f27"}, - {file = "h3-4.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f627af7327c47006609d08f53fd993fbf811a2580d70b057f054434f50400aa"}, - {file = "h3-4.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:69bf4b9365acb093544b28b5b2bfa59bf1d98281a121492c887cd3137739ae33"}, - {file = "h3-4.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fadb5c817880aa1edc97c7e318fd62931ba367c7213a13b0a60439be0b719afd"}, - {file = "h3-4.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13c1e6b7b11e68b11df8c496ef9c275415eb492d9e91a04eacd90676f31d703"}, - {file = "h3-4.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5400c3b8e40ec81b63babf1760dadf5c014081c41be2eb7a031c00efa4fbf2d"}, - {file = "h3-4.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ed5493ba7f01840287a39ae37394f42db78dad84771db7c71cd6aa20ee0d70"}, - {file = "h3-4.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daa2943bcb3872b44d854031e0baaa7a39f2d39ff49959b567f20f3313a512d6"}, - {file = "h3-4.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:f514c68ee0cb4b2a960c3927d44cf2b9a14ef7f44fe1c0d87d28d58bcbaf2b31"}, - {file = "h3-4.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e6b8e352a2a72f0a3a20bebf55ae70da4b1a3af05d48924e2bb78c78e859513f"}, - {file = "h3-4.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faef924773d08d42ab35ab38a3db8652860276dd0a8fa99216811edcf9359d05"}, - {file = "h3-4.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e969b03b0f4e0ac32db4b2deec4dd89233024c6a8ef6de0a9e164ae9c219961a"}, - {file = "h3-4.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e76824d9987877640931485a4d7ffa56c7653daf329501a80ef323be5c2e9f75"}, - {file = "h3-4.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a4e8e211074b1c88c2a1ad8372f5ce48d2d1fd1b13533e52b54fdeba4e0fe6f8"}, - {file = "h3-4.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6af355d3c8d7316b3a5606f2fe67266e81ee3bfeba239d574373a7df0925ff05"}, - {file = "h3-4.2.1.tar.gz", hash = "sha256:e64afe4166a6e31b06df71f0da6039683b04d88c2fec8c73d7bf4d5f36d7cb23"}, + {file = "h3-4.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:75bae45428b133c3006a3c72646e42856c8800a74e47a818e1bcd242a86e9dae"}, + {file = "h3-4.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9992eb522636cbcf7c5b5fd32436395bee9423805ee06fd8ad6eca83f4af654"}, + {file = "h3-4.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7687de29510ece132e59739bd87dcb3fe25783150502685d7d5be266ea4162b5"}, + {file = "h3-4.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51d715d0471bb581bd94f72529c3194e9b91ce30202672d2050161292f774742"}, + {file = "h3-4.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f4eab1fdfc15b83edac415377790114cae5f117ae7d3293fc9b27354fb2987e6"}, + {file = "h3-4.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:6c2677a20c46148a31838e5b4eed11554fd83a9f07c63a2056f4187cb1ba605f"}, + {file = "h3-4.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4039d3bc8236ea371402c755867c6b0d36d072dcd1117e22a9040dd63ba522c"}, + {file = "h3-4.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d637189edf2f1523d625e03415ce4b7fdfe224e6254b73c987b8774214a09edf"}, + {file = "h3-4.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5b88187fcfc88a4315633b0cf27fa55b736948b78833045d03a08a33db4feb3"}, + {file = "h3-4.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2459e482147a37292e9638f08bf3b4f32df972457064721b888883c1883848c9"}, + {file = "h3-4.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4db576b1d96d3d828f1b06eef943e1ad00eb9ff2caae5fb894ec3f4badcfde34"}, + {file = "h3-4.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:db1dc702760a8b94abbd1ddb6efd1e88c63032f0bbd7288159af54ce6a611f3a"}, + {file = "h3-4.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09b7df785cf8a191e33e3b15ba34aef864b78061b2aeaee87a434aa703bb29c"}, + {file = "h3-4.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35afe80a011abf9ffa8bc885b847a01a0909fca6556f6f997d6adf7beec87e38"}, + {file = "h3-4.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a56aa550e56de5fc9726fd20f484b23a336d6b0844e40604500ce9c3bc8f86c"}, + {file = "h3-4.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e83d7484441f42a6bcd0ffc9626387e4ff8a7563ae659667f5b59b814fe198b7"}, + {file = "h3-4.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737d7cf3aeebc60b0e091220a1bdf94608f16457589cf67acc288444ee5f5dc8"}, + {file = "h3-4.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bc61382bceff776f0d6278e6ba54d94c9e15137d527adbd50914c7591f9c4460"}, + {file = "h3-4.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70059de58956d58a92213a0b7adbc4fcf643a102911bc1ad637c99554765add"}, + {file = "h3-4.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a9b32da65d9ed6f97ca9ff404721c97f0faec5656e205a7601628ae402f2b282"}, + {file = "h3-4.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bea5b734343c5074c60a575cb609157f51aa1c8da5c4064491bb5c58a4dac55"}, + {file = "h3-4.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7836e8e32c30f34fba1c26cdce8e4e7b4a9f48d18f3a2ab9edfeee995deb92a"}, + {file = "h3-4.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:652299d3bef76af65f5166bc4af0598afa6c7d93cfc13c7a645a95d8c8e5b10e"}, + {file = "h3-4.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfda0f2864462f074cc22df75a50b9db010988e341a1871993675bb21954891b"}, + {file = "h3-4.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c7320ea6551b21275e14cde684ff060b2a7ee1cfd7c4232cccc8922ead5820c"}, + {file = "h3-4.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e8e6ba11dba11b130fced35e553b2813f4ab0ff5d34d570443e7e9d454845d6"}, + {file = "h3-4.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4988081ae5544bf316bff6dcf627328a244841580f73238eadef5a7374b63384"}, + {file = "h3-4.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd448725c7c36c59edf831e4666ea950060e99afdfc38e384da3cbab21453da"}, + {file = "h3-4.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ffb174db4b7fff195ac72bd1c4eef9b4b720ede5c991257b0136860a9c2d3f5"}, + {file = "h3-4.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:7fc37373148b2c5626da1a580db4d686dac3243e1cb1c29b20aab281c640a129"}, + {file = "h3-4.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8794ee6f88c6af206bc74d18cac0c3b57aeea8eb697a884e6aa7ad9c8c132d26"}, + {file = "h3-4.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6006c28b9d1fbf993c916983ed497add8a0be73a2bf9fb684324624cc69097c1"}, + {file = "h3-4.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529ae90ac230561cc9150038d629d396772f272bef95af870d281cb14145d6f3"}, + {file = "h3-4.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7bcdc503332b7087f083a699c1434084cad38dd15d60e4390da54019628a813"}, + {file = "h3-4.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0b48d8b1efe07e13aecb279c8154962d05b13cb7056d5ed133f66ddd71a490a5"}, + {file = "h3-4.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:b91698ec93973dfbc8926d49a2aca00f7ec23f34e354744ec2db24185954b188"}, + {file = "h3-4.2.2.tar.gz", hash = "sha256:5cc78d546b5c732f480a842e3e436c393fe37fe59b063fe9cb5589206b7c4c7e"}, ] [package.extras] @@ -993,13 +999,13 @@ test = ["aioresponses (>=0.7.3)", "coverage (>=4.0.3)", "flake8 (>=5.0.3)", "htt [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1090,13 +1096,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.4.2" +version = "1.5.0" description = "Lightweight pipelining with Python functions" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, - {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, + {file = "joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491"}, + {file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"}, ] [[package]] @@ -1432,6 +1438,30 @@ matplotlib = ">=3.1,<3.7.1 || >3.7.1" [package.extras] docs = ["pandas", "pydata_sphinx_theme (!=0.10.1)", "sphinx", "sphinx-gallery"] +[[package]] +name = "narwhals" +version = "1.38.0" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "narwhals-1.38.0-py3-none-any.whl", hash = "sha256:148de8f1bec24644e260f6f2fe9b8e2e435f49ad13537a5261f0de34701e9153"}, + {file = "narwhals-1.38.0.tar.gz", hash = "sha256:0a356a21ad00de0db0e631332a823a6a6755544bd10b8e68a02d75029c71392e"}, +] + +[package.extras] +cudf = ["cudf (>=24.10.0)"] +dask = ["dask[dataframe] (>=2024.8)"] +duckdb = ["duckdb (>=1.0)"] +ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] +modin = ["modin"] +pandas = ["pandas (>=0.25.3)"] +polars = ["polars (>=0.20.3)"] +pyarrow = ["pyarrow (>=11.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sqlframe = ["sqlframe (>=3.22.0)"] + [[package]] name = "nh3" version = "0.2.18" @@ -1459,37 +1489,37 @@ files = [ [[package]] name = "numba" -version = "0.61.0" +version = "0.61.2" description = "compiling Python code using LLVM" optional = false python-versions = ">=3.10" files = [ - {file = "numba-0.61.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9cab9783a700fa428b1a54d65295122bc03b3de1d01fb819a6b9dbbddfdb8c43"}, - {file = "numba-0.61.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:46c5ae094fb3706f5adf9021bfb7fc11e44818d61afee695cdee4eadfed45e98"}, - {file = "numba-0.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fb74e81aa78a2303e30593d8331327dfc0d2522b5db05ac967556a26db3ef87"}, - {file = "numba-0.61.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0ebbd4827091384ab8c4615ba1b3ca8bc639a3a000157d9c37ba85d34cd0da1b"}, - {file = "numba-0.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:43aa4d7d10c542d3c78106b8481e0cbaaec788c39ee8e3d7901682748ffdf0b4"}, - {file = "numba-0.61.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:bf64c2d0f3d161af603de3825172fb83c2600bcb1d53ae8ea568d4c53ba6ac08"}, - {file = "numba-0.61.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de5aa7904741425f28e1028b85850b31f0a245e9eb4f7c38507fb893283a066c"}, - {file = "numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21c2fe25019267a608e2710a6a947f557486b4b0478b02e45a81cf606a05a7d4"}, - {file = "numba-0.61.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:74250b26ed6a1428763e774dc5b2d4e70d93f73795635b5412b8346a4d054574"}, - {file = "numba-0.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:b72bbc8708e98b3741ad0c63f9929c47b623cc4ee86e17030a4f3e301e8401ac"}, - {file = "numba-0.61.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:152146ecdbb8d8176f294e9f755411e6f270103a11c3ff50cecc413f794e52c8"}, - {file = "numba-0.61.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5cafa6095716fcb081618c28a8d27bf7c001e09696f595b41836dec114be2905"}, - {file = "numba-0.61.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffe9fe373ed30638d6e20a0269f817b2c75d447141f55a675bfcf2d1fe2e87fb"}, - {file = "numba-0.61.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9f25f7fef0206d55c1cfb796ad833cbbc044e2884751e56e798351280038484c"}, - {file = "numba-0.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:550d389573bc3b895e1ccb18289feea11d937011de4d278b09dc7ed585d1cdcb"}, - {file = "numba-0.61.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:b96fafbdcf6f69b69855273e988696aae4974115a815f6818fef4af7afa1f6b8"}, - {file = "numba-0.61.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f6c452dca1de8e60e593f7066df052dd8da09b243566ecd26d2b796e5d3087d"}, - {file = "numba-0.61.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44240e694d4aa321430c97b21453e46014fe6c7b8b7d932afa7f6a88cc5d7e5e"}, - {file = "numba-0.61.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:764f0e47004f126f58c3b28e0a02374c420a9d15157b90806d68590f5c20cc89"}, - {file = "numba-0.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:074cd38c5b1f9c65a4319d1f3928165f48975ef0537ad43385b2bd908e6e2e35"}, - {file = "numba-0.61.0.tar.gz", hash = "sha256:888d2e89b8160899e19591467e8fdd4970e07606e1fbc248f239c89818d5f925"}, + {file = "numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a"}, + {file = "numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd"}, + {file = "numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642"}, + {file = "numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2"}, + {file = "numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9"}, + {file = "numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2"}, + {file = "numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b"}, + {file = "numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60"}, + {file = "numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18"}, + {file = "numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1"}, + {file = "numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2"}, + {file = "numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8"}, + {file = "numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546"}, + {file = "numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd"}, + {file = "numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18"}, + {file = "numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154"}, + {file = "numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140"}, + {file = "numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab"}, + {file = "numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e"}, + {file = "numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7"}, + {file = "numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d"}, ] [package.dependencies] llvmlite = "==0.44.*" -numpy = ">=1.24,<2.2" +numpy = ">=1.24,<2.3" [[package]] name = "numpy" @@ -1563,13 +1593,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] @@ -1797,39 +1827,37 @@ six = ">=1.8.0" [[package]] name = "proto-plus" -version = "1.26.0" +version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" files = [ - {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, - {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<7.0.0" [package.extras] testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.3" +version = "6.30.2" description = "" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, - {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, - {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, - {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, - {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, - {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, - {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, - {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, - {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, + {file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"}, + {file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"}, + {file = "protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d"}, + {file = "protobuf-6.30.2-cp39-cp39-win32.whl", hash = "sha256:524afedc03b31b15586ca7f64d877a98b184f007180ce25183d1a5cb230ee72b"}, + {file = "protobuf-6.30.2-cp39-cp39-win_amd64.whl", hash = "sha256:acec579c39c88bd8fbbacab1b8052c793efe83a0a5bd99db4a31423a25c0a0e2"}, + {file = "protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51"}, + {file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"}, ] [[package]] @@ -1921,17 +1949,17 @@ files = [ [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, - {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" @@ -2103,126 +2131,18 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] diagrams = ["jinja2", "railroad-diagrams"] -[[package]] -name = "pyqt5" -version = "5.15.11" -description = "Python bindings for the Qt cross platform application toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2"}, - {file = "PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6"}, - {file = "PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1"}, - {file = "PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd"}, - {file = "PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517"}, - {file = "PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52"}, -] - -[package.dependencies] -PyQt5-Qt5 = ">=5.15.2,<5.16.0" -PyQt5-sip = ">=12.15,<13" - -[[package]] -name = "pyqt5-qt5" -version = "5.15.16" -description = "The subset of a Qt installation needed by PyQt5." -optional = false -python-versions = "*" -files = [ - {file = "PyQt5_Qt5-5.15.16-1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2cfa8f50dd29618ef98f29355f83d8a5f3e41003be22128e9b5d94d214b6b468"}, - {file = "PyQt5_Qt5-5.15.16-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:18b6fec012de60921fcb131cf2a21368171dc29050d43e4b81a64be407a36105"}, - {file = "PyQt5_Qt5-5.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1a0e7ae35a7615c74a293705204579650930486a89af23082462f429dae504a"}, - {file = "PyQt5_Qt5-5.15.16-py3-none-manylinux2014_x86_64.whl", hash = "sha256:5ee1754a6460849cba76c0f0c490c0ccc3b514abc780b141cf772db22b76b54b"}, -] - -[[package]] -name = "pyqt5-sip" -version = "12.17.0" -description = "The sip module support for PyQt5" -optional = false -python-versions = ">=3.9" -files = [ - {file = "PyQt5_sip-12.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec47914cc751608e587c1c2fdabeaf4af7fdc28b9f62796c583bea01c1a1aa3e"}, - {file = "PyQt5_sip-12.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2f2a8dcc7626fe0da73a0918e05ce2460c7a14ddc946049310e6e35052105434"}, - {file = "PyQt5_sip-12.17.0-cp310-cp310-win32.whl", hash = "sha256:0c75d28b8282be3c1d7dbc76950d6e6eba1e334783224e9b9835ce1a9c64f482"}, - {file = "PyQt5_sip-12.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c4bc535bae0dfa764e8534e893619fe843ce5a2e25f901c439bcb960114f686"}, - {file = "PyQt5_sip-12.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c912807dd638644168ea8c7a447bfd9d85a19471b98c2c588c4d2e911c09b0a"}, - {file = "PyQt5_sip-12.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:71514a7d43b44faa1d65a74ad2c5da92c03a251bdc749f009c313f06cceacc9a"}, - {file = "PyQt5_sip-12.17.0-cp311-cp311-win32.whl", hash = "sha256:023466ae96f72fbb8419b44c3f97475de6642fa5632520d0f50fc1a52a3e8200"}, - {file = "PyQt5_sip-12.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb565469d08dcb0a427def0c45e722323beb62db79454260482b6948bfd52d47"}, - {file = "PyQt5_sip-12.17.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea08341c8a5da00c81df0d689ecd4ee47a95e1ecad9e362581c92513f2068005"}, - {file = "PyQt5_sip-12.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a92478d6808040fbe614bb61500fbb3f19f72714b99369ec28d26a7e3494115"}, - {file = "PyQt5_sip-12.17.0-cp312-cp312-win32.whl", hash = "sha256:b0ff280b28813e9bfd3a4de99490739fc29b776dc48f1c849caca7239a10fc8b"}, - {file = "PyQt5_sip-12.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:54c31de7706d8a9a8c0fc3ea2c70468aba54b027d4974803f8eace9c22aad41c"}, - {file = "PyQt5_sip-12.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c7a7ff355e369616b6bcb41d45b742327c104b2bf1674ec79b8d67f8f2fa9543"}, - {file = "PyQt5_sip-12.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:419b9027e92b0b707632c370cfc6dc1f3b43c6313242fc4db57a537029bd179c"}, - {file = "PyQt5_sip-12.17.0-cp313-cp313-win32.whl", hash = "sha256:351beab964a19f5671b2a3e816ecf4d3543a99a7e0650f88a947fea251a7589f"}, - {file = "PyQt5_sip-12.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:672c209d05661fab8e17607c193bf43991d268a1eefbc2c4551fbf30fd8bb2ca"}, - {file = "PyQt5_sip-12.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d65a9c1b4cbbd8e856254609f56e897d2cb5c903f77b75fb720cb3a32c76b92b"}, - {file = "PyQt5_sip-12.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:32b03e7e77ecd7b4119eba486b0706fa59b490bcceb585f9b6ddec8a582082db"}, - {file = "PyQt5_sip-12.17.0-cp39-cp39-win32.whl", hash = "sha256:5b6c734f4ad28f3defac4890ed747d391d246af279200935d49953bc7d915b8c"}, - {file = "PyQt5_sip-12.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:855e8f5787d57e26a48d8c3de1220a8e92ab83be8d73966deac62fdae03ea2f9"}, - {file = "pyqt5_sip-12.17.0.tar.gz", hash = "sha256:682dadcdbd2239af9fdc0c0628e2776b820e128bec88b49b8d692fe682f90b4f"}, -] - -[[package]] -name = "pyqt5-stubs" -version = "5.15.6.0" -description = "PEP561 stub files for the PyQt5 framework" -optional = false -python-versions = ">= 3.5" -files = [ - {file = "PyQt5-stubs-5.15.6.0.tar.gz", hash = "sha256:91270ac23ebf38a1dc04cd97aa852cd08af82dc839100e5395af1447e3e99707"}, - {file = "PyQt5_stubs-5.15.6.0-py3-none-any.whl", hash = "sha256:7fb8177c72489a8911f021b7bd7c33f12c87f6dba92dcef3fdcdb5d9400f0f3f"}, -] - -[package.extras] -dev = ["mypy (==0.930)", "pytest", "pytest-xvfb"] - -[[package]] -name = "pyqtwebengine" -version = "5.15.7" -description = "Python bindings for the Qt WebEngine framework" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyQtWebEngine-5.15.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:e17187d9a3db3bab041f31385ed72832312557fefc5bd63ae4692df306dc1572"}, - {file = "PyQtWebEngine-5.15.7-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:021814af1ff7d8be447c5314891cd4ddc931deae393dc2d38a816569aa0eb8cd"}, - {file = "PyQtWebEngine-5.15.7-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:965461ca0cf414e03bd510a9a0e2ea36dc21deaa7fc4a631be4a1f2aa0327179"}, - {file = "PyQtWebEngine-5.15.7-cp38-abi3-win32.whl", hash = "sha256:c0680527b1af3e0145ce5e0f2ba2156ff0b4b38844392cf0ddd37ede6a9edeab"}, - {file = "PyQtWebEngine-5.15.7-cp38-abi3-win_amd64.whl", hash = "sha256:bd5e8c426d6f6b352cd15800d64a89b2a4a11e098460b818c7bdcf5e5612e44f"}, - {file = "PyQtWebEngine-5.15.7.tar.gz", hash = "sha256:f121ac6e4a2f96ac289619bcfc37f64e68362f24a346553f5d6c42efa4228a4d"}, -] - -[package.dependencies] -PyQt5 = ">=5.15.4" -PyQt5-sip = ">=12.13,<13" -PyQtWebEngine-Qt5 = ">=5.15.0,<5.16.0" - -[[package]] -name = "pyqtwebengine-qt5" -version = "5.15.16" -description = "The subset of a Qt installation needed by PyQtWebEngine." -optional = false -python-versions = "*" -files = [ - {file = "PyQtWebEngine_Qt5-5.15.16-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:80ea0170708cbaf8c9d88c11b869f82d12a7b324c4ee14f617a11bd177f4f43a"}, - {file = "PyQtWebEngine_Qt5-5.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:887a5c4f5b0419bad13d701f08533615216d5ee00cbb0e8d52a5fd6f8633adbb"}, - {file = "PyQtWebEngine_Qt5-5.15.16-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d27d4b31625e03cc310d385989e9662d080c1dda0d24b55dada653c98bd0c44e"}, -] - [[package]] name = "pytest" version = "7.4.4" @@ -2261,13 +2181,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] @@ -2295,22 +2215,6 @@ files = [ {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] -[[package]] -name = "pywinpty" -version = "2.0.15" -description = "Pseudo terminal support for Windows from Python." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e"}, - {file = "pywinpty-2.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:9a6bcec2df2707aaa9d08b86071970ee32c5026e10bcc3cc5f6f391d85baf7ca"}, - {file = "pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc"}, - {file = "pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408"}, - {file = "pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901"}, - {file = "pywinpty-2.0.15-cp39-cp39-win_amd64.whl", hash = "sha256:d261cd88fcd358cfb48a7ca0700db3e1c088c9c10403c9ebc0d8a8b57aa6a117"}, - {file = "pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2"}, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -2373,20 +2277,6 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] -[[package]] -name = "qt-py" -version = "1.4.6" -description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." -optional = false -python-versions = "*" -files = [ - {file = "qt_py-1.4.6-py2.py3-none-any.whl", hash = "sha256:1e0f8da9af74f2b3448904fab313f6f79cad56b82895f1a2c541243f00cc244e"}, - {file = "qt_py-1.4.6.tar.gz", hash = "sha256:d26f808a093754f0b44858745965bab138525cffc77c1296a3293171b2e2469f"}, -] - -[package.dependencies] -types-PySide2 = "*" - [[package]] name = "reactivex" version = "4.0.4" @@ -2507,13 +2397,13 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" description = "Pure-Python RSA implementation" optional = false -python-versions = ">=3.6,<4" +python-versions = "<4,>=3.6" files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] [package.dependencies] @@ -2521,29 +2411,29 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.11.2" +version = "0.11.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477"}, - {file = "ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272"}, - {file = "ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f"}, - {file = "ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc"}, - {file = "ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080"}, - {file = "ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4"}, - {file = "ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94"}, + {file = "ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3"}, + {file = "ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835"}, + {file = "ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304"}, + {file = "ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2"}, + {file = "ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4"}, + {file = "ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2"}, + {file = "ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8"}, ] [[package]] @@ -2680,13 +2570,13 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "76.0.0" +version = "76.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6"}, - {file = "setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4"}, + {file = "setuptools-76.1.0-py3-none-any.whl", hash = "sha256:34750dcb17d046929f545dec9b8349fe42bf4ba13ddffee78428aec422dbfb73"}, + {file = "setuptools-76.1.0.tar.gz", hash = "sha256:4959b9ad482ada2ba2320c8f1a8d8481d4d8d668908a7a1b84d987375cd7f5bd"}, ] [package.extras] @@ -2737,80 +2627,80 @@ docs = ["kaleido", "mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings[ [[package]] name = "sqlalchemy" -version = "2.0.38" +version = "2.0.40" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, - {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, - {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bece9527f5a98466d67fb5d34dc560c4da964240d8b09024bb21c1246545e04e"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8bb131ffd2165fae48162c7bbd0d97c84ab961deea9b8bab16366543deeab625"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9408fd453d5f8990405cc9def9af46bfbe3183e6110401b407c2d073c3388f47"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win32.whl", hash = "sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win_amd64.whl", hash = "sha256:c7b927155112ac858357ccf9d255dd8c044fd9ad2dc6ce4c4149527c901fa4c3"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50f5885bbed261fc97e2e66c5156244f9704083a674b8d17f24c72217d29baf5"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf0e99cdb600eabcd1d65cdba0d3c91418fee21c4aa1d28db47d095b1064a7d8"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe147fcd85aaed53ce90645c91ed5fca0cc88a797314c70dfd9d35925bd5d106"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf7cee56bd552385c1ee39af360772fbfc2f43be005c78d1140204ad6148438"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4aeb939bcac234b88e2d25d5381655e8353fe06b4e50b1c55ecffe56951d18c2"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c268b5100cfeaa222c40f55e169d484efa1384b44bf9ca415eae6d556f02cb08"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win32.whl", hash = "sha256:46628ebcec4f23a1584fb52f2abe12ddb00f3bb3b7b337618b80fc1b51177aff"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win_amd64.whl", hash = "sha256:7e0505719939e52a7b0c65d20e84a6044eb3712bb6f239c6b1db77ba8e173a37"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c884de19528e0fcd9dc34ee94c810581dd6e74aef75437ff17e696c2bfefae3e"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1abb387710283fc5983d8a1209d9696a4eae9db8d7ac94b402981fe2fe2e39ad"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfa124eda500ba4b0d3afc3e91ea27ed4754e727c7f025f293a22f512bcd4c9"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6b28d303b9d57c17a5164eb1fd2d5119bb6ff4413d5894e74873280483eeb5"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b5a5bbe29c10c5bfd63893747a1bf6f8049df607638c786252cb9243b86b6706"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f0fda83e113bb0fb27dc003685f32a5dcb99c9c4f41f4fa0838ac35265c23b5c"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win32.whl", hash = "sha256:957f8d85d5e834397ef78a6109550aeb0d27a53b5032f7a57f2451e1adc37e98"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win_amd64.whl", hash = "sha256:1ffdf9c91428e59744f8e6f98190516f8e1d05eec90e936eb08b257332c5e870"}, + {file = "sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a"}, + {file = "sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -2821,7 +2711,7 @@ mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -2848,65 +2738,50 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] [[package]] name = "tenacity" -version = "9.0.0" +version = "9.1.2" description = "Retry code until it succeeds" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, - {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, + {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, + {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, ] [package.extras] doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] -[[package]] -name = "termqt" -version = "1.0.0" -description = "A terminal emulator widget implemented in Python/Qt." -optional = false -python-versions = ">=3.7" -files = [ - {file = "termqt-1.0.0-py3-none-any.whl", hash = "sha256:bc5c0f6c5086b4ecc84477635b3bb6a41b551cfa31e8b24cf8b7a9bf5a1ff18e"}, - {file = "termqt-1.0.0.tar.gz", hash = "sha256:c181065649a79e54335d6e65eca7d6755eb9a44f9f61a4700d59ff2ca1fa3410"}, -] - -[package.dependencies] -pywinpty = {version = ">=2.0.1", markers = "platform_system == \"Windows\""} -"Qt.py" = ">=1.3.0" - [[package]] name = "threadpoolctl" -version = "3.5.0" +version = "3.6.0" description = "threadpoolctl" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, - {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, + {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, + {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, ] [[package]] name = "timezonefinder" -version = "6.5.8" +version = "6.5.9" description = "python package for finding the timezone of any point on earth (coordinates) offline" optional = false python-versions = "<4,>=3.8" files = [ - {file = "timezonefinder-6.5.8-cp310-cp310-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d539dbfade8bd3ef3c738cf7f849d845b62ac323774899859d401bb1c29a1849"}, - {file = "timezonefinder-6.5.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:85c55ceeb67c07919d7a71a036b37cfc612a956c4e0ad77eee72db4746da7599"}, - {file = "timezonefinder-6.5.8-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a635453bf65cbc0af46b9c5aeab306e15ac75fa089ac6c1e0a208a3c2898e7"}, - {file = "timezonefinder-6.5.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6e60c23ad89587bccbb396a9ac3e39f9ef35e49490842ae375fd42e36c82358"}, - {file = "timezonefinder-6.5.8-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723db23dfe476cfe636c45f9000f361b180c306b018beb66a2c9cef85ef1ccd5"}, - {file = "timezonefinder-6.5.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:caade719b741accff1983d6851b1d5438f20a7e9c9342a88b81f80df9f09081e"}, - {file = "timezonefinder-6.5.8-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8f8b7e9f91883ec16410f7fb4f3f47237928e434532a4564a5d4f14a28b1ddf"}, - {file = "timezonefinder-6.5.8-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c0f3edd2f25277f7eb4906de523260da371b2631db96605df8a48133497a6ef"}, - {file = "timezonefinder-6.5.8-cp38-cp38-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826943660242bab78fedd41ede2b66cef17350df48507e573502634a85b662d0"}, - {file = "timezonefinder-6.5.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85a7b523220bad54281af0eb3cd826cbc004085aeeadc82fc9d92c03011a59e0"}, - {file = "timezonefinder-6.5.8-cp39-cp39-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f79339bd532178ea94bf66f02212512825086100e2066d17f15550c31803be3"}, - {file = "timezonefinder-6.5.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:837d9a91cb648657c8ac5c365eea3cc48811744d45e7190dd9108b3740347cfb"}, - {file = "timezonefinder-6.5.8.tar.gz", hash = "sha256:0993ba3416e1e117eeb88af03d805e7627d36e2b8a4b0c85eaae630c29cbb093"}, + {file = "timezonefinder-6.5.9-cp310-cp310-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52d5a4a8fc96990f72d9d3d48297e789217f67689d3178c9ff8ea3ab57125e3b"}, + {file = "timezonefinder-6.5.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a84ad5afb02ca1b536481cee05a8f2d5d7dd4818f73cd780acd03aa3cc033c9"}, + {file = "timezonefinder-6.5.9-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:643535f76436b13216ed1c3b69c6ed8f793253810916ca6bef3b8e0cf3084fef"}, + {file = "timezonefinder-6.5.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4fa18e4b3f3ac1469bc50031700bc9696477cbf40953bdcbdec7bef20f3200d0"}, + {file = "timezonefinder-6.5.9-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9a0caf638f43b6dd9980731d1424500c7a6a5048db808aad7032560df5c663"}, + {file = "timezonefinder-6.5.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c5347c4a73b40af4867a2946a5172ec68b644e7036888b9b5a0e568499bfc0f3"}, + {file = "timezonefinder-6.5.9-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d1f30b550c24598459643285ef0f5469524ad7a32dd854199e7a2a463600ff"}, + {file = "timezonefinder-6.5.9-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4f36877ad3a988f329cbd3f04f7cadc56dce073ad4a7bda7397f46ac2a61f9a4"}, + {file = "timezonefinder-6.5.9-cp38-cp38-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7af80ceac094932c07750d384539f6c4397c7b1bcbbb51dc9d5a90a65ad1df4"}, + {file = "timezonefinder-6.5.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e402d54b6442c5203e0dfb3c6d1b4c96371d4e71ca86c99a35589ca0c90f5db"}, + {file = "timezonefinder-6.5.9-cp39-cp39-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6ad1719ae5769f5218facf5ceb25cc16a188e4b1a7baa2c7baf53cfd8568a"}, + {file = "timezonefinder-6.5.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c1ed4828b27b38ec454b0d82458fd35f7a7039f9a640afce51338c1c91a56a27"}, + {file = "timezonefinder-6.5.9.tar.gz", hash = "sha256:0d84c792a499fd098a35c701c3e3293423ba8d45c81b3eecd7c7cb72c7f1f415"}, ] [package.dependencies] @@ -2970,17 +2845,6 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] -[[package]] -name = "tomli-w" -version = "1.2.0" -description = "A lil' TOML writer" -optional = false -python-versions = ">=3.9" -files = [ - {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, - {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, -] - [[package]] name = "tornado" version = "6.4.2" @@ -3021,37 +2885,26 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] -[[package]] -name = "types-pyside2" -version = "5.15.2.1.7" -description = "The most accurate stubs for PySide2" -optional = false -python-versions = "*" -files = [ - {file = "types_pyside2-5.15.2.1.7-py2.py3-none-any.whl", hash = "sha256:a7bec4cb4657179415ca7ec7c70a45f9f9938664e22f385c85fd7cd724b07d4d"}, - {file = "types_pyside2-5.15.2.1.7.tar.gz", hash = "sha256:1d65072deb97481ad481b3414f94d02fd5da07f5e709c2d439ced14f79b2537c"}, -] - [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] [[package]] name = "tzdata" -version = "2025.1" +version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, - {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] @@ -3073,12 +2926,12 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "ubc-solar-data-tools" -version = "1.5.1" +version = "1.8.0" description = "UBC Solar's data analysis and querying tools" optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "ubc_solar_data_tools-1.5.1.tar.gz", hash = "sha256:6ff135d2f1cb3321ae124bbd068f4edc6830137316757b8e6583620f015bd5cb"}, + {file = "ubc_solar_data_tools-1.8.0.tar.gz", hash = "sha256:c1422c1aa053e7257f21d091d6eabb5e7a00a12725167d5022a8949dccc499b2"}, ] [package.dependencies] @@ -3093,22 +2946,17 @@ python-dotenv = ">=1.0.1,<2.0.0" pytz = ">=2024.2,<2025.0" requests = ">=2.32.3,<3.0.0" scipy = ">=1.14.1,<2.0.0" +solcast = {version = ">=1.3.0,<2.0.0", extras = ["pandas"]} sqlalchemy = ">=2.0.5,<3.0.0" [[package]] name = "ubc-solar-physics" -version = "1.4.2" +version = "1.5.1.dev10+g9cd81ab" description = "UBC Solar's Simulation Environment" optional = false python-versions = ">=3.9" -files = [ - {file = "ubc_solar_physics-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:a688d458d548f620e3c7b9b3e8f7565a64ace015c23ad88c2da00cbe84a1bc52"}, - {file = "ubc_solar_physics-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:dfde1678dd5008bb92bb2cd0e49910b0f6e66a8e141795800a33469587400b42"}, - {file = "ubc_solar_physics-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:8d945ade854b0b9ca33be67534bfd646bfef292da096dac157bfc8e1e9328005"}, - {file = "ubc_solar_physics-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:efd274fffc5f55a233d4c39325756d30b4dd24604cf60e38ce1acc2bbd47f620"}, - {file = "ubc_solar_physics-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:730ee7c74038e7898bef6ff07b4793711a3ce92dd68d783d8717f8ac25c5a9fb"}, - {file = "ubc_solar_physics-1.4.2.tar.gz", hash = "sha256:30d4bba88027ae24f74285d0c16c1aad93fd9e2375ff33152cfd77f20cd45969"}, -] +files = [] +develop = false [package.dependencies] "backports.tarfile" = "1.2.0" @@ -3146,6 +2994,10 @@ tqdm = "4.66.5" urllib3 = "2.2.2" zipp = "3.20.0" +[package.source] +type = "directory" +url = "../physics" + [[package]] name = "uritemplate" version = "4.1.1" @@ -3176,13 +3028,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "xyzservices" -version = "2025.1.0" +version = "2025.4.0" description = "Source of XYZ tiles providers" optional = false python-versions = ">=3.8" files = [ - {file = "xyzservices-2025.1.0-py3-none-any.whl", hash = "sha256:fa599956c5ab32dad1689960b3bb08fdcdbe0252cc82d84fc60ae415dc648907"}, - {file = "xyzservices-2025.1.0.tar.gz", hash = "sha256:5cdbb0907c20be1be066c6e2dc69c645842d1113a4e83e642065604a21f254ba"}, + {file = "xyzservices-2025.4.0-py3-none-any.whl", hash = "sha256:8d4db9a59213ccb4ce1cf70210584f30b10795bff47627cdfb862b39ff6e10c9"}, + {file = "xyzservices-2025.4.0.tar.gz", hash = "sha256:6fe764713648fac53450fbc61a3c366cb6ae5335a1b2ae0c3796b495de3709d8"}, ] [[package]] @@ -3203,4 +3055,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c9b84fdb7f3258bd05aee741e1ad57be4f8254b4491702a22c039bff99fa79e6" +content-hash = "96cea714334c0a3f1fb86ad2ddfb70b0d06f5b6aab5b146c6209d0c1070f3fd0" diff --git a/pyproject.toml b/pyproject.toml index 29093fa9..9e7c041f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,15 +7,15 @@ name = "ubc-solar-simulation" version = "1.1.0" description = "UBC Solar's Simulation Environment" readme = "README.md" -requires-python = ">=3.10" -license = {text = "MIT"} +requires-python = ">=3.12" +license = { text = "MIT" } authors = [ - {name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com"}, - {name = "Joshua Riefman", email = "joshuariefman@gmail.com"} + { name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com" }, + { name = "Joshua Riefman", email = "joshuariefman@gmail.com" } ] maintainers = [ - {name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com"}, - {name = "Joshua Riefman", email = "joshuariefman@gmail.com"} + { name = "UBC Solar Strategy Team", email = "strategy@ubcsolar.com" }, + { name = "Joshua Riefman", email = "joshuariefman@gmail.com" } ] keywords = ["car", "simulation", "solar"] classifiers = [ @@ -55,19 +55,17 @@ dependencies = [ "mplcursors>=0.5.3", "pandas>=2.2.2", "bokeh>=3.5.1", - "ubc-solar-physics>=1.4.2", "pydantic>=2,<3", - "ubc-solar-data-tools>=1.5.0", + "ubc-solar-data-tools==1.9.1", "anytree>=2.12.1", "folium>=0.19.5", + "pyqt5==5.15.11", "pyqt5-stubs>=5.15.6.0", - "pyqtwebengine>=5.15.7", + "pyqtwebengine==5.15.7", "tomli>=2.2.1", "tomli-w>=1.2.0", - "ansi2html>=1.9.2", - "termqt>=1.0.0", - "pyte>=0.8.2", - "pyqtdarktheme>=2.1.0", + "qt-material>=2.17", + "ubc-solar-physics==1.8.2", ] [project.urls] @@ -75,11 +73,49 @@ Homepage = "https://ubcsolar.com" Repository = "https://github.com/UBC-Solar/Simulation" Documentation = "https://ubc-solar-simulation.readthedocs.io/en/latest/" - -[dependency-groups] +[project.optional-dependencies] dev = [ - "ruff >=0.11.2" + "ruff>=0.11.2" ] [tool.setuptools] packages = ["simulation"] + +[tool.poetry.dependencies] +python = "^3.10" +bayesian_optimization = "^1.4.3" +matplotlib = "^3.7.2" +Pillow = "^9.4.0" +plotly = "^5.6.0" +polyline = "^1.4.0" +pytest = "^7.1.1" +python-dotenv = "^1.0.0" +pytz = "*" +requests = "^2.26.0" +scikit_learn = "^1.2.2" +setuptools = "^76.0.0" +timezonefinder = "^6.0.1" +tqdm = "^4.64.0" +strenum = "^0.4.15" +cffi = "^1.15.1" +pygad = "^3.0.1" +haversine = "^2.8.1" +gitpython = "^3.1.42" +google-api-core = "^2.17.1" +google-api-python-client = "^2.120.0" +google-auth = "^2.28.1" +google-auth-httplib2 = "^0.2.0" +google-auth-oauthlib = "^1.2.0" +googleapis-common-protos = "^1.62.0" +dill = ">=0.3.8" +solcast = "^1.2.3" +tzlocal = "^5.2" +mplcursors = "^0.5.3" +pandas = "^2.2.2" +bokeh = "^3.5.1" +ubc-solar-physics = {path = "../physics"} +pydantic = "2.*" +ubc-solar-data-tools = "^1.5.0" +anytree = "^2.12.1" +folium = "^0.19.5" +geopy = "^2.4.1" From dea7213b24c349c31ca19016e76c0a467f216dd9 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Tue, 30 Sep 2025 19:37:23 -0700 Subject: [PATCH 32/44] Undo changes to diagnostic_interface --- diagnostic_interface/tabs/copy.py | 172 ------------------ .../widgets/command_output.py | 113 ------------ 2 files changed, 285 deletions(-) delete mode 100644 diagnostic_interface/tabs/copy.py delete mode 100644 diagnostic_interface/widgets/command_output.py diff --git a/diagnostic_interface/tabs/copy.py b/diagnostic_interface/tabs/copy.py deleted file mode 100644 index 2b2a2072..00000000 --- a/diagnostic_interface/tabs/copy.py +++ /dev/null @@ -1,172 +0,0 @@ -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QPushButton, QMessageBox, - QGroupBox, QHBoxLayout -) -from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject, QTimer -from poetry.console.commands import self - -from diagnostic_interface import settings -from diagnostic_interface.canvas import CustomNavigationToolbar, PlotCanvas - - -HELP_MESSAGES = { - "VehicleVelocity": "This plot shows velocity over time.\n\n" - "- X-axis: Time\n" - "- Y-axis: Velocity (m/s)\n" - "- Data is sourced from the car's telemetry system.\n", -} - - -class PlotRefreshWorkerSignals(QObject): - finished = pyqtSignal(bool) # success or failure - - -class PlotRefreshWorker(QRunnable): - def __init__(self, plot_canvas, origin, source, event, data_name): - #def __init__(self, plot_canvas): - super().__init__() - self.plot_canvas = plot_canvas - self.origin = origin - self.source = source - self.event = event - self.data_name = data_name - self.signals = PlotRefreshWorkerSignals() - - def run(self): - success = self.plot_canvas.query_and_plot(self.origin, self.source, self.event, self.data_name) - self.signals.finished.emit(success) - - -class PlotTab2(QWidget): - close_requested = pyqtSignal(QWidget) - - #def __init__(self, origin : str, source: str, event: str, data_name: str, parent=None): - def __init__(self, origin = "production", source = "power", event = "FSGP_2024_Day_1", data_name1 = "PackPower", data_name2 = "MotorPower", parent = None): - super().__init__(parent) - - self.origin = origin - self.source = source - self.event = event - self.data_name1 = data_name1 - self.data_name2 = data_name2 - - self._thread_pool = QThreadPool() - - # Layout setup - self.layout = QVBoxLayout(self) - self.layout.setSpacing(10) - self.layout.setContentsMargins(30, 30, 30, 30) - - self.plot_canvas1 = PlotCanvas(self) - self.plot_canvas2 = PlotCanvas(self) - - self.toolbar1 = CustomNavigationToolbar(canvas=self.plot_canvas1) - self.toolbar2 = CustomNavigationToolbar(canvas=self.plot_canvas2) - # Buttons - help_button = QPushButton("Help") - help_button.setObjectName("helpButton") - help_button.clicked.connect(lambda: self.show_help_message(self.data_name1, self.data_name2, self.event)) - - close_button = QPushButton("Close Tab") - close_button.setObjectName("closeButton") - close_button.clicked.connect(self.request_close) - - button_group = QGroupBox("Actions") - button_layout = QHBoxLayout() - button_layout.addWidget(help_button) - button_layout.addWidget(close_button) - button_group.setLayout(button_layout) - - self.layout.addWidget(self.toolbar1) - self.layout.addWidget(self.plot_canvas1) - self.layout.addWidget(self.toolbar2) - self.layout.addWidget(self.plot_canvas2) - - self.layout.addWidget(button_group) - - self.setStyleSheet(""" - QPushButton#helpButton, QPushButton#closeButton { - padding: 6px 12px; - border-radius: 8px; - } - """) - - self.refresh_timer = QTimer() - self.refresh_timer.timeout.connect(self.refresh_plot) - -#stuff added: - # self.data_name1 = "PackPower" - # self.data_name2 = "MotorPower" - # - # self.origin1 = "production" - # self.source1 = "power" - # self.event1 = "FSGP_2024_Day_1" - - plot1 = self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name1) - plot2 = self.plot_canvas2.query_and_plot(self.origin, self.source, self.event, self.data_name2) - - # if not self.plot_canvas1.query_and_plot(self.origin, self.source, self.event, self.data_name): - # self.request_close() - - if not (plot1 and plot2): - self.request_close() - - def set_tab_active(self, active: bool) -> None: - if active: - self.refresh_timer.setInterval(settings.plot_timer_interval * 1000) - self.refresh_timer.start() - QTimer.singleShot(0, self.refresh_plot) - - else: - self.refresh_timer.stop() - - def refresh_plot(self): - worker1 = PlotRefreshWorker( - self.plot_canvas1, - #self.plot_canvas2, - self.origin, - self.source, - self.event, - self.data_name1 - ) - - worker1.signals.finished.connect(self._on_plot_refresh_finished) - self._thread_pool.start(worker1) - - worker2 = PlotRefreshWorker( - self.plot_canvas2, - # self.plot_canvas2, - self.origin, - self.source, - self.event, - self.data_name2 - ) - worker2.signals.finished.connect(self._on_plot_refresh_finished) - self._thread_pool.start(worker2) - - def _on_plot_refresh_finished(self, success: bool): - if not success: - self.request_close() - - def request_close(self): - self.close_requested.emit(self) - - def show_help_message(self, data_name1, data_name2, event): - message1 = HELP_MESSAGES.get(data_name1, "No specific help available for this plot.") - message2 = HELP_MESSAGES.get(data_name2, "No specific help available for this plot.") - QMessageBox.information(self, f"Help: {data_name1}", message1) - QMessageBox.information(self, f"Help: {data_name2}", message2) - - - - - - - - - - - - - - diff --git a/diagnostic_interface/widgets/command_output.py b/diagnostic_interface/widgets/command_output.py deleted file mode 100644 index 09a52f06..00000000 --- a/diagnostic_interface/widgets/command_output.py +++ /dev/null @@ -1,113 +0,0 @@ -import re -import shlex -from PyQt5.QtCore import Qt, pyqtSignal, QProcess -from PyQt5.QtWidgets import QTextEdit, QMessageBox - - -class CommandOutputWidget(QTextEdit): - """A QTextEdit that runs a command in a QProcess and renders ANSI output (incl. spinners).""" - _chunk_ready = pyqtSignal(str) - - def __init__(self, parent=None, max_lines=10): - super().__init__(parent) - self.setReadOnly(True) - self.setAcceptRichText(False) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.max_lines = max_lines - - self._active = False - - self._proc = QProcess(self) - self._proc.setProcessChannelMode(QProcess.MergedChannels) - self._proc.readyReadStandardOutput.connect(self._on_ready_read) - self._chunk_ready.connect(self._append_chunk) - - def deactivate(self): - self._active = False - - def activate(self): - self._active = True - - def start_cmd(self, cmd: str): - args = shlex.split(cmd) - if args: - self._proc.start(args[0], args[1:]) - - def stop(self): - """Terminate the process.""" - self._proc.terminate() - - def _on_ready_read(self): - if self._active: - raw = self._proc.readAllStandardOutput() - text = bytes(raw).decode("utf-8", errors="replace") - self._chunk_ready.emit(text) - - def _append_chunk(self, text: str): - # Normalize spinner resets and restores to carriage returns - text = re.sub(r'\x1b7|\x1b\[s', '\r', text) - text = re.sub(r'\x1b8|\x1b\[u', '', text) - - parts = re.split(r'(\r\n|\r|\n)', text) - cursor = self.textCursor() - cursor.beginEditBlock() - self.setUpdatesEnabled(False) - - for part in parts: - if part == '\r': - # Remove the previous line for spinner animation - self._remove_last_line() - else: - cursor.insertText(part) - - cursor.endEditBlock() - self.setUpdatesEnabled(True) - - self._prune_old_lines() - self._scroll_to_bottom() - - # Handle confirmation prompts ending with '(y/N) >' - if text.rstrip().endswith('(y/N) >'): - self._ask_and_respond('(y/N)') - - def _prune_old_lines(self): - doc = self.document() - overflow = doc.blockCount() - self.max_lines - if overflow > 0: - block = doc.findBlockByNumber(overflow) - cursor = self.textCursor() - cursor.setPosition(0) - cursor.setPosition(block.position(), cursor.KeepAnchor) - cursor.removeSelectedText() - - def _remove_last_line(self): - cursor = self.textCursor() - cursor.movePosition(cursor.End) - cursor.select(cursor.BlockUnderCursor) - cursor.removeSelectedText() - cursor.deletePreviousChar() - - def _ask_and_respond(self, prompt: str): - answer = QMessageBox.question( - self, - "Confirmation Required", - prompt, - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes - ) - resp = ("y\n" if answer == QMessageBox.Yes else "n\n").encode('utf-8') - self._proc.write(resp) - - def _scroll_to_bottom(self): - sb = self.verticalScrollBar() - sb.setValue(sb.maximum()) - - # Disable wheel scrolling - def wheelEvent(self, ev): - pass - - # Disable arrow/page scrolling - def keyPressEvent(self, ev): - if ev.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): - return - super().keyPressEvent(ev) From 117527fcb4553be2ca0700c4ea7a5b904dbf1572 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Tue, 30 Sep 2025 19:38:18 -0700 Subject: [PATCH 33/44] Undo changes to diagnostic_interface (1) --- .../widgets/command_output.py | 113 ++++++++++++++++ .../widgets/comnand_output.py | 128 ------------------ 2 files changed, 113 insertions(+), 128 deletions(-) create mode 100644 diagnostic_interface/widgets/command_output.py delete mode 100644 diagnostic_interface/widgets/comnand_output.py diff --git a/diagnostic_interface/widgets/command_output.py b/diagnostic_interface/widgets/command_output.py new file mode 100644 index 00000000..09a52f06 --- /dev/null +++ b/diagnostic_interface/widgets/command_output.py @@ -0,0 +1,113 @@ +import re +import shlex +from PyQt5.QtCore import Qt, pyqtSignal, QProcess +from PyQt5.QtWidgets import QTextEdit, QMessageBox + + +class CommandOutputWidget(QTextEdit): + """A QTextEdit that runs a command in a QProcess and renders ANSI output (incl. spinners).""" + _chunk_ready = pyqtSignal(str) + + def __init__(self, parent=None, max_lines=10): + super().__init__(parent) + self.setReadOnly(True) + self.setAcceptRichText(False) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.max_lines = max_lines + + self._active = False + + self._proc = QProcess(self) + self._proc.setProcessChannelMode(QProcess.MergedChannels) + self._proc.readyReadStandardOutput.connect(self._on_ready_read) + self._chunk_ready.connect(self._append_chunk) + + def deactivate(self): + self._active = False + + def activate(self): + self._active = True + + def start_cmd(self, cmd: str): + args = shlex.split(cmd) + if args: + self._proc.start(args[0], args[1:]) + + def stop(self): + """Terminate the process.""" + self._proc.terminate() + + def _on_ready_read(self): + if self._active: + raw = self._proc.readAllStandardOutput() + text = bytes(raw).decode("utf-8", errors="replace") + self._chunk_ready.emit(text) + + def _append_chunk(self, text: str): + # Normalize spinner resets and restores to carriage returns + text = re.sub(r'\x1b7|\x1b\[s', '\r', text) + text = re.sub(r'\x1b8|\x1b\[u', '', text) + + parts = re.split(r'(\r\n|\r|\n)', text) + cursor = self.textCursor() + cursor.beginEditBlock() + self.setUpdatesEnabled(False) + + for part in parts: + if part == '\r': + # Remove the previous line for spinner animation + self._remove_last_line() + else: + cursor.insertText(part) + + cursor.endEditBlock() + self.setUpdatesEnabled(True) + + self._prune_old_lines() + self._scroll_to_bottom() + + # Handle confirmation prompts ending with '(y/N) >' + if text.rstrip().endswith('(y/N) >'): + self._ask_and_respond('(y/N)') + + def _prune_old_lines(self): + doc = self.document() + overflow = doc.blockCount() - self.max_lines + if overflow > 0: + block = doc.findBlockByNumber(overflow) + cursor = self.textCursor() + cursor.setPosition(0) + cursor.setPosition(block.position(), cursor.KeepAnchor) + cursor.removeSelectedText() + + def _remove_last_line(self): + cursor = self.textCursor() + cursor.movePosition(cursor.End) + cursor.select(cursor.BlockUnderCursor) + cursor.removeSelectedText() + cursor.deletePreviousChar() + + def _ask_and_respond(self, prompt: str): + answer = QMessageBox.question( + self, + "Confirmation Required", + prompt, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + resp = ("y\n" if answer == QMessageBox.Yes else "n\n").encode('utf-8') + self._proc.write(resp) + + def _scroll_to_bottom(self): + sb = self.verticalScrollBar() + sb.setValue(sb.maximum()) + + # Disable wheel scrolling + def wheelEvent(self, ev): + pass + + # Disable arrow/page scrolling + def keyPressEvent(self, ev): + if ev.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): + return + super().keyPressEvent(ev) diff --git a/diagnostic_interface/widgets/comnand_output.py b/diagnostic_interface/widgets/comnand_output.py deleted file mode 100644 index a886b7da..00000000 --- a/diagnostic_interface/widgets/comnand_output.py +++ /dev/null @@ -1,128 +0,0 @@ -import re -from PyQt5.QtCore import Qt, pyqtSignal, QProcess -from PyQt5.QtWidgets import QTextEdit, QMessageBox -from ansi2html import Ansi2HTMLConverter -import os -import shlex -import sys - -RESET = "\x1b[0m" - - -class CommandOutputWidget(QTextEdit): - """A QTextEdit that runs a command in a QProcess and renders ANSI output (incl. spinners).""" - _chunk_ready = pyqtSignal(str) - - def __init__(self, parent=None): - super().__init__(parent) - self.setReadOnly(True) - self.setAcceptRichText(True) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - self._converter = Ansi2HTMLConverter(dark_bg=False, inline=True) - self._proc = QProcess(self) - self._proc.setProcessChannelMode(QProcess.MergedChannels) - self._proc.readyReadStandardOutput.connect(self._on_ready_read) - self._chunk_ready.connect(self._append_chunk) - - def start_in_venv( - self, - project_root: str, - script_path: str, - args: list[str] = None, - venv_subdir: str = "environment", - ): - """ - Activate the venv under `project_root/venv_subdir`, then exec: - python {script_path} {args...} - in a PTY so spinners (save/restore ANSI) work. - """ - args = args or [] - - activate = os.path.join(project_root, venv_subdir, "bin", "activate") - script = os.path.join(project_root, script_path) - qa = shlex.quote(activate) - qs = shlex.quote(script) - qargs = " ".join(shlex.quote(a) for a in args) - - # the “inner” command we want to run under PTY - inner = f"source {qa} && exec python {qs}" + (f" {qargs}" if qargs else "") - - if sys.platform.startswith("linux"): - wrapper = f"script -q -e -c {shlex.quote(inner)} /dev/null" - else: - # on macOS (BSD script) there's no -c, so pass the command as args - # -q: quiet, no start/end messages - # -e: return child exit code (BSD supports -e) - # file: /dev/null - # command...: bash -lc inner - wrapper = f"script -q -e /dev/null bash -lc {shlex.quote(inner)}" - - self._proc.setWorkingDirectory(project_root) - self._proc.start("bash", ["-lc", wrapper]) - - def stop(self): - """Terminate the process.""" - self._proc.terminate() - - def _on_ready_read(self): - raw = self._proc.readAllStandardOutput() - text = bytes(raw).decode("utf-8", errors="replace") - self._chunk_ready.emit(text) - - def _append_chunk(self, text: str): - # 1) Convert ANSI save/restore → '\r' (and drop restore) - text = re.sub(r'\x1b7|\x1b\[s', '\r', text) - text = re.sub(r'\x1b8|\x1b\[u', '', text) - - # 2) Split on CR/LF and render ANSI → HTML - parts = re.split(r"(\r\n|\r|\n)", text) - for part in parts: - if part == "\r": - self._remove_last_line() - elif part in ("\n", "\r\n"): - self.insertHtml("
") - else: - html = self._converter.convert(RESET + part + RESET, full=False) - self.insertHtml(html) - - self._scroll_to_bottom() - - # 3) If the last line is exactly "(y/N) >", prompt the user - lines = text.splitlines() - if lines and re.search(r"\(y/N\)\s*>\s*$", lines[-1]): - prompt = re.sub(r"\s*\(y/N\)\s*>\s*$", "(y/N)", lines[-1]) - self._ask_and_respond(prompt) - - def _ask_and_respond(self, prompt_text: str): - answer = QMessageBox.question( - self, - "Confirmation Required", - prompt_text, - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes - ) - resp = ("y\n" if answer == QMessageBox.Yes else "n\n").encode("utf-8") - self._proc.write(resp) - - def _remove_last_line(self): - """Delete the last block (line) in the QTextEdit.""" - cursor = self.textCursor() - cursor.movePosition(cursor.End) - cursor.select(cursor.BlockUnderCursor) - cursor.removeSelectedText() - cursor.deletePreviousChar() - - def _scroll_to_bottom(self): - sb = self.verticalScrollBar() - sb.setValue(sb.maximum()) - - # disable wheel scrolling if you like - def wheelEvent(self, ev): - pass - - def keyPressEvent(self, ev): - # disable up/down arrow scrolling - if ev.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): - return - super().keyPressEvent(ev) From 19aebe64914e6fae4e5784053a85c51deadaf40e Mon Sep 17 00:00:00 2001 From: sanar Date: Tue, 30 Sep 2025 19:51:58 -0700 Subject: [PATCH 34/44] - --- simulation/config/BrightSide.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulation/config/BrightSide.toml b/simulation/config/BrightSide.toml index 95229006..b7369100 100644 --- a/simulation/config/BrightSide.toml +++ b/simulation/config/BrightSide.toml @@ -43,3 +43,5 @@ drag_coefficient = 1.166e-01 [motor_config.AdvancedMotor] cornering_coefficient = 1.0 + +[motor_config.Aeroshell] From 39496a67047d53ce4b4c8abef6bd8f28c1535bfc Mon Sep 17 00:00:00 2001 From: sanar Date: Tue, 30 Sep 2025 19:59:42 -0700 Subject: [PATCH 35/44] - --- simulation/config/BrightSide.toml | 5 +++++ simulation/config/models/_car.py | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/simulation/config/BrightSide.toml b/simulation/config/BrightSide.toml index b7369100..3495996a 100644 --- a/simulation/config/BrightSide.toml +++ b/simulation/config/BrightSide.toml @@ -45,3 +45,8 @@ drag_coefficient = 1.166e-01 cornering_coefficient = 1.0 [motor_config.Aeroshell] +drag_lookup = {0:0.137601477, 18: 0.2335363083, 36: 0.5965968882, 54: 1.224448936, 72: 1.861868971, 90: 2.38148208, 108: 2.073196244, 126: 1.587471653, 144: 0.5564901716, 162: 0.2141437734, 180: 0.1386601712 } +down_lookup = {0:0.37526598, 18: 0.3378390168, 36: 0.576439927, 54: 0.8675973423, 72: 1.19551954, 90: 2.683269654, 108: 2.223002744, 126: 1.581662338, 144: 0.17190782, 162: 0.1882638387, 180: 0.2153506426 } + + + diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index fbfb8609..d4a8a186 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -109,7 +109,6 @@ class AeroshellConfig(Config): model_config = ConfigDict(frozen=True) drag_lookup: dict[float:float] #lookup table that corresponds angles to drag force, computed by a CFD down_lookup: dict[float:float] #lookup table that corresponds angles to down force, computed by a CFD - wind_reference_speed: float # a reference wind speed in order to scale aerodynamic force calculations class RegenConfig(Config): """ From 0d4b15e81f6d6b2c06fb4482e40b9b84705b2ffb Mon Sep 17 00:00:00 2001 From: skrifana Date: Tue, 30 Sep 2025 20:20:11 -0700 Subject: [PATCH 36/44] undo unnecessary changes --- simulation/model/Simulation.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index 89f16888..f5ccfee5 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -236,15 +236,14 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: # ----- Array calculations ----- - cumulative_delta_energy = np.cumsum(self.delta_energy) + battery_variables_array = self.model.battery.update_array( cumulative_delta_energy ) # stores the battery SOC at each time step self.state_of_charge = battery_variables_array[0] - self.state_of_charge[np.abs(self.state_of_charge) < 1e-03] = 0 - self.raw_soc = self.model.battery.get_raw_soc(np.cumsum(self.delta_energy)) + # # This functionality may want to be removed in the future (speed array gets mangled when SOC <= 0) # self.speed_kmh = np.logical_and(self.not_charge, self.state_of_charge) * self.speed_kmh @@ -253,15 +252,11 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: np.logical_and(self.tick_array, self.speed_kmh) * self.model.simulation_dt ) - self.final_soc = self.state_of_charge[-1] * 100 + 0.0 - - self.distance = self.speed_kmh * (self.time_in_motion / 3600) - self.distances = np.cumsum(self.distance) + # Car cannot exceed Max distance, and it is not in motion after exceeded self.distances = self.distances.clip(0, self.max_route_distance / 1000) - self.map_data_indices = get_map_data_indices(self.closest_gis_indices) self.distance_travelled = self.distances[-1] From b3215afab6616242cf4826b09cb38e7ddbc21ce8 Mon Sep 17 00:00:00 2001 From: skrifana Date: Thu, 2 Oct 2025 11:33:29 -0700 Subject: [PATCH 37/44] Update _car.py --- simulation/config/models/_car.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index d4a8a186..df09ed67 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -100,8 +100,7 @@ class BasicMotorConfig(MotorConfig): class AdvancedMotorConfig(MotorConfig): cornering_coefficient: float - - + class AeroshellConfig(Config): """ Configuration object describing the aerodynamics forces (specifically drag and downforce) of a vehicle. @@ -112,8 +111,6 @@ class AeroshellConfig(Config): class RegenConfig(Config): """ -class RegenConfig(Config): - Configuration object describing the regenerative braking systems of a vehicle. """ From 8dcb18dde656e27053aa792c85014be65f3a4373 Mon Sep 17 00:00:00 2001 From: skrifana Date: Thu, 2 Oct 2025 11:35:26 -0700 Subject: [PATCH 38/44] Update Model.py --- simulation/model/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulation/model/Model.py b/simulation/model/Model.py index 7a92c3b2..90b8c5ff 100644 --- a/simulation/model/Model.py +++ b/simulation/model/Model.py @@ -327,4 +327,4 @@ def get_driving_time_divisions(self) -> int: ) .sum() .astype(int) - ) \ No newline at end of file + ) From 11b1d3431bc3ecf817f7d75171589cb02610b61b Mon Sep 17 00:00:00 2001 From: skrifana Date: Thu, 2 Oct 2025 11:38:26 -0700 Subject: [PATCH 39/44] Update ModelBuilder.py --- simulation/model/ModelBuilder.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/simulation/model/ModelBuilder.py b/simulation/model/ModelBuilder.py index 51e4e850..b30fbff8 100644 --- a/simulation/model/ModelBuilder.py +++ b/simulation/model/ModelBuilder.py @@ -47,6 +47,7 @@ class ModelBuilder: This class follows the fluid builder pattern, such that its methods return itself allowing for operations like, >>> ModelBuilder().set_environment_config(environment_config).set_initial_conditions(initial_conditions).compile() + Once all configuration has been set, `ModelBuilder` can be compiled, and then a `Model` acquired from it with `get`. """ @@ -122,11 +123,11 @@ def set_hyperparameters(self, hyperparameters: SimulationHyperparametersConfig): return self def set_environment_config( - self, - environment_config: EnvironmentConfig, - rebuild_weather_cache: bool = False, - rebuild_competition_cache: bool = False, - rebuild_route_cache: bool = False, + self, + environment_config: EnvironmentConfig, + rebuild_weather_cache: bool = False, + rebuild_competition_cache: bool = False, + rebuild_route_cache: bool = False, ): """ Set the environment configuration of the `Model` to be built. @@ -304,14 +305,13 @@ def _set_route_data(self): self.origin_coord = route.coords[0] self.dest_coord = route.coords[-1] self.waypoints = route.coords[ - 1:-1 - ] # Get all coords between first and last coordinate + 1:-1 + ] # Get all coords between first and last coordinate def _set_weather_data(self): environment_config = self._environment_config - environment_hash = ModelBuilder._truncate_hash( - hash(environment_config) + hash(environment_config.weather_query_config)) + environment_hash = ModelBuilder._truncate_hash(hash(environment_config) + hash(environment_config.weather_query_config)) weather_data_path = WeatherPath / environment_hash weather_query_config = environment_config.weather_query_config @@ -486,5 +486,5 @@ def get(self) -> Model: max_acceleration=self.max_acceleration, max_deceleration=self.max_deceleration, start_time=self.start_time, - num_laps=self.num_laps + num_laps = self.num_laps ) From f229a8bc727bba3c08a940b8463e852c6df79132 Mon Sep 17 00:00:00 2001 From: skrifana Date: Thu, 2 Oct 2025 11:42:30 -0700 Subject: [PATCH 40/44] Update Simulation.py --- simulation/model/Simulation.py | 60 ++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index f5ccfee5..b2751693 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -77,16 +77,16 @@ def __init__(self, model): self.map_data_indices = None self.wind_attack_angles = None - def run_simulation_calculations(self, speed_kmh: NDArray) -> None: + def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) -> None: """ Simulate the model by sequentially running calculations for the entire model duration at once. - To begin, we use the driving speeds array to obtain the theoretical position of the car at every tick. + To begin, we use the driving speeds_directory array to obtain the theoretical position of the car at every tick. Then, we map the position of the car at every tick to a GIS coordinate and to a Weather coordinate. Next, we use the GIS coordinates to calculate gradients, vehicle bearings, and we also determine the time in which we arrive at each position. Then, we use the time and Weather coordinate to map each tick to a weather - forecast. From the weather, we can calculate wind speeds, cloud cover, and estimate solar irradiance. + forecast. From the weather, we can calculate wind speeds_directory, cloud cover, and estimate solar irradiance. From the aforementioned calculations, we can determine the energy needed for the motor, and the energy that the solar arrays will collect; from those two, we can determine the delta energy at every tick. Then, we use the delta energy to determine how much energy we are drawing or storing from/into the battery @@ -116,10 +116,15 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: closest_weather_indices is a 1:1 mapping between a weather condition, and its closest point on a map. """ - self.closest_gis_indices = self.model.gis.calculate_closest_gis_indices( - self.distances + track_speeds_normalized = (track_speeds - np.mean(track_speeds)) / 2 + self.closest_gis_indices, self.speed_kmh = self.model.gis.calculate_speeds_and_position( + self.speed_kmh, + track_speeds_normalized, + self.model.simulation_dt ) + self.speed_kmh = np.clip(self.speed_kmh, a_min=0.0, a_max=None) + self.model.meteorology.spatially_localize( self.cumulative_distances, simplify_weather=True ) @@ -132,7 +137,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.max_route_distance = self.cumulative_distances[-1] self.route_length = ( - self.max_route_distance / 1000.0 + self.max_route_distance / 1000.0 ) # store the route length in kilometers # Array of elevations at every route point @@ -156,8 +161,9 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.time_zones = self.model.gis.get_time_zones(self.closest_gis_indices) # Local times in UNIX timestamps + local_time_of_initialization = self.model.time_of_initialization + self.time_zones[0] local_times = adjust_timestamps_to_local_times( - self.timestamps, self.model.time_of_initialization, self.time_zones + self.timestamps, local_time_of_initialization, self.time_zones ) # Get the weather at every location @@ -168,7 +174,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.absolute_wind_speeds = self.model.meteorology.wind_speed self.wind_directions = self.model.meteorology.wind_direction - # Get the wind speeds at every location + # Get the wind speeds_directory at every location self.wind_speeds = get_array_directional_wind_speed( self.gis_vehicle_bearings, self.absolute_wind_speeds, self.wind_directions ) @@ -185,7 +191,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.gis_route_elevations_at_each_tick, ) - # TLDR: we have now obtained solar irradiances, wind speeds, and gradients at each tick + # TLDR: we have now obtained solar irradiances, wind speeds_directory, and gradients at each tick # ----- Energy Calculations ----- @@ -193,7 +199,11 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.model.simulation_dt ) + coords = self.model.gis.get_path()[:self.model.gis.num_unique_coords] + coords_at_each_tick = coords[self.closest_gis_indices] + self.motor_consumed_energy = self.model.motor.calculate_energy_in( + self.speed_kmh, self.gradients, self.wind_speeds, self.model.simulation_dt, coords_at_each_tick self.speed_kmh, self.gradients, self.drag_force, self.down_force, self.model.simulation_dt ) @@ -205,8 +215,8 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.speed_kmh, self.gis_route_elevations_at_each_tick, 0.0, 10000.0 ) - self.not_charge = self.model.race.charging_boolean[self.model.start_time:] - self.not_race = self.model.race.driving_boolean[self.model.start_time:] + self.not_charge = self.model.race.charging_boolean[self.model.start_time :] + self.not_race = self.model.race.driving_boolean[self.model.start_time :] if self.model.simulation_dt != 1: self.not_charge = self.not_charge[:: self.model.simulation_dt] @@ -236,29 +246,38 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: # ----- Array calculations ----- - battery_variables_array = self.model.battery.update_array( - cumulative_delta_energy + delta_energy_array=self.delta_energy, + tick=self.model.simulation_dt ) # stores the battery SOC at each time step self.state_of_charge = battery_variables_array[0] - + self.raw_soc = self.state_of_charge + self.state_of_charge[np.abs(self.state_of_charge) < 1e-03] = 0 # # This functionality may want to be removed in the future (speed array gets mangled when SOC <= 0) # self.speed_kmh = np.logical_and(self.not_charge, self.state_of_charge) * self.speed_kmh self.time_in_motion = ( - np.logical_and(self.tick_array, self.speed_kmh) * self.model.simulation_dt + np.logical_and(self.tick_array, self.speed_kmh) * self.model.simulation_dt ) - + self.final_soc = self.state_of_charge[-1] * 100 + 0.0 + + self.distance = self.speed_kmh * (self.time_in_motion / 3600) + self.distances = np.cumsum(self.distance) - # Car cannot exceed Max distance, and it is not in motion after exceeded - self.distances = self.distances.clip(0, self.max_route_distance / 1000) + self.map_data_indices = get_map_data_indices(self.closest_gis_indices) + battery_dead_indices = np.where(self.state_of_charge < 0)[0] - self.distance_travelled = self.distances[-1] + if len(battery_dead_indices) > 0: + stop_index = battery_dead_indices[0] + else: + stop_index = -1 + + self.distance_travelled = self.distances[stop_index] if self.distance_travelled >= self.route_length: self.time_taken = self.timestamps[ @@ -270,7 +289,7 @@ def run_simulation_calculations(self, speed_kmh: NDArray) -> None: self.calculations_have_happened = True def get_results( - self, requested_properties: Union[list, str] + self, requested_properties: Union[list, str] ) -> Union[list, np.ndarray, float]: """ @@ -337,7 +356,6 @@ def get_results( "drag_force": self.drag_force, "down_force": self.down_force, "wind_attack_angles": self.wind_attack_angles, - } if "default" in requested_properties or requested_properties == "default": From 0b0e120bfe5470d5410d07499b1ae34811df2372 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Sat, 4 Oct 2025 11:08:58 -0700 Subject: [PATCH 41/44] Fix a few things --- simulation/config/BrightSide.toml | 8 ++++---- simulation/config/models/_car.py | 8 +++++--- simulation/model/Simulation.py | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/simulation/config/BrightSide.toml b/simulation/config/BrightSide.toml index 3495996a..ab9b9e2c 100644 --- a/simulation/config/BrightSide.toml +++ b/simulation/config/BrightSide.toml @@ -33,7 +33,7 @@ max_acceleration = 6 max_deceleration = 6 [motor_config] -motor_type = "AdvancedMotor" +motor_type = "BasicMotor" road_friction = 2.340e-02 tire_radius = 0.2032 vehicle_frontal_area = 1.1853 @@ -44,9 +44,9 @@ drag_coefficient = 1.166e-01 [motor_config.AdvancedMotor] cornering_coefficient = 1.0 -[motor_config.Aeroshell] -drag_lookup = {0:0.137601477, 18: 0.2335363083, 36: 0.5965968882, 54: 1.224448936, 72: 1.861868971, 90: 2.38148208, 108: 2.073196244, 126: 1.587471653, 144: 0.5564901716, 162: 0.2141437734, 180: 0.1386601712 } -down_lookup = {0:0.37526598, 18: 0.3378390168, 36: 0.576439927, 54: 0.8675973423, 72: 1.19551954, 90: 2.683269654, 108: 2.223002744, 126: 1.581662338, 144: 0.17190782, 162: 0.1882638387, 180: 0.2153506426 } +[aeroshell_config] +drag_lookup = {0 = 0.137601477, 18 = 0.2335363083, 36 = 0.5965968882, 54= 1.224448936, 72= 1.861868971, 90= 2.38148208, 108= 2.073196244, 126= 1.587471653, 144= 0.5564901716, 162= 0.2141437734, 180= 0.1386601712 } +down_lookup = {0=0.37526598, 18= 0.3378390168, 36= 0.576439927, 54= 0.8675973423, 72= 1.19551954, 90= 2.683269654, 108= 2.223002744, 126= 1.581662338, 144= 0.17190782, 162= 0.1882638387, 180= 0.2153506426 } diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index df09ed67..8932fa22 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -100,14 +100,16 @@ class BasicMotorConfig(MotorConfig): class AdvancedMotorConfig(MotorConfig): cornering_coefficient: float - + + class AeroshellConfig(Config): """ Configuration object describing the aerodynamics forces (specifically drag and downforce) of a vehicle. """ model_config = ConfigDict(frozen=True) - drag_lookup: dict[float:float] #lookup table that corresponds angles to drag force, computed by a CFD - down_lookup: dict[float:float] #lookup table that corresponds angles to down force, computed by a CFD + drag_lookup: dict[float, float] #lookup table that corresponds angles to drag force, computed by a CFD + down_lookup: dict[float, float] #lookup table that corresponds angles to down force, computed by a CFD + class RegenConfig(Config): """ diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index b2751693..d0849329 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -203,7 +203,6 @@ def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) coords_at_each_tick = coords[self.closest_gis_indices] self.motor_consumed_energy = self.model.motor.calculate_energy_in( - self.speed_kmh, self.gradients, self.wind_speeds, self.model.simulation_dt, coords_at_each_tick self.speed_kmh, self.gradients, self.drag_force, self.down_force, self.model.simulation_dt ) From d514438b39bb818484b26fc66ec5fdcff66068db Mon Sep 17 00:00:00 2001 From: skrifana Date: Sat, 4 Oct 2025 14:03:17 -0700 Subject: [PATCH 42/44] Update Simulation.py --- simulation/model/Simulation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index d0849329..97683a4b 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -178,6 +178,8 @@ def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) self.wind_speeds = get_array_directional_wind_speed( self.gis_vehicle_bearings, self.absolute_wind_speeds, self.wind_directions ) + #get the wind attack angles + self.wind_attack_angles = self.model.meteorology.wind_direction - (self.gis_vehicle_bearings + 180) #convert azimuthal angle to meteorological convention # with calculated wind_speeds, we can now calculate (aerodynamic) drag and down forces in order to pass into motor model calculations self.drag_force = self.model.aeroshell.calculate_drag(self.wind_speeds, self.wind_attack_angles, self.speed_kmh/3.6) From 270b2753ec720585e679fee832e1cf28a312f5c1 Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Sat, 4 Oct 2025 14:12:38 -0700 Subject: [PATCH 43/44] Add physics --- pyproject.toml | 2 +- uv.lock | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9e7c041f..df87d785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "tomli>=2.2.1", "tomli-w>=1.2.0", "qt-material>=2.17", - "ubc-solar-physics==1.8.2", + "ubc-solar-physics==1.9.1", ] [project.urls] diff --git a/uv.lock b/uv.lock index f62c33d0..4c1d30ec 100644 --- a/uv.lock +++ b/uv.lock @@ -1706,8 +1706,10 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/5e/4d/8694391d6b014bc49f8fd2eb8c05c94526b36fba8dc76d439f3d51948e46/timezonefinder-6.5.9.tar.gz", hash = "sha256:0d84c792a499fd098a35c701c3e3293423ba8d45c81b3eecd7c7cb72c7f1f415", size = 51435008 } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/aa/57fb0cb5b739a12ce6fc401b9d1e5d0b2ee092449c1d3ae14aa815409062/timezonefinder-6.5.9-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9a0caf638f43b6dd9980731d1424500c7a6a5048db808aad7032560df5c663", size = 51443482 }, + { url = "https://files.pythonhosted.org/packages/70/de/c96b04236e9aa7902f4e416c5989fd019efb0e09e69f40cedb1e6e7bcd26/timezonefinder-6.5.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039919be603809b98f3d2b6a4ed33110ac7cc6bbd507959531401c91f3469fb", size = 51444624 }, { url = "https://files.pythonhosted.org/packages/2b/b9/c8a05e55096deea0b789d649da9729312ab1d97e3c52b0f0254c18f94cc7/timezonefinder-6.5.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c5347c4a73b40af4867a2946a5172ec68b644e7036888b9b5a0e568499bfc0f3", size = 51445521 }, { url = "https://files.pythonhosted.org/packages/53/6c/498b3f453b15d7ecf2ecf54a8f1cc7fbd1d3c3eed86a5182081ebc17b180/timezonefinder-6.5.9-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d1f30b550c24598459643285ef0f5469524ad7a32dd854199e7a2a463600ff", size = 51443403 }, + { url = "https://files.pythonhosted.org/packages/4e/5a/dfcd510203ccaf01099394850df7ee3733afffa57bfca3499b7f39484edf/timezonefinder-6.5.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb175d470e6f6abbeb13bd3a638bb1a0aa4124e1a93d040e5d933cf69076c60", size = 51444549 }, { url = "https://files.pythonhosted.org/packages/8b/16/6ae92a93dd703c0485e4c0a76e28bc829f9c7eba548a1b2e17fb3342e4c3/timezonefinder-6.5.9-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4f36877ad3a988f329cbd3f04f7cadc56dce073ad4a7bda7397f46ac2a61f9a4", size = 51445432 }, ] @@ -1842,7 +1844,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/6b/1c/614e7daf9845006ad [[package]] name = "ubc-solar-physics" -version = "1.8.2" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile" }, @@ -1881,18 +1883,18 @@ dependencies = [ { name = "urllib3" }, { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/6b/77e420561b851f545cfa1d0e270b5cfc1b3da00a6295990a1c3244255aed/ubc_solar_physics-1.8.2.tar.gz", hash = "sha256:3868f1033240ea8369e887411f0b0e3383178d8f6c4cd2dfb29c5ece67a263c2", size = 22460102 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6d/ca790af6b0c9293c04a6bf78d6fb001f38568dd3240bbaa69e0587093d36/ubc_solar_physics-1.9.1.tar.gz", hash = "sha256:594d11cdb3e92092834a9288a3c353dcf653cc717edb04e30ac2930f1789359d", size = 22509102 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/98/f796f5531bbcfdcdf12ffe58139580781b7c0535d1795bd1bc9283027de1/ubc_solar_physics-1.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8f835438f9a5b21608413df8318a7a3789e690518d4adbc19f9488c6580a9bf5", size = 340184 }, - { url = "https://files.pythonhosted.org/packages/37/15/16acdc473ccf407a8c592d1bbfb36fa9a52114a088d979d6ee3afcc67009/ubc_solar_physics-1.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a99bef3278223f3aca844070a16c82e5e73b65ddf8882b165921e7321d4e7c6", size = 331885 }, - { url = "https://files.pythonhosted.org/packages/e7/d8/d251ec15b054ebf4a3fe142cafc1e81c532cf5cf121085667ea6bc9c0c08/ubc_solar_physics-1.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adc4e225939f0036f3729ee9b056deed64060bee8703261fd6375a8d0660fda", size = 367762 }, - { url = "https://files.pythonhosted.org/packages/2b/26/1c614bc80c25f9ddb9d8807edd041787d62e7a4465011ff57cc22292c8ff/ubc_solar_physics-1.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c7d820fc544e5e96febe440cc83ae50305fb4350848fc849a1af8274a77520", size = 383224 }, - { url = "https://files.pythonhosted.org/packages/ff/9a/bb58d5beec5078bfa7e1599f3f6bd0e0fabecd3117aabf7750499a3d8ea1/ubc_solar_physics-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:eb0bbf066b492c7b9340c8331c46d092b974c9c1a659c976493669d30902bbae", size = 225132 }, - { url = "https://files.pythonhosted.org/packages/0c/42/384f3f9154c1e8b84a7a5cceb62348fc83056672ba96a788dcdf60cb7976/ubc_solar_physics-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ceae39368db703dd46150d99a38f8ca5ec8172a311cf7046e6e587490551fb1", size = 340184 }, - { url = "https://files.pythonhosted.org/packages/f6/90/932e1c328a5cb2ee2e5748ae16759ac523ae886e342b1cb20632c9b0631a/ubc_solar_physics-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72e96c11179013687e98353d82badf26f2a01eceb6b427f290b56fd32098ba07", size = 331887 }, - { url = "https://files.pythonhosted.org/packages/d1/fc/e4966d56fa650372b48b1c54cb4e0905a875a9c5d6afecf925863d392d35/ubc_solar_physics-1.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f08a3072bcc90b1f14f9b4f4a190bb18f2e5b63961e6774c8a135543d313132", size = 367763 }, - { url = "https://files.pythonhosted.org/packages/be/fb/e924aa61b035a567ce5f006b8a0e9a52c8851479c2234b30358890fe1541/ubc_solar_physics-1.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e6791e99aae088a7bdba52241d572daa3ca9a124a4f1fc724c04258e283d839", size = 383226 }, - { url = "https://files.pythonhosted.org/packages/92/68/593bac4c33cf2162d2977dad98bfbcaa47959bed8c696870f37bf0e7598e/ubc_solar_physics-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb1706181c9621611c8649f2ba3aab8510b538c5a6e0d8190ef3e82b3210916c", size = 225134 }, + { url = "https://files.pythonhosted.org/packages/48/0b/a603789d8bb98e94b9775d9dc3061605d8fa48f3260d748b135e9ae47bd5/ubc_solar_physics-1.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2c80e0067dc7e4cdc68ed1c1fdbbe7fd39c12358c93d7d4667f67b76cc4eeac3", size = 387180 }, + { url = "https://files.pythonhosted.org/packages/0e/05/dbfbfe49120a232c41511b77fb6af02b65179af26f3d69043b24a9737ff4/ubc_solar_physics-1.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:897bf1df66b50e632d2cc1569a24faa7849b1ce55b2e44e9b21c9bfc58c8c0a9", size = 377756 }, + { url = "https://files.pythonhosted.org/packages/8f/08/afa4058cab73cfab01b465968b1d79c3b36df1a6da9880540249d17bd034/ubc_solar_physics-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fa00609f2271732fb6f80f6309aa55a0eeb6f97625bd7a62a63eaf706171d8", size = 414809 }, + { url = "https://files.pythonhosted.org/packages/46/90/855c7e28ddf64346d69ea98ebb34d4c4e126b7bbfebbf2f4464debd34a6b/ubc_solar_physics-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97ee3edbb3fec08e1edab764ca39c3718cf77457a47812f809c38433c5823fec", size = 429296 }, + { url = "https://files.pythonhosted.org/packages/82/d1/ba0de3f95a6d1bddce0eedccb6600aa8a73a206c628d48ca8994d42f4c43/ubc_solar_physics-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:16177e72589965a062bcdc57131bfa32def173377a962edbddaa5ffcfaa05a68", size = 270643 }, + { url = "https://files.pythonhosted.org/packages/c9/9b/f01dd2d00964266ecd1db89cca2131035fc4655051e35f4f5a58afc529ce/ubc_solar_physics-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2f16994df9b6c937f29b1a70a0c609af9b1f1f93ef383dfd1ed5da88ee9e6cd", size = 387180 }, + { url = "https://files.pythonhosted.org/packages/a2/6e/170c96d2bc2a279b590f82a9308175f188fb791008761d54301013e9fcb8/ubc_solar_physics-1.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:124707ea07e91aff91ccc20bd910a208140cdd00aee675cfe1b6c9e2efca3ba4", size = 377755 }, + { url = "https://files.pythonhosted.org/packages/3c/39/2797b662d0dac4e9d5cbd876d57054f8246b74398ed6cc1ddf85bc06f967/ubc_solar_physics-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2081c3cfdfed6ae98f1efef862f20dd2b89f18b0531f274f9d4fe65ad297f912", size = 414810 }, + { url = "https://files.pythonhosted.org/packages/90/b5/e46d0fd09e7b26924bdd1b39e26b0663a13f35d9fbcd4e6a239df32c29cb/ubc_solar_physics-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a09e418c09c4371e65e3d46fca239bbde613559245d77527d9745cf9bee55d21", size = 429297 }, + { url = "https://files.pythonhosted.org/packages/83/a6/b761f242388209e4f4825065738a12db8166806a8b74ca1addf7656b229e/ubc_solar_physics-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ce7a018abd8ac4c72a365cd347907a2c04c1ca3074c4b31ed900bd9e5beec85", size = 270647 }, ] [[package]] @@ -1991,7 +1993,7 @@ requires-dist = [ { name = "tqdm", specifier = ">=4.64.0" }, { name = "tzlocal", specifier = ">=5.2" }, { name = "ubc-solar-data-tools", specifier = "==1.9.1" }, - { name = "ubc-solar-physics", specifier = "==1.8.2" }, + { name = "ubc-solar-physics", specifier = "==1.9.1" }, ] provides-extras = ["dev"] From 2b1d498b8d89e2089e993e4ad93808256ece183f Mon Sep 17 00:00:00 2001 From: Joshua Riefman Date: Sat, 4 Oct 2025 14:16:24 -0700 Subject: [PATCH 44/44] Changes from Ruff linter --- diagnostic_interface/canvas/realtime_canvas.py | 1 - diagnostic_interface/widgets/realtime_map_widget.py | 3 ++- micro_strategy/__init__.py | 3 +++ prescriptive_interface/prescriptive_ui.py | 4 ++-- simulation/model/Simulation.py | 3 --- simulation/optimization/genetic.py | 5 +---- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/diagnostic_interface/canvas/realtime_canvas.py b/diagnostic_interface/canvas/realtime_canvas.py index 705e6643..0529681b 100644 --- a/diagnostic_interface/canvas/realtime_canvas.py +++ b/diagnostic_interface/canvas/realtime_canvas.py @@ -6,7 +6,6 @@ from diagnostic_interface import settings from PyQt5.QtWidgets import QMessageBox import mplcursors -from datetime import timezone from dateutil import tz diff --git a/diagnostic_interface/widgets/realtime_map_widget.py b/diagnostic_interface/widgets/realtime_map_widget.py index fdf407a3..e9acdc29 100644 --- a/diagnostic_interface/widgets/realtime_map_widget.py +++ b/diagnostic_interface/widgets/realtime_map_widget.py @@ -45,7 +45,8 @@ def update_map(self, vertex_values: NDArray[float], latitudes: NDArray, longitud touchZoom=False, ) - get_coord = lambda x: [latitudes[x], longitudes[x]] + def get_coord(x): + return [latitudes[x], longitudes[x]] for i, (latitude, longitude, vertex_value) in enumerate(zip(latitudes, longitudes, vertex_values)): color = mcolors.to_hex(cmap(norm(vertex_value))) diff --git a/micro_strategy/__init__.py b/micro_strategy/__init__.py index a22a5655..f9b187c1 100644 --- a/micro_strategy/__init__.py +++ b/micro_strategy/__init__.py @@ -3,3 +3,6 @@ OptimizedSpeedsPath = Path(__file__).parent / "optimization_results" / "optimized_speeds_filtered.npy" +__all__ = [ + "run_micro_simulation" +] \ No newline at end of file diff --git a/prescriptive_interface/prescriptive_ui.py b/prescriptive_interface/prescriptive_ui.py index 9c4fd4d8..b714ad9a 100644 --- a/prescriptive_interface/prescriptive_ui.py +++ b/prescriptive_interface/prescriptive_ui.py @@ -11,18 +11,18 @@ from diagnostic_interface.widgets import SplashOverlay from simulation.cmd.run_simulation import get_default_settings from simulation.config import SimulationReturnType, SimulationHyperparametersConfig, InitialConditions -from pathlib import Path from PyQt5.QtCore import QTimer from PyQt5.QtCore import Qt from qt_material import apply_stylesheet -config_dir = Path(__file__).parent.parent / "simulation" / "config" from pathlib import Path from prescriptive_interface import (SimulationTab, OptimizationTab, HtmlViewerTab, SimulationSettingsDict, OptimizationThread, SimulationThread, MutableInitialConditions, InitialConditionsDialog) +config_dir = Path(__file__).parent.parent / "simulation" / "config" + my_speeds_dir = Path("prescriptive_interface/speeds_directory") my_speeds_dir.mkdir(parents=True, exist_ok=True) # Create it if it doesn't exist diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index 97683a4b..2963d738 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -201,9 +201,6 @@ def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) self.model.simulation_dt ) - coords = self.model.gis.get_path()[:self.model.gis.num_unique_coords] - coords_at_each_tick = coords[self.closest_gis_indices] - self.motor_consumed_energy = self.model.motor.calculate_energy_in( self.speed_kmh, self.gradients, self.drag_force, self.down_force, self.model.simulation_dt ) diff --git a/simulation/optimization/genetic.py b/simulation/optimization/genetic.py index fe12b697..5d883982 100644 --- a/simulation/optimization/genetic.py +++ b/simulation/optimization/genetic.py @@ -306,9 +306,6 @@ def __init__( # Bind the value of each gene to be between 0 and 1 as chromosomes should be normalized. gene_space = {"low": 0.0, "high": 1.0} - # Add a time delay between generations (used for debug purposes) - delay_after_generation = 0.0 - # Store diversity of generation per optimization iteration self.diversity = [] @@ -392,7 +389,7 @@ def get_initial_population( """ - population_file = population_directory / "initial_population.npz" + population_file = "initial_population.npz" arrays_from_cache = 0 new_initial_population = None