From ebe989951e1d46e8bec878647944d58060a655f7 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Tue, 29 Oct 2024 16:54:44 +0100 Subject: [PATCH 1/2] feat: request assets in a separate thread --- sepal_ui/scripts/thread_controller.py | 117 ++++++++++++++++++++++++++ sepal_ui/sepalwidgets/inputs.py | 10 ++- 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 sepal_ui/scripts/thread_controller.py diff --git a/sepal_ui/scripts/thread_controller.py b/sepal_ui/scripts/thread_controller.py new file mode 100644 index 00000000..b6e5bbe8 --- /dev/null +++ b/sepal_ui/scripts/thread_controller.py @@ -0,0 +1,117 @@ +import threading +from typing import Callable, Optional, List +import sepal_ui.sepalwidgets as sw + + +class TaskController: + def __init__( + self, + function: Callable, + callback: Optional[Callable] = None, + alert: Optional[sw.Alert] = None, + start_button: Optional[sw.Btn] = None, + stop_button: Optional[sw.Btn] = None, + disable_components: Optional[List] = None, + *function_args, + **function_kwargs, + ): + """ + Initializes the TaskController. + + Args: + function: The long-running function to execute. + callback: A function to call with the result after the task completes. + alert: An optional alert widget for displaying messages. + start_button: An optional button to start the task. + stop_button: An optional button to stop the task. + disable_components: A list of components to disable while the task is running. + *function_args: Positional arguments for the function. + **function_kwargs: Keyword arguments for the function. + """ + self.alert = alert + self.function = function + self.function_args = function_args + self.function_kwargs = function_kwargs + self.callback = callback + self.disable_components = disable_components or [] + + self.start_button = start_button + self.stop_button = stop_button + + self.task_thread = None + self.stop_event = threading.Event() + + # Set up event handlers if buttons are provided + if self.start_button is not None: + self.start_button.on_event("click", self.start_task) + if self.stop_button is not None: + self.stop_button.on_event("click", self.stop_task) + + def start_task(self, *args): + """Starts the long-running task in a separate thread.""" + print("Starting task...") + if self.task_thread is not None and self.task_thread.is_alive(): + # Task is already running + return + + try: + # Clear the stop event + self.stop_event.clear() + + # Disable components + self.set_components_enabled(False) + + # Reset the alert if provided + if self.alert is not None: + self.alert.reset() + + # Start the task thread + self.task_thread = threading.Thread(target=self._run_task) + self.task_thread.start() + + print("Task started.") + except Exception as e: + print(f"Exception in start_task: {e}") + + def _run_task(self): + """Runs the long-running task and handles completion.""" + try: + if self.start_button is not None: + self.start_button.loading = True + + # Run the user's function + result = self.function(*self.function_args, **self.function_kwargs) + + # Call the callback with the result, if provided + if self.callback: + self.callback(result) + + except Exception as e: + # Handle exceptions and display an error message + print(f"Exception in _run_task: {e}") + if self.alert is not None: + self.alert.append_msg(f"Error occurred: {e}", type_="error") + finally: + # Re-enable components + self.set_components_enabled(True) + + if self.start_button is not None: + self.start_button.loading = False + if self.stop_button is not None: + self.stop_button.loading = False + + def stop_task(self, *args): + """Signals the task to stop.""" + if self.stop_button is not None: + self.stop_button.loading = True + + # Signal the task to stop + self.stop_event.set() + + if self.alert is not None: + self.alert.append_msg("The process was interrupted by the user.", type_="warning") + + def set_components_enabled(self, enabled: bool): + """Enables or disables UI components.""" + for component in self.disable_components: + component.disabled = not enabled diff --git a/sepal_ui/sepalwidgets/inputs.py b/sepal_ui/sepalwidgets/inputs.py index 5d45bdc0..f3abfdaa 100644 --- a/sepal_ui/sepalwidgets/inputs.py +++ b/sepal_ui/sepalwidgets/inputs.py @@ -31,6 +31,7 @@ from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import gee from sepal_ui.scripts import utils as su +from sepal_ui.scripts.thread_controller import TaskController from sepal_ui.sepalwidgets.btn import Btn from sepal_ui.sepalwidgets.sepalwidget import SepalWidget @@ -711,10 +712,8 @@ def __init__( # load the assets in the combobox - if not self._initial_assets: - self._initial_assets.extend(gee.get_assets(self.folder)) - - self._get_items(gee_assets=self._initial_assets) + task_controller = TaskController(self._get_items, gee_assets=self._initial_assets) + task_controller.start_task() self._fill_no_data({}) # add js behaviours @@ -767,6 +766,9 @@ def _validate(self, change: dict) -> None: @sd.switch("loading", "disabled") def _get_items(self, *args, gee_assets: List[dict] = None) -> Self: + + if not self._initial_assets: + self._initial_assets.extend(gee.get_assets(self.folder)) # init the item list items = [] From 1c9a591638af1c392391c600866b7c50f88417db Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Thu, 31 Oct 2024 16:23:00 +0100 Subject: [PATCH 2/2] lint --- sepal_ui/scripts/thread_controller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sepal_ui/scripts/thread_controller.py b/sepal_ui/scripts/thread_controller.py index b6e5bbe8..0e93bfbb 100644 --- a/sepal_ui/scripts/thread_controller.py +++ b/sepal_ui/scripts/thread_controller.py @@ -1,5 +1,8 @@ +"""Controller for running long-running tasks in a separate thread.""" + import threading -from typing import Callable, Optional, List +from typing import Callable, List, Optional + import sepal_ui.sepalwidgets as sw @@ -15,8 +18,7 @@ def __init__( *function_args, **function_kwargs, ): - """ - Initializes the TaskController. + """Initializes the TaskController. Args: function: The long-running function to execute.