Browse Source

Merge branch 'develop' into test/Metric-component

namnguyen 10 months ago
parent
commit
25b8467558

+ 26 - 11
frontend/taipy/src/ScenarioDag.tsx

@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { Point } from '@projectstorm/geometry';
+import React, { useCallback, useEffect, useMemo, useState, useRef } from "react";
+import { Point } from "@projectstorm/geometry";
 import { CanvasWidget } from "@projectstorm/react-canvas-core";
 import { CanvasWidget } from "@projectstorm/react-canvas-core";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import AppBar from "@mui/material/AppBar";
 import AppBar from "@mui/material/AppBar";
@@ -76,6 +76,8 @@ const getValidScenario = (scenar: DisplayModel | DisplayModel[]) =>
         ? (scenar[0] as DisplayModel)
         ? (scenar[0] as DisplayModel)
         : undefined;
         : undefined;
 
 
+const preventWheel = (e: Event) => e.preventDefault();
+
 const ScenarioDag = (props: ScenarioDagProps) => {
 const ScenarioDag = (props: ScenarioDagProps) => {
     const { showToolbar = true, onSelect, onAction } = props;
     const { showToolbar = true, onSelect, onAction } = props;
     const [scenarioId, setScenarioId] = useState("");
     const [scenarioId, setScenarioId] = useState("");
@@ -85,6 +87,7 @@ const ScenarioDag = (props: ScenarioDagProps) => {
     const [taskStatuses, setTaskStatuses] = useState<TaskStatuses>();
     const [taskStatuses, setTaskStatuses] = useState<TaskStatuses>();
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
+    const canvasRef = useRef<CanvasWidget>(null);
 
 
     const render = useDynamicProperty(props.render, props.defaultRender, true);
     const render = useDynamicProperty(props.render, props.defaultRender, true);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
@@ -144,16 +147,18 @@ const ScenarioDag = (props: ScenarioDagProps) => {
             // populate model
             // populate model
             doLayout = populateModel(addStatusToDisplayModel(displayModel, taskStatuses), model);
             doLayout = populateModel(addStatusToDisplayModel(displayModel, taskStatuses), model);
         }
         }
-        const rects = engine.getModel() && engine
-            .getModel()
-            .getNodes()
-            .reduce((pv, nm) => {
-                pv[nm.getID()] = nm.getPosition();
-                return pv;
-            }, {} as Record<string, Point>);
+        const rects =
+            engine.getModel() &&
+            engine
+                .getModel()
+                .getNodes()
+                .reduce((pv, nm) => {
+                    pv[nm.getID()] = nm.getPosition();
+                    return pv;
+                }, {} as Record<string, Point>);
         const hasPos = rects && Object.keys(rects).length;
         const hasPos = rects && Object.keys(rects).length;
         if (hasPos) {
         if (hasPos) {
-            model.getNodes().forEach(nm => rects[nm.getID()] && nm.setPosition(rects[nm.getID()]));
+            model.getNodes().forEach((nm) => rects[nm.getID()] && nm.setPosition(rects[nm.getID()]));
         }
         }
         engine.setModel(model);
         engine.setModel(model);
         model.setLocked(true);
         model.setLocked(true);
@@ -165,10 +170,20 @@ const ScenarioDag = (props: ScenarioDagProps) => {
         showVar && dispatch(createSendUpdateAction(showVar, render, module));
         showVar && dispatch(createSendUpdateAction(showVar, render, module));
     }, [render, props.updateVars, dispatch, module]);
     }, [render, props.updateVars, dispatch, module]);
 
 
+    useEffect(() => {
+        setTimeout(() => {
+            // wait for div to be referenced and then set a non passive listener on wheel event
+            canvasRef.current?.ref.current?.addEventListener("wheel", preventWheel, { passive: false });
+        }, 300);
+        // remove the listener
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+        return () => canvasRef.current?.ref.current?.removeEventListener("wheel", preventWheel);
+    }, []);
+
     return render && scenarioId ? (
     return render && scenarioId ? (
         <Paper sx={sizeSx} id={props.id} className={className}>
         <Paper sx={sizeSx} id={props.id} className={className}>
             {showToolbar ? <DagTitle zoomToFit={zoomToFit} /> : null}
             {showToolbar ? <DagTitle zoomToFit={zoomToFit} /> : null}
-            <CanvasWidget engine={engine} />
+            <CanvasWidget engine={engine} ref={canvasRef} />
         </Paper>
         </Paper>
     ) : null;
     ) : null;
 };
 };

+ 31 - 4
taipy/core/scenario/_scenario_manager.py

@@ -9,7 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-import datetime
+from datetime import datetime
 from functools import partial
 from functools import partial
 from typing import Any, Callable, Dict, List, Literal, Optional, Union
 from typing import Any, Callable, Dict, List, Literal, Optional, Union
 
 
@@ -122,7 +122,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
     def _create(
     def _create(
         cls,
         cls,
         config: ScenarioConfig,
         config: ScenarioConfig,
-        creation_date: Optional[datetime.datetime] = None,
+        creation_date: Optional[datetime] = None,
         name: Optional[str] = None,
         name: Optional[str] = None,
     ) -> Scenario:
     ) -> Scenario:
         _task_manager = _TaskManagerFactory._build_manager()
         _task_manager = _TaskManagerFactory._build_manager()
@@ -288,9 +288,8 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
     def _get_primary_scenarios(cls) -> List[Scenario]:
     def _get_primary_scenarios(cls) -> List[Scenario]:
         return [scenario for scenario in cls._get_all() if scenario.is_primary]
         return [scenario for scenario in cls._get_all() if scenario.is_primary]
 
 
-    @classmethod
+    @staticmethod
     def _sort_scenarios(
     def _sort_scenarios(
-        cls,
         scenarios: List[Scenario],
         scenarios: List[Scenario],
         descending: bool = False,
         descending: bool = False,
         sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
         sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
@@ -306,6 +305,34 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
             scenarios.sort(key=lambda x: (x.name, x.id), reverse=descending)
             scenarios.sort(key=lambda x: (x.name, x.id), reverse=descending)
         return scenarios
         return scenarios
 
 
+    @staticmethod
+    def _filter_by_creation_time(
+        scenarios: List[Scenario],
+        created_start_time: Optional[datetime] = None,
+        created_end_time: Optional[datetime] = None,
+    ) -> List[Scenario]:
+        """
+        Filter a list of scenarios by a given creation time period.
+        The time period is inclusive.
+
+        Parameters:
+            created_start_time (Optional[datetime]): Start time of the period.
+            created_end_time (Optional[datetime]): End time of the period.
+
+        Returns:
+            List[Scenario]: List of scenarios created in the given time period.
+        """
+        if not created_start_time and not created_end_time:
+            return scenarios
+
+        if not created_start_time:
+            return [scenario for scenario in scenarios if scenario.creation_date <= created_end_time]
+
+        if not created_end_time:
+            return [scenario for scenario in scenarios if created_start_time <= scenario.creation_date]
+
+        return [scenario for scenario in scenarios if created_start_time <= scenario.creation_date <= created_end_time]
+
     @classmethod
     @classmethod
     def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
     def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
         if isinstance(scenario, str):
         if isinstance(scenario, str):

+ 13 - 0
taipy/core/taipy.py

@@ -510,6 +510,8 @@ def get_scenarios(
     tag: Optional[str] = None,
     tag: Optional[str] = None,
     is_sorted: bool = False,
     is_sorted: bool = False,
     descending: bool = False,
     descending: bool = False,
+    created_start_time: Optional[datetime] = None,
+    created_end_time: Optional[datetime] = None,
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
 ) -> List[Scenario]:
 ) -> List[Scenario]:
     """Retrieve a list of existing scenarios filtered by cycle or tag.
     """Retrieve a list of existing scenarios filtered by cycle or tag.
@@ -526,6 +528,8 @@ def get_scenarios(
             The default value is False.
             The default value is False.
         descending (bool): If True, sort the output list of scenarios in descending order.
         descending (bool): If True, sort the output list of scenarios in descending order.
             The default value is False.
             The default value is False.
+        created_start_time (Optional[datetime]): The optional inclusive start date to filter scenarios by creation date.
+        created_end_time (Optional[datetime]): The optional inclusive end date to filter scenarios by creation date.
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
@@ -548,6 +552,8 @@ def get_scenarios(
     else:
     else:
         scenarios = []
         scenarios = []
 
 
+    if created_start_time or created_end_time:
+        scenarios = scenario_manager._filter_by_creation_time(scenarios, created_start_time, created_end_time)
     if is_sorted:
     if is_sorted:
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
     return scenarios
     return scenarios
@@ -569,6 +575,8 @@ def get_primary(cycle: Cycle) -> Optional[Scenario]:
 def get_primary_scenarios(
 def get_primary_scenarios(
     is_sorted: bool = False,
     is_sorted: bool = False,
     descending: bool = False,
     descending: bool = False,
+    created_start_time: Optional[datetime] = None,
+    created_end_time: Optional[datetime] = None,
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
 ) -> List[Scenario]:
 ) -> List[Scenario]:
     """Retrieve a list of all primary scenarios.
     """Retrieve a list of all primary scenarios.
@@ -578,6 +586,8 @@ def get_primary_scenarios(
             The default value is False.
             The default value is False.
         descending (bool): If True, sort the output list of scenarios in descending order.
         descending (bool): If True, sort the output list of scenarios in descending order.
             The default value is False.
             The default value is False.
+        created_start_time (Optional[datetime]): The optional inclusive start date to filter scenarios by creation date.
+        created_end_time (Optional[datetime]): The optional inclusive end date to filter scenarios by creation date.
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
@@ -589,6 +599,9 @@ def get_primary_scenarios(
     """
     """
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenarios = scenario_manager._get_primary_scenarios()
     scenarios = scenario_manager._get_primary_scenarios()
+
+    if created_start_time or created_end_time:
+        scenarios = scenario_manager._filter_by_creation_time(scenarios, created_start_time, created_end_time)
     if is_sorted:
     if is_sorted:
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
     return scenarios
     return scenarios

+ 48 - 48
taipy/gui/builder/_element.py

@@ -18,11 +18,13 @@ import io
 import re
 import re
 import sys
 import sys
 import typing as t
 import typing as t
+import uuid
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from collections.abc import Iterable
 from collections.abc import Iterable
 from types import FrameType, FunctionType
 from types import FrameType, FunctionType
 
 
 from .._warnings import _warn
 from .._warnings import _warn
+from ..utils import _getscopeattr
 
 
 if sys.version_info < (3, 9):
 if sys.version_info < (3, 9):
     from ..utils.unparse import _Unparser
     from ..utils.unparse import _Unparser
@@ -50,8 +52,8 @@ class _Element(ABC):
         return obj
         return obj
 
 
     def __init__(self, *args, **kwargs) -> None:
     def __init__(self, *args, **kwargs) -> None:
-        self.__variables: t.Dict[str, FunctionType] = {}
         self._properties: t.Dict[str, t.Any] = {}
         self._properties: t.Dict[str, t.Any] = {}
+        self._lambdas: t.Dict[str, str] = {}
         self.__calling_frame = t.cast(
         self.__calling_frame = t.cast(
             FrameType, t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back).f_back
             FrameType, t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back).f_back
         )
         )
@@ -65,6 +67,11 @@ class _Element(ABC):
         self._properties.update(kwargs)
         self._properties.update(kwargs)
         self.parse_properties()
         self.parse_properties()
 
 
+    def _evaluate_lambdas(self, gui: Gui):
+        for k, lmbd in self._lambdas.items():
+            expr = gui._evaluate_expr(f"{{{lmbd}}}")
+            gui._bind_var_val(k, _getscopeattr(gui, expr))
+
     # Convert property value to string/function
     # Convert property value to string/function
     def parse_properties(self):
     def parse_properties(self):
         self._properties = {
         self._properties = {
@@ -86,53 +93,46 @@ class _Element(ABC):
             return value
             return value
         if isinstance(value, FunctionType):
         if isinstance(value, FunctionType):
             if key.startswith("on_"):
             if key.startswith("on_"):
-                return value
-            else:
-                try:
-                    st = ast.parse(inspect.getsource(value.__code__).strip())
-                    lambda_by_name: t.Dict[str, ast.Lambda] = {}
-                    _LambdaByName(self._ELEMENT_NAME, lambda_by_name).visit(st)
-                    lambda_fn = lambda_by_name.get(
-                        key,
-                        lambda_by_name.get(_LambdaByName._DEFAULT_NAME, None)
-                        if key == self._DEFAULT_PROPERTY
-                        else None,
+                if value.__name__.startswith("<"):
+                    return value
+                return value.__name__
+
+            try:
+                st = ast.parse(inspect.getsource(value.__code__).strip())
+                lambda_by_name: t.Dict[str, ast.Lambda] = {}
+                _LambdaByName(self._ELEMENT_NAME, lambda_by_name).visit(st)
+                lambda_fn = lambda_by_name.get(
+                    key,
+                    lambda_by_name.get(_LambdaByName._DEFAULT_NAME, None) if key == self._DEFAULT_PROPERTY else None,
+                )
+                if lambda_fn is not None:
+                    args = [arg.arg for arg in lambda_fn.args.args]
+                    targets = [
+                        compr.target.id  # type: ignore[attr-defined]
+                        for node in ast.walk(lambda_fn.body)
+                        if isinstance(node, ast.ListComp)
+                        for compr in node.generators
+                    ]
+                    tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(
+                        lambda_fn
                     )
                     )
-                    if lambda_fn is not None:
-                        args = [arg.arg for arg in lambda_fn.args.args]
-                        targets = [
-                            compr.target.id  # type: ignore[attr-defined]
-                            for node in ast.walk(lambda_fn.body)
-                            if isinstance(node, ast.ListComp)
-                            for compr in node.generators
-                        ]
-                        tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(
-                            lambda_fn
-                        )
-                        ast.fix_missing_locations(tree)
-                        if sys.version_info < (3, 9):  # python 3.8 ast has no unparse
-                            string_fd = io.StringIO()
-                            _Unparser(tree, string_fd)
-                            string_fd.seek(0)
-                            lambda_text = string_fd.read()
-                        else:
-                            lambda_text = ast.unparse(tree)
-                        new_code = compile(f"{_Element._NEW_LAMBDA_NAME} = {lambda_text}", "<ast>", "exec")
-                        namespace: t.Dict[str, FunctionType] = {}
-                        exec(new_code, namespace)
-                        var_name = f"__lambda_{id(namespace[_Element._NEW_LAMBDA_NAME])}"
-                        self.__variables[var_name] = namespace[_Element._NEW_LAMBDA_NAME]
-                        return f'{{{var_name}({", ".join(args)})}}'
-                except Exception as e:
-                    _warn("Error in lambda expression", e)
+                    ast.fix_missing_locations(tree)
+                    if sys.version_info < (3, 9):  # python 3.8 ast has no unparse
+                        string_fd = io.StringIO()
+                        _Unparser(tree, string_fd)
+                        string_fd.seek(0)
+                        lambda_text = string_fd.read()
+                    else:
+                        lambda_text = ast.unparse(tree)
+                    lambda_name = f"__lambda_{uuid.uuid4().hex}"
+                    self._lambdas[lambda_name] = lambda_text
+                    return f'{{{lambda_name}({", ".join(args)})}}'
+            except Exception as e:
+                _warn("Error in lambda expression", e)
         if hasattr(value, "__name__"):
         if hasattr(value, "__name__"):
             return str(getattr(value, "__name__"))  # noqa: B009
             return str(getattr(value, "__name__"))  # noqa: B009
         return str(value)
         return str(value)
 
 
-    def _bind_variables(self, gui: "Gui"):
-        for var_name, var_value in self.__variables.items():
-            gui._bind_var_val(var_name, var_value)
-
     @abstractmethod
     @abstractmethod
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
         pass
         pass
@@ -159,7 +159,7 @@ class _Block(_Element):
         _BuilderContextManager().pop()
         _BuilderContextManager().pop()
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
-        self._bind_variables(gui)
+        self._evaluate_lambdas(gui)
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         return f"{el[0]}{self._render_children(gui)}</{el[1]}>"
         return f"{el[0]}{self._render_children(gui)}</{el[1]}>"
 
 
@@ -170,7 +170,7 @@ class _Block(_Element):
 class _DefaultBlock(_Block):
 class _DefaultBlock(_Block):
     _ELEMENT_NAME = "part"
     _ELEMENT_NAME = "part"
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, **kwargs):  # do not remove as it could break the search in frames
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
 
 
@@ -223,7 +223,7 @@ class html(_Block):
         self._content = args[1] if len(args) > 1 else ""
         self._content = args[1] if len(args) > 1 else ""
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
-        self._bind_variables(gui)
+        self._evaluate_lambdas(gui)
         if self._ELEMENT_NAME:
         if self._ELEMENT_NAME:
             attrs = ""
             attrs = ""
             if self._properties:
             if self._properties:
@@ -236,11 +236,11 @@ class html(_Block):
 class _Control(_Element):
 class _Control(_Element):
     """NOT DOCUMENTED"""
     """NOT DOCUMENTED"""
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, **kwargs):  # do not remove as it could break the search in frames
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
-        self._bind_variables(gui)
+        self._evaluate_lambdas(gui)
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         return (
         return (
             f"<div>{el[0]}</{el[1]}></div>"
             f"<div>{el[0]}</{el[1]}></div>"

+ 2 - 0
taipy/gui/builder/_utils.py

@@ -39,6 +39,8 @@ class _TransformVarToValue(ast.NodeTransformer):
         if var_parts[0] in self.non_vars:
         if var_parts[0] in self.non_vars:
             return node
             return node
         value = _get_value_in_frame(self.frame, var_parts[0])
         value = _get_value_in_frame(self.frame, var_parts[0])
+        if callable(value):
+            return node
         if len(var_parts) > 1:
         if len(var_parts) > 1:
             value = attrgetter(var_parts[1])(value)
             value = attrgetter(var_parts[1])(value)
         return ast.Constant(value=value, kind=None)
         return ast.Constant(value=value, kind=None)

+ 53 - 0
tests/core/scenario/test_scenario_manager.py

@@ -13,6 +13,7 @@ from datetime import datetime, timedelta
 from typing import Callable, Iterable, Optional
 from typing import Callable, Iterable, Optional
 from unittest.mock import ANY, patch
 from unittest.mock import ANY, patch
 
 
+import freezegun
 import pytest
 import pytest
 
 
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
@@ -1481,3 +1482,55 @@ def test_get_scenarios_by_config_id_in_multiple_versions_environment():
 
 
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
+
+
+def test_filter_scenarios_by_creation_datetime():
+    scenario_config_1 = Config.configure_scenario("s1", sequence_configs=[])
+
+    with freezegun.freeze_time("2024-01-01"):
+        s_1_1 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-01-03"):
+        s_1_2 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-02-01"):
+        s_1_3 = _ScenarioManager._create(scenario_config_1)
+
+    all_scenarios = _ScenarioManager._get_all()
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios
+
+    # The time period is inclusive
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 3),
+    )
+    assert len(filtered_scenarios) == 2
+    assert sorted([s_1_1.id, s_1_2.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2023, 1, 1),
+        created_end_time=datetime(2025, 1, 1),
+    )
+    assert len(filtered_scenarios) == 3
+    assert sorted([s_1_1.id, s_1_2.id, s_1_3.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 2, 1),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_3] == filtered_scenarios
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios

+ 53 - 0
tests/core/scenario/test_scenario_manager_with_sql_repo.py

@@ -11,6 +11,7 @@
 
 
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 
 
+import freezegun
 import pytest
 import pytest
 
 
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
@@ -435,3 +436,55 @@ def test_get_scenarios_by_config_id_in_multiple_versions_environment(init_sql_re
 
 
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
+
+
+def test_filter_scenarios_by_creation_datetime(init_sql_repo):
+    scenario_config_1 = Config.configure_scenario("s1", sequence_configs=[])
+
+    with freezegun.freeze_time("2024-01-01"):
+        s_1_1 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-01-03"):
+        s_1_2 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-02-01"):
+        s_1_3 = _ScenarioManager._create(scenario_config_1)
+
+    all_scenarios = _ScenarioManager._get_all()
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios
+
+    # The time period is inclusive
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 3),
+    )
+    assert len(filtered_scenarios) == 2
+    assert sorted([s_1_1.id, s_1_2.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2023, 1, 1),
+        created_end_time=datetime(2025, 1, 1),
+    )
+    assert len(filtered_scenarios) == 3
+    assert sorted([s_1_1.id, s_1_2.id, s_1_3.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 2, 1),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_3] == filtered_scenarios
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios

+ 6 - 0
tests/core/test_taipy.py

@@ -431,6 +431,9 @@ class TestTaipy:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_all_by_tag") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_all_by_tag") as mck:
             tp.get_scenarios(tag="tag")
             tp.get_scenarios(tag="tag")
             mck.assert_called_once_with("tag")
             mck.assert_called_once_with("tag")
+        with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._filter_by_creation_time") as mck:
+            tp.get_scenarios(created_start_time=datetime.datetime(2021, 1, 1))
+            mck.assert_called_once_with([], datetime.datetime(2021, 1, 1), None)
 
 
     def test_get_scenarios_sorted(self):
     def test_get_scenarios_sorted(self):
         scenario_1_cfg = Config.configure_scenario(id="scenario_1")
         scenario_1_cfg = Config.configure_scenario(id="scenario_1")
@@ -500,6 +503,9 @@ class TestTaipy:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_primary_scenarios") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_primary_scenarios") as mck:
             tp.get_primary_scenarios()
             tp.get_primary_scenarios()
             mck.assert_called_once_with()
             mck.assert_called_once_with()
+        with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._filter_by_creation_time") as mck:
+            tp.get_scenarios(created_end_time=datetime.datetime(2021, 1, 1))
+            mck.assert_called_once_with([], None, datetime.datetime(2021, 1, 1))
 
 
     def test_set_primary(self, scenario):
     def test_set_primary(self, scenario):
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._set_primary") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._set_primary") as mck:

+ 30 - 0
tests/gui/builder/test_on_action.py

@@ -0,0 +1,30 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import taipy.gui.builder as tgb
+from taipy.gui import Gui, notify
+
+
+def test_builder_on_function(gui: Gui, test_client, helpers):
+    def on_slider(state):
+        notify(state, "success", f"Value: {state.value}")
+    gui._bind_var_val("on_slider", on_slider)
+    with tgb.Page(frame=None) as page:
+        tgb.slider(value="{value}", on_change=on_slider)  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['<Slider','onChange="on_slider"']
+    helpers.test_control_builder(gui, page, expected_list)
+
+
+def test_builder_on_lambda(gui: Gui, test_client, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.slider(value="{value}", on_change=lambda s: notify(s, "success", f"Lambda Value: {s.value}"))  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['<Slider','onChange="__lambda_']
+    helpers.test_control_builder(gui, page, expected_list)