Skip to content

Commit

Permalink
feat: Add ExperimentManager and AppState (#10)
Browse files Browse the repository at this point in the history
## 📥 Pull Request Description

This pull request implements the experiment manager feature. This
feature was requested to be able to filter experiments based on their
properties. The AppState has also been added.

## 👀 Affected Areas

Experiments

Co-authored-by: Nils Uhrberg <[email protected]>
  • Loading branch information
ihsanKisi and aiakide authored May 28, 2024
1 parent da7266e commit 083a021
Show file tree
Hide file tree
Showing 18 changed files with 471 additions and 270 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ body:
description: By submitting this feature request, you agree to follow our [Code of Conduct](https://github.com/codecentric-oss/niceml-dashboard/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
required: true
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ Please provide any relevant links (e.g. documentation, external resources) that

If applicable, please include screenshots of the before and after effects of your changes.

Thank you for your contribution! 🎉
Thank you for your contribution! 🎉
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ Remember that this code of conduct applies to all project spaces, including GitH

Let's work together to build a welcoming and inclusive community where everyone can contribute and grow.

Note: This code of conduct was adapted from the Contributor Covenant (https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
Note: This code of conduct was adapted from the Contributor Covenant (https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# niceml-dashboard
- Welcome on board.

Empty file.
42 changes: 42 additions & 0 deletions nicemldashboard/basecomponents/buttons/sidebarbutton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
This module provides a class for the buttons on the sidebar.
"""
from nicegui import ui
from nicemldashboard.experiment.type import ExperimentType

from nicemldashboard.state.appstate import (
get_event_manager,
ExperimentStateKeys,
ExperimentEvents,
)


class SidebarButton(ui.button):
"""
This class describes a sidebarbutton and has methods to handle on click events.
"""

def __init__(
self,
*args,
experiment_type: ExperimentType,
**kwargs,
) -> None:
"""
Inits SidebarButton class with the provided experiment type
Args:
*args:
experiment_type:
**kwargs:
"""
super().__init__(*args, **kwargs)
self.experiment_type = experiment_type
self.on("click", self._change_experiment_type)

def _change_experiment_type(self):
experiment_state_data = get_event_manager().get_dict(
ExperimentStateKeys.EXPERIMENT_DICT
)
experiment_state_data[
ExperimentEvents.ON_EXPERIMENT_PREFIX_CHANGE
] = self.experiment_type
File renamed without changes.
25 changes: 12 additions & 13 deletions nicemldashboard/basecomponents/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
from typing import Optional, List

from nicegui import ui
from nicemldashboard.basecomponents.buttons import SidebarToggleButton

from nicemldashboard.basecomponents.buttons.sidebarbutton import SidebarButton
from nicemldashboard.basecomponents.buttons.sidebartogglebutton import (
SidebarToggleButton,
)
from nicemldashboard.experiment.type import ExperimentType


Expand All @@ -26,24 +30,19 @@ def sidebar(experiment_types: Optional[List[ExperimentType]] = None):
Defaults to None, in which case buttons for all available
experiment types will be displayed.
"""
experiment_types = experiment_types or [
exp_type.value for exp_type in ExperimentType.__members__.values()
]

with ui.left_drawer(top_corner=False, bottom_corner=True, fixed=True).classes(
"sidebar"
).props("width=70") as left_drawer:
side_bar_toggle = SidebarToggleButton(left_drawer=left_drawer)

for experiment_type in experiment_types:
with ui.button(
for experiment_type in ExperimentType.__members__.values():
with SidebarButton(
color="transparent",
icon=experiment_type.icon,
).props(
"flat"
).classes("exp-type-btn").bind_text_from(
experiment_type,
icon=experiment_type.value.icon,
experiment_type=experiment_type,
).props("flat").classes("exp-type-btn").bind_text_from(
experiment_type.value,
"prefix",
backward=lambda x: ("" if not side_bar_toggle.is_expanded() else x),
):
ui.tooltip(experiment_type.name)
ui.tooltip(experiment_type.value.name)
14 changes: 12 additions & 2 deletions nicemldashboard/basecomponents/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@
"""

from typing import List
from nicegui import ui
from nicegui.observables import ObservableDict
from nicemldashboard.experiment.experiment import Experiment
from nicemldashboard.experiment.experimentmanager import ExperimentManager
from nicemldashboard.state.appstate import ExperimentEvents


def experiment_runs_table(experiments: List[Experiment]):
@ui.refreshable
def experiment_runs_table(
experiment_manager: ExperimentManager, exp_dic: ObservableDict
):
"""
Create a table for displaying experiment runs.
Args:
experiments: List of experiment runs to display in the table.
"""

experiments = experiment_manager.filter_by(
experiment_type=exp_dic.get(ExperimentEvents.ON_EXPERIMENT_PREFIX_CHANGE)
)

ui.table(
columns=Experiment.get_columns(),
rows=[run.get_row() for run in experiments],
Expand Down
1 change: 1 addition & 0 deletions nicemldashboard/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from nicemldashboard.pages.home import home
from nicemldashboard.utils.settings import Settings


# TODO: Make file paths os-agnostic
app.add_static_files("/fonts", "nicemldashboard/assets/fonts")

Expand Down
7 changes: 7 additions & 0 deletions nicemldashboard/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
This module provides custom exception implementations for the app
"""


class ExperimentFilterError(TypeError):
"""There is a filtering error."""
48 changes: 48 additions & 0 deletions nicemldashboard/experiment/experimentmanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
This module provides an experiment manager. Currently, experiment manager
supports filtering experiments by their properties.
"""
import logging
from typing import List

from nicemldashboard.exceptions import ExperimentFilterError
from nicemldashboard.experiment.experiment import Experiment


class ExperimentManager:
"""
Allows filtering experiments by making the filter_by method available
"""

def __init__(self, experiments: List[Experiment]):
"""
Initializes an ExperimentManager with the provided list of experiments.
"""
self.experiments = experiments

def filter_by(self, **filters) -> List[Experiment]:
"""
Filters the experiment list after filtering with the provided filters
Args:
**filters: A dictionary of filters to filter by
Returns:
List of filtered experiments
"""
filtered_experiments = []
for experiment_attribute, field_value in filters.items():
try:
filtered_experiments = [
exp
for exp in self.experiments
if getattr(exp, experiment_attribute, None) == field_value
]
except ExperimentFilterError as e:
# Log the error message with details of which experiment and filter caused it.
logging.warning(
f"Incomparable types between attribute '{experiment_attribute}' "
f"with field_value '{field_value}' "
f"and filter field_value '{field_value}': {e}"
)
return filtered_experiments
16 changes: 15 additions & 1 deletion nicemldashboard/pages/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,33 @@

from nicegui import ui


from nicemldashboard.state.appstate import (
AppState,
ExperimentStateKeys,
init_event_manager,
)

from nicemldashboard.basecomponents.sidebar import sidebar
from nicemldashboard.basecomponents.table import experiment_runs_table
from nicemldashboard.experiment.utils import get_random_experiments
from nicemldashboard.experiment.experimentmanager import ExperimentManager


@ui.page("/")
def home():
"""
Define the layout of the home page.
"""

ui.add_scss("nicemldashboard/assets/style.scss")
_instance = AppState()
init_event_manager(_instance)
exp_data = _instance.get_dict(ExperimentStateKeys.EXPERIMENT_DICT)
exp_data.on_change(experiment_runs_table.refresh)

experiments = get_random_experiments(experiment_count=20)
experiment_manager = ExperimentManager(experiments)

sidebar()
with ui.grid().classes("content"):
Expand All @@ -33,4 +47,4 @@ def home():
with ui.row():
ui.input(label="Experiment run", placeholder="Search for run")
ui.separator()
experiment_runs_table(experiments=experiments)
experiment_runs_table(experiment_manager, exp_data)
Empty file.
53 changes: 38 additions & 15 deletions nicemldashboard/State.py → nicemldashboard/state/appstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,37 @@
Also includes helper functions to initialize and get the instance of the Event Manager.
Enums for Dicts are defined for consistency.
"""
from enum import Enum
from abc import ABCMeta
from enum import EnumMeta
from typing import Dict

from nicegui import context
import logging
from nicegui.observables import ObservableDict

logger = logging.getLogger(__name__)

_EVENT_MNGR_ATTR_NAME = "_niceml"
_EVENT_MNGR_ATTR_NAME = "_nicemldashboard"


class EventManager:
class StateKeys(ABCMeta, EnumMeta):
"""
Provides an abstract class for StateKey management
"""


class AppState:
"""
Manages event dictionaries and provides methods to retrieve and manage them.
"""

_observable_dicts: dict[str, ObservableDict]
_state_data: Dict[StateKeys, ObservableDict]

def __init__(self):
"""
Initializes the EventManager with an empty dictionary of observable dictionaries.
"""
self._observable_dicts = {}
self._state_data = {}

def get_dict(self, dict_name: str) -> ObservableDict:
"""
Expand All @@ -37,20 +46,20 @@ def get_dict(self, dict_name: str) -> ObservableDict:
Returns:
ObservableDict: The retrieved or newly created observable dictionary.
"""
if dict_name in self._observable_dicts:
return self._observable_dicts[dict_name]
if dict_name in self._state_data:
return self._state_data[dict_name]
else:
new_dict = ObservableDict()
self._observable_dicts[dict_name] = new_dict
self._state_data[dict_name] = new_dict
return new_dict


def _get_event_manager() -> EventManager:
def get_event_manager() -> AppState:
"""
Retrieves the EventManager instance from the current client context.
Returns:
EventManager: The EventManager instance.
AppState: The EventManager instance.
Raises:
RuntimeError: If Event Manager is not initialized.
Expand All @@ -61,25 +70,39 @@ def _get_event_manager() -> EventManager:
return getattr(client, _EVENT_MNGR_ATTR_NAME)


def init_event_manager(event_manager: EventManager):
def init_event_manager(event_manager: AppState):
"""
Initializes the EventManager for the current client context.
If already initialized, gives back a console output.
Args:
event_manager (EventManager): The EventManager instance to initialize.
event_manager (AppState): The EventManager instance to initialize.
"""
try:
client = context.get_client()
if getattr(client, _EVENT_MNGR_ATTR_NAME, None) is None:
setattr(client, _EVENT_MNGR_ATTR_NAME, event_manager)
except RuntimeError:
logger.debug("Event Manager cannot be initialized in a background task")
logger.warning("Event Manager cannot be initialized in a background task")


class StateEvent(ABCMeta, EnumMeta):
"""
Manages the keys for the observable dictionaries
"""


class ExperimentEvents(StateEvent):
"""
Manages the experimente events in the observable dictionaries
"""

ON_EXPERIMENT_PREFIX_CHANGE = "on_experiment_prefix_change"


class Dicts(Enum):
class ExperimentStateKeys(StateKeys):
"""
Manages the observable dictionaries
"""

experiment_dict = "experiment_dict"
EXPERIMENT_DICT = "experiment_dict"
Loading

0 comments on commit 083a021

Please sign in to comment.