diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..51f88beae8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ +## What type of PR is this? (check all applicable) + +- [ ] Refactor +- [ ] Feature +- [ ] Bug Fix +- [ ] Optimization +- [ ] Documentation Update + +## Description + +## Related Tickets & Documents + + + +- Related Issue # +- Closes # + +## How to reproduce the issue + +_Please replace this line with instructions on how to reproduce the issue or test the feature._ + +## Other branches or releases that this needs to be backported +_Describe which projects this change will impact and that needs to be backported._ + +## Checklist +_We encourage you to keep the code coverage percentage at 80% and above._ + +- [ ] Does this solution meet the acceptance criteria of the related issue? +- [ ] Is the related issue checklist completed? +- [ ] Does this PR adds unit tests for the developed code? If not, why? +- [ ] End-to-End tests have been added or updated? +- [ ] Was the documentation updated, or a dedicated issue for documentation created? (If applicable) +- [ ] Is the release notes updated? (If applicable) diff --git a/README.md b/README.md index 93203e1cfa..a38df0a192 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -[![Hacktoberfest_clickable](https://github.com/user-attachments/assets/b1b5a430-6df9-40c2-999f-de3433f61251)](https://assorted-son-815.notion.site/Hacktoberfest-2024-with-Taipy-2a5032a3f01642709e88ffaa5d0d169e) - -
diff --git a/doc/gui/examples/controls/chat_streaming.py b/doc/gui/examples/controls/chat_streaming.py new file mode 100644 index 0000000000..6faedc5438 --- /dev/null +++ b/doc/gui/examples/controls/chat_streaming.py @@ -0,0 +1,192 @@ +# 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. +# ----------------------------------------------------------------------------------------- +# To execute this script, make sure that the taipy-gui package is installed in your +# Python environment and run: +# python " # noqa: E501 + response = make_response(render_template_string(reload_html), 400) response.set_cookie( _Server._RESOURCE_HANDLER_ARG, "", secure=request.is_secure, httponly=True, expires=0, path="/" ) diff --git a/taipy/gui_core/_adapters.py b/taipy/gui_core/_adapters.py index 579a8b6fd2..cffa66c716 100644 --- a/taipy/gui_core/_adapters.py +++ b/taipy/gui_core/_adapters.py @@ -278,14 +278,20 @@ def get_hash(): } -def _filter_value(base_val: t.Any, operator: t.Callable, val: t.Any, adapt: t.Optional[t.Callable] = None): +def _filter_value( + base_val: t.Any, + operator: t.Callable, + val: t.Any, + adapt: t.Optional[t.Callable] = None, + match_case: bool = False, +): if base_val is None: base_val = "" if isinstance(val, str) else 0 else: if isinstance(base_val, (datetime, date)): base_val = base_val.isoformat() val = adapt(base_val, val) if adapt else val - if isinstance(base_val, str) and isinstance(val, str): + if not match_case and isinstance(base_val, str) and isinstance(val, str): base_val = base_val.lower() val = val.lower() return operator(base_val, val) @@ -305,7 +311,7 @@ def _adapt_type(base_val, val): return val -def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any): +def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any, match_case: bool = False): if operator is contains: types = {type(v) for v in list_val} if len(types) == 1: @@ -315,11 +321,18 @@ def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any): else: val = _adapt_type(typed_val, val) return contains(list(list_val), val) - return next(filter(lambda v: _filter_value(v, operator, val), list_val), None) is not None + return next(filter(lambda v: _filter_value(v, operator, val, match_case=match_case), list_val), None) is not None def _invoke_action( - ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn: t.Optional[str] + ent: t.Any, + col: str, + col_type: str, + is_dn: bool, + action: str, + val: t.Any, + col_fn: t.Optional[str] = None, + match_case: bool = False, ) -> bool: if ent is None: return False @@ -337,8 +350,8 @@ def _invoke_action( if isinstance(cur_val, DataNode): cur_val = cur_val.read() if not isinstance(cur_val, str) and isinstance(cur_val, Iterable): - return _filter_iterable(cur_val, op, val) - return _filter_value(cur_val, op, val, _adapt_type) + return _filter_iterable(cur_val, op, val, match_case) + return _filter_value(cur_val, op, val, _adapt_type, match_case) except Exception as e: if _is_debugging(): _warn(f"Error filtering with {col} {action} {val} on {ent}.", e) diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py index 0a7aa69a33..522a143c82 100644 --- a/taipy/gui_core/_context.py +++ b/taipy/gui_core/_context.py @@ -284,10 +284,18 @@ def scenario_adapter(self, scenario: Scenario): return None def filter_entities( - self, cycle_scenario: t.List, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn=None + self, + cycle_scenario: t.List, + col: str, + col_type: str, + is_dn: bool, + action: str, + val: t.Any, + col_fn=None, + match_case: bool = False, ): cycle_scenario[2] = [ - e for e in cycle_scenario[2] if _invoke_action(e, col, col_type, is_dn, action, val, col_fn) + e for e in cycle_scenario[2] if _invoke_action(e, col, col_type, is_dn, action, val, col_fn, match_case) ] return cycle_scenario @@ -326,6 +334,7 @@ def get_filtered_scenario_list( col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None val = fd.get("value") action = fd.get("action", "") + match_case = fd.get("matchCase", False) is not False customs = CustomScenarioFilter._get_custom(col) if customs: with self.gui._set_locals_context(customs[0] or None): @@ -344,14 +353,14 @@ def get_filtered_scenario_list( e for e in filtered_list if not isinstance(e, Scenario) - or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn) + or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn, match_case) ] # level 2 filtering filtered_list = [ e if isinstance(e, Scenario) else self.filter_entities( - t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn + t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn, match_case ) for e in filtered_list ] @@ -649,6 +658,7 @@ def get_filtered_datanode_list( col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None val = fd.get("value") action = fd.get("action", "") + match_case = fd.get("matchCase", False) is not False customs = CustomScenarioFilter._get_custom(col) if customs: with self.gui._set_locals_context(customs[0] or None): @@ -666,15 +676,17 @@ def get_filtered_datanode_list( e for e in filtered_list if not isinstance(e, DataNode) - or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn) + or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn, match_case) ] # level 3 filtering filtered_list = [ e if isinstance(e, DataNode) - else self.filter_entities(d, t.cast(str, col), col_type, False, action, val, col_fn) + else self.filter_entities( + t.cast(list, d), t.cast(str, col), col_type, False, action, val, col_fn, match_case + ) for e in filtered_list - for d in t.cast(list, t.cast(list, e)[2]) + for d in (t.cast(list, t.cast(list, e)[2]) if isinstance(e, list) else [e]) ] # remove empty cycles return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))] diff --git a/tests/gui_core/test_context_filter.py b/tests/gui_core/test_context_filter.py new file mode 100644 index 0000000000..ac3bf528a8 --- /dev/null +++ b/tests/gui_core/test_context_filter.py @@ -0,0 +1,106 @@ +# 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 typing as t +from unittest.mock import Mock, patch + +from taipy.common.config.common.scope import Scope +from taipy.core import DataNode, Scenario +from taipy.core.data.pickle import PickleDataNode +from taipy.gui_core._context import _GuiCoreContext + +scenario_a = Scenario("scenario_a_config_id", None, {"a_prop": "a"}) +scenario_b = Scenario("scenario_b_config_id", None, {"a_prop": "b"}) +scenarios: t.List[t.Union[t.List, Scenario, None]] = [scenario_a, scenario_b] + + +class TestGuiCoreContext_filter_scenarios: + def test_get_filtered_scenario_list_no_filter(self): + gui_core_context = _GuiCoreContext(Mock()) + assert gui_core_context.get_filtered_scenario_list(scenarios, None) is scenarios + + def test_get_filtered_scenario_list_a_filter(self): + gui_core_context = _GuiCoreContext(Mock()) + res = gui_core_context.get_filtered_scenario_list( + scenarios, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains"}] + ) + assert len(res) == 1 + assert res[0] is scenario_a + + def test_get_filtered_scenario_list_a_filter_case(self): + gui_core_context = _GuiCoreContext(Mock()) + res = gui_core_context.get_filtered_scenario_list( + scenarios, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains", "matchCase": True}] + ) + assert len(res) == 1 + assert res[0] is scenario_a + + res = gui_core_context.get_filtered_scenario_list( + scenarios, [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": False}] + ) + assert len(res) == 1 + assert res[0] is scenario_a + + res = gui_core_context.get_filtered_scenario_list( + scenarios, [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": True}] + ) + assert len(res) == 0 + + +datanode_a = PickleDataNode("datanode_a_config_id", Scope.SCENARIO) +datanode_b = PickleDataNode("datanode_b_config_id", Scope.SCENARIO) +datanodes: t.List[t.Union[t.List, DataNode, None]] = [datanode_a, datanode_b] + + +def mock_core_get(entity_id): + if entity_id == datanode_a.id: + return datanode_a + if entity_id == datanode_b.id: + return datanode_b + return None + + +class TestGuiCoreContext_filter_datanodes: + def test_get_filtered_datanode_list_no_filter(self): + gui_core_context = _GuiCoreContext(Mock()) + assert gui_core_context.get_filtered_datanode_list(datanodes, None) is datanodes + + def test_get_filtered_datanode_list_a_filter(self): + with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get): + gui_core_context = _GuiCoreContext(Mock()) + res = gui_core_context.get_filtered_datanode_list( + datanodes, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains"}] + ) + assert len(res) == 1 + assert res[0] is datanode_a + + def test_get_filtered_datanode_list_a_filter_case(self): + with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get): + gui_core_context = _GuiCoreContext(Mock()) + res = gui_core_context.get_filtered_datanode_list( + datanodes, + [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains", "matchCase": True}], + ) + assert len(res) == 1 + assert res[0] is datanode_a + + res = gui_core_context.get_filtered_datanode_list( + datanodes, + [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": False}], + ) + assert len(res) == 1 + assert res[0] is datanode_a + + res = gui_core_context.get_filtered_datanode_list( + datanodes, + [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": True}], + ) + assert len(res) == 0 diff --git a/tests/rest/test_datanode.py b/tests/rest/test_datanode.py index 264a1f59b6..12f9fc26b6 100644 --- a/tests/rest/test_datanode.py +++ b/tests/rest/test_datanode.py @@ -33,8 +33,9 @@ def test_delete_datanode(client): rep = client.get(user_url) assert rep.status_code == 404 - with mock.patch("taipy.core.data._data_manager._DataManager._delete"), mock.patch( - "taipy.core.data._data_manager._DataManager._get" + with ( + mock.patch("taipy.core.data._data_manager._DataManager._delete"), + mock.patch("taipy.core.data._data_manager._DataManager._get"), ): # test get_datanode rep = client.delete(url_for("api.datanode_by_id", datanode_id="foo")) @@ -63,7 +64,7 @@ def test_get_all_datanodes(client, default_datanode_config_list): for ds in range(10): with mock.patch("taipy.rest.api.resources.datanode.DataNodeList.fetch_config") as config_mock: config_mock.return_value = default_datanode_config_list[ds] - datanodes_url = url_for("api.datanodes", config_id=config_mock.name) + datanodes_url = url_for("api.datanodes", config_id=default_datanode_config_list[ds].name) client.post(datanodes_url) rep = client.get(datanodes_url) diff --git a/tests/rest/test_scenario.py b/tests/rest/test_scenario.py index 0e616b1d68..2ce0dc8a7d 100644 --- a/tests/rest/test_scenario.py +++ b/tests/rest/test_scenario.py @@ -34,8 +34,9 @@ def test_delete_scenario(client): rep = client.get(user_url) assert rep.status_code == 404 - with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._delete"), mock.patch( - "taipy.core.scenario._scenario_manager._ScenarioManager._get" + with ( + mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._delete"), + mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get"), ): # test get_scenario rep = client.delete(url_for("api.scenario_by_id", scenario_id="foo")) @@ -64,7 +65,7 @@ def test_get_all_scenarios(client, default_sequence, default_scenario_config_lis for ds in range(10): with mock.patch("taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock: config_mock.return_value = default_scenario_config_list[ds] - scenarios_url = url_for("api.scenarios", config_id=config_mock.name) + scenarios_url = url_for("api.scenarios", config_id=default_scenario_config_list[ds].name) client.post(scenarios_url) rep = client.get(scenarios_url) diff --git a/tests/rest/test_sequence.py b/tests/rest/test_sequence.py index 50961ef6aa..2e1cfe4d4d 100644 --- a/tests/rest/test_sequence.py +++ b/tests/rest/test_sequence.py @@ -36,8 +36,9 @@ def test_delete_sequence(client): rep = client.get(user_url) assert rep.status_code == 404 - with mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._delete"), mock.patch( - "taipy.core.sequence._sequence_manager._SequenceManager._get" + with ( + mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._delete"), + mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._get"), ): # test get_sequence rep = client.delete(url_for("api.sequence_by_id", sequence_id="foo")) @@ -73,7 +74,7 @@ def test_get_all_sequences(client, default_scenario_config_list): for ds in range(10): with mock.patch("taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock: config_mock.return_value = default_scenario_config_list[ds] - scenario_url = url_for("api.scenarios", config_id=config_mock.name) + scenario_url = url_for("api.scenarios", config_id=default_scenario_config_list[ds].name) client.post(scenario_url) sequences_url = url_for("api.sequences") diff --git a/tests/rest/test_task.py b/tests/rest/test_task.py index 1ce6d731d5..e221a22c40 100644 --- a/tests/rest/test_task.py +++ b/tests/rest/test_task.py @@ -34,8 +34,9 @@ def test_delete_task(client): rep = client.get(user_url) assert rep.status_code == 404 - with mock.patch("taipy.core.task._task_manager._TaskManager._delete"), mock.patch( - "taipy.core.task._task_manager._TaskManager._get" + with ( + mock.patch("taipy.core.task._task_manager._TaskManager._delete"), + mock.patch("taipy.core.task._task_manager._TaskManager._get"), ): # test get_task rep = client.delete(url_for("api.task_by_id", task_id="foo")) @@ -64,7 +65,7 @@ def test_get_all_tasks(client, task_data, default_task_config_list): for ds in range(10): with mock.patch("taipy.rest.api.resources.task.TaskList.fetch_config") as config_mock: config_mock.return_value = default_task_config_list[ds] - tasks_url = url_for("api.tasks", config_id=config_mock.name) + tasks_url = url_for("api.tasks", config_id=default_task_config_list[ds].name) client.post(tasks_url) rep = client.get(tasks_url)