Skip to content

Commit

Permalink
feat: request assets in a separate thread (#954)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfguerrerom authored Oct 31, 2024
1 parent a6431bb commit fa02412
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 4 deletions.
119 changes: 119 additions & 0 deletions sepal_ui/scripts/thread_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Controller for running long-running tasks in a separate thread."""

import threading
from typing import Callable, List, Optional

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
10 changes: 6 additions & 4 deletions sepal_ui/sepalwidgets/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []

Expand Down

0 comments on commit fa02412

Please sign in to comment.