Răsfoiți Sursa

Merge branch 'develop' into feature/split-data-folder

João André 1 an în urmă
părinte
comite
d1a43d2206

+ 4 - 4
taipy/config/common/_validate_id.py

@@ -17,11 +17,11 @@ __INVALID_TAIPY_ID_TERMS = ["CYCLE", "SCENARIO", "SEQUENCE", "TASK", "DATANODE"]
 
 
 def _validate_id(name: str):
-    for invalid_taipy_id_term in __INVALID_TAIPY_ID_TERMS:
-        if invalid_taipy_id_term in name:
-            raise InvalidConfigurationId(f"{name} is not a valid identifier. {invalid_taipy_id_term} is restricted.")
+    for invalid__id_term in __INVALID_TAIPY_ID_TERMS:
+        if invalid__id_term in name:
+            raise InvalidConfigurationId(f"'{name}' is not a valid identifier. '{invalid__id_term}' is restricted.")
 
     if name.isidentifier() and not keyword.iskeyword(name):
         return name
 
-    raise InvalidConfigurationId(f"{name} is not a valid identifier.")
+    raise InvalidConfigurationId(f"'{name}' is not a valid identifier.")

+ 12 - 2
taipy/core/exceptions/exceptions.py

@@ -186,8 +186,18 @@ class InvalidSequence(Exception):
 class NonExistingSequence(Exception):
     """Raised if a requested Sequence is not known by the Sequence Manager."""
 
-    def __init__(self, sequence_id: str):
-        self.message = f"Sequence: {sequence_id} does not exist."
+    def __init__(self, sequence_id: str, scenario_id: str=None):
+        if scenario_id:
+            self.message = f"Sequence: {sequence_id} does not exist in scenario {scenario_id}."
+        else:
+            self.message = f"Sequence: {sequence_id} does not exist."
+
+
+class SequenceAlreadyExists(Exception):
+    """Raised if a Sequence already exists."""
+
+    def __init__(self, sequence_name: str, scenario_id: str):
+        self.message = f"Sequence: {sequence_name} already exists in scenario {scenario_id}."
 
 
 class SequenceBelongsToNonExistingScenario(Exception):

+ 69 - 5
taipy/core/scenario/scenario.py

@@ -8,7 +8,6 @@
 # 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.
-
 from __future__ import annotations
 
 import pathlib
@@ -37,6 +36,7 @@ from ..exceptions.exceptions import (
     NonExistingDataNode,
     NonExistingSequence,
     NonExistingTask,
+    SequenceAlreadyExists,
     SequenceTaskDoesNotExistInScenario,
 )
 from ..job.job import Job
@@ -194,11 +194,54 @@ class Scenario(_Entity, Submittable, _Labeled):
 
         Raises:
             SequenceTaskDoesNotExistInScenario^: If a task in the sequence does not exist in the scenario.
+            SequenceAlreadyExists^: If a sequence with the same name already exists in the scenario.
+        """
+        if name in self.sequences:
+            raise SequenceAlreadyExists(name, self.id)
+        seq = self._set_sequence(name, tasks, properties, subscribers)
+        Notifier.publish(_make_event(seq, EventOperation.CREATION))
+
+    def update_sequence(
+        self,
+        name: str,
+        tasks: Union[List[Task], List[TaskId]],
+        properties: Optional[Dict] = None,
+        subscribers: Optional[List[_Subscriber]] = None,
+    ):
+        """Update an existing sequence.
+
+        Parameters:
+            name (str): The name of the sequence to update.
+            tasks (Union[List[Task], List[TaskId]]): The new list of scenario's tasks.
+            properties (Optional[Dict]): The new properties of the sequence.
+            subscribers (Optional[List[_Subscriber]]): The new list of callbacks to be called on `Job^`'s status change.
+
+        Raises:
+            SequenceTaskDoesNotExistInScenario^: If a task in the list does not exist in the scenario.
+            SequenceAlreadyExists^: If a sequence with the same name already exists in the scenario.
         """
+        if name not in self.sequences:
+            raise NonExistingSequence(name, self.id)
+        seq = self._set_sequence(name, tasks, properties, subscribers)
+        Notifier.publish(_make_event(seq, EventOperation.UPDATE))
+
+    def _set_sequence(
+        self,
+        name: str,
+        tasks: Union[List[Task], List[TaskId]],
+        properties: Optional[Dict] = None,
+        subscribers: Optional[List[_Subscriber]] = None,
+    ) -> Sequence:
         _scenario = _Reloader()._reload(self._MANAGER_NAME, self)
         _scenario_task_ids = set(task.id if isinstance(task, Task) else task for task in _scenario._tasks)
         _sequence_task_ids: Set[TaskId] = set(task.id if isinstance(task, Task) else task for task in tasks)
         self.__check_sequence_tasks_exist_in_scenario_tasks(name, _sequence_task_ids, self.id, _scenario_task_ids)
+        from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
+        seq_manager = _SequenceManagerFactory._build_manager()
+        seq = seq_manager._create(name, tasks, subscribers or [], properties or {}, self.id, self.version)
+        if not seq._is_consistent():
+            raise InvalidSequence(name)
+
         _sequences = _Reloader()._reload(self._MANAGER_NAME, self)._sequences
         _sequences.update(
             {
@@ -210,9 +253,7 @@ class Scenario(_Entity, Submittable, _Labeled):
             }
         )
         self.sequences = _sequences  # type: ignore
-        if not self.sequences[name]._is_consistent():
-            raise InvalidSequence(name)
-        Notifier.publish(_make_event(self.sequences[name], EventOperation.CREATION))
+        return seq
 
     def add_sequences(self, sequences: Dict[str, Union[List[Task], List[TaskId]]]):
         """Add multiple sequences to the scenario.
@@ -269,6 +310,29 @@ class Scenario(_Entity, Submittable, _Labeled):
             )
         self.sequences = _sequences  # type: ignore
 
+    def rename_sequence(self, old_name, new_name):
+        """Rename a sequence of the scenario.
+
+        Parameters:
+            old_name (str): The current name of the sequence to rename.
+            new_name (str): The new name of the sequence.
+
+        Raises:
+            SequenceAlreadyExists^: If a sequence with the same name already exists in the scenario.
+        """
+        if old_name == new_name:
+            return
+        if new_name in self.sequences:
+            raise SequenceAlreadyExists(new_name, self.id)
+        self._sequences[new_name] = self._sequences[old_name]
+        del self._sequences[old_name]
+        self.sequences = self._sequences  # type: ignore
+        Notifier.publish(Event(EventEntityType.SCENARIO,
+                               EventOperation.UPDATE,
+                               entity_id=self.id,
+                               attribute_name="sequences",
+                               attribute_value=self._sequences))
+
     @staticmethod
     def __check_sequence_tasks_exist_in_scenario_tasks(
         sequence_name: str, sequence_task_ids: Set[TaskId], scenario_id: ScenarioId, scenario_task_ids: Set[TaskId]
@@ -299,7 +363,7 @@ class Scenario(_Entity, Submittable, _Labeled):
                 self.version,
             )
             if not isinstance(p, Sequence):
-                raise NonExistingSequence(sequence_name)
+                raise NonExistingSequence(sequence_name, self.id)
             _sequences[sequence_name] = p
         return _sequences
 

+ 2 - 1
taipy/core/sequence/sequence.py

@@ -74,7 +74,8 @@ class Sequence(_Entity, Submittable, _Labeled):
 
     @staticmethod
     def _new_id(sequence_name: str, scenario_id) -> SequenceId:
-        return SequenceId(Sequence._SEPARATOR.join([Sequence._ID_PREFIX, _validate_id(sequence_name), scenario_id]))
+        seq_id = sequence_name.replace(" ", "_")
+        return SequenceId(Sequence._SEPARATOR.join([Sequence._ID_PREFIX, _validate_id(seq_id), scenario_id]))
 
     def __hash__(self):
         return hash(self.id)

+ 4 - 5
taipy/gui/viselements.json

@@ -561,8 +561,8 @@
             "doc": "Allows dynamic config refresh if set to True."
           },
           {
-            "name": "dynamic(figure)",
-            "type": "plotly.graph_objects.Figure",
+            "name": "figure",
+            "type": "dynamic(plotly.graph_objects.Figure)",
             "doc": "A figure as produced by plotly."
           }
         ]
@@ -768,14 +768,13 @@
                 "name": "title",
                 "default_property": true,
                 "type": "str",
-                "default_value": "Log in",
+                "default_value": "\"Log in\"",
                 "doc": "The title of the login dialog."
             },
             {
                 "name": "on_action",
                 "type": "Callback",
-                "default_value": "on_login",
-                "doc": "The name of the function that is triggered when the dialog's button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:<ul><li>The first element is the user name</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul>",
+                "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
                 "signature": [["state", "State"], ["id", "str"], ["payload", "dict"]]
               },
               {

+ 151 - 82
tests/core/scenario/test_scenario.py

@@ -8,7 +8,6 @@
 # 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.
-
 from datetime import datetime, timedelta
 from unittest import mock
 
@@ -23,7 +22,7 @@ from taipy.core.cycle.cycle import Cycle, CycleId
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.in_memory import DataNode, InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
-from taipy.core.exceptions.exceptions import SequenceTaskDoesNotExistInScenario
+from taipy.core.exceptions.exceptions import SequenceAlreadyExists, SequenceTaskDoesNotExistInScenario
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario_id import ScenarioId
@@ -83,7 +82,7 @@ def test_create_scenario_with_task_and_additional_dn_and_sequence():
     dn_2 = PickleDataNode("abc", Scope.SCENARIO)
     task = Task("qux", {}, print, [dn_1])
 
-    scenario = Scenario("quux", set([task]), {}, set([dn_2]), sequences={"acb": {"tasks": [task]}})
+    scenario = Scenario("quux", {task}, {}, {dn_2}, sequences={"acb": {"tasks": [task]}})
     sequence = scenario.sequences["acb"]
     assert scenario.id is not None
     assert scenario.config_id == "quux"
@@ -101,7 +100,7 @@ def test_create_scenario_with_task_and_additional_dn_and_sequence():
 
 def test_create_scenario_invalid_config_id():
     with pytest.raises(InvalidConfigurationId):
-        Scenario("foo bar", [], {})
+        Scenario("foo bar", set(), {})
 
 
 def test_create_scenario_and_add_sequences():
@@ -123,7 +122,7 @@ def test_create_scenario_and_add_sequences():
     task_manager._set(task_1)
     task_manager._set(task_2)
 
-    scenario = Scenario("scenario", set([task_1]), {})
+    scenario = Scenario("scenario", {task_1}, {})
     scenario.sequences = {"sequence_1": {"tasks": [task_1]}, "sequence_2": {"tasks": []}}
     assert scenario.id is not None
     assert scenario.config_id == "scenario"
@@ -160,7 +159,7 @@ def test_create_scenario_overlapping_sequences():
     task_manager._set(task_1)
     task_manager._set(task_2)
 
-    scenario = Scenario("scenario", set([task_1, task_2]), {})
+    scenario = Scenario("scenario", {task_1, task_2}, {})
     scenario.add_sequence("sequence_1", [task_1])
     scenario.add_sequence("sequence_2", [task_1, task_2])
     assert scenario.id is not None
@@ -204,7 +203,7 @@ def test_create_scenario_one_additional_dn():
     task_manager._set(task_1)
     task_manager._set(task_2)
 
-    scenario = Scenario("scenario", set(), {}, set([additional_dn_1]))
+    scenario = Scenario("scenario", set(), {}, {additional_dn_1})
     assert scenario.id is not None
     assert scenario.config_id == "scenario"
     assert len(scenario.tasks) == 0
@@ -235,7 +234,7 @@ def test_create_scenario_wth_additional_dns():
     task_manager._set(task_1)
     task_manager._set(task_2)
 
-    scenario = Scenario("scenario", set(), {}, set([additional_dn_1, additional_dn_2]))
+    scenario = Scenario("scenario", set(), {}, {additional_dn_1, additional_dn_2})
     assert scenario.id is not None
     assert scenario.config_id == "scenario"
     assert len(scenario.tasks) == 0
@@ -251,7 +250,7 @@ def test_create_scenario_wth_additional_dns():
         additional_dn_2.config_id: additional_dn_2,
     }
 
-    scenario_1 = Scenario("scenario_1", set([task_1]), {}, set([additional_dn_1]))
+    scenario_1 = Scenario("scenario_1", {task_1}, {}, {additional_dn_1})
     assert scenario_1.id is not None
     assert scenario_1.config_id == "scenario_1"
     assert len(scenario_1.tasks) == 1
@@ -267,7 +266,7 @@ def test_create_scenario_wth_additional_dns():
         additional_dn_1.config_id: additional_dn_1,
     }
 
-    scenario_2 = Scenario("scenario_2", set([task_1, task_2]), {}, set([additional_dn_1, additional_dn_2]))
+    scenario_2 = Scenario("scenario_2", {task_1, task_2}, {}, {additional_dn_1, additional_dn_2})
     assert scenario_2.id is not None
     assert scenario_2.config_id == "scenario_2"
     assert len(scenario_2.tasks) == 2
@@ -293,7 +292,7 @@ def test_raise_sequence_tasks_not_in_scenario(data_node):
     task_2 = Task("task_2", {}, print, input=[data_node])
 
     with pytest.raises(SequenceTaskDoesNotExistInScenario) as err:
-        Scenario("scenario", [], {}, sequences={"sequence": {"tasks": [task_1]}}, scenario_id="SCENARIO_scenario")
+        Scenario("scenario", set(), {}, sequences={"sequence": {"tasks": [task_1]}}, scenario_id="SCENARIO_scenario")
     assert err.value.args == ([task_1.id], "sequence", "SCENARIO_scenario")
 
     with pytest.raises(SequenceTaskDoesNotExistInScenario) as err:
@@ -306,7 +305,7 @@ def test_raise_sequence_tasks_not_in_scenario(data_node):
         )
     assert err.value.args == ([task_2.id], "sequence", "SCENARIO_scenario")
 
-    Scenario("scenario", [task_1], {}, sequences={"sequence": {"tasks": [task_1]}})
+    Scenario("scenario", {task_1}, {}, sequences={"sequence": {"tasks": [task_1]}})
     Scenario(
         "scenario",
         [task_1, task_2],
@@ -315,7 +314,7 @@ def test_raise_sequence_tasks_not_in_scenario(data_node):
     )
 
 
-def test_raise_tasks_not_in_scenario_with_add_sequence_api(data_node):
+def test_adding_sequence_raises_tasks_not_in_scenario(data_node):
     task_1 = Task("task_1", {}, print, output=[data_node])
     task_2 = Task("task_2", {}, print, input=[data_node])
     scenario = Scenario("scenario", [task_1], {})
@@ -345,8 +344,137 @@ def test_raise_tasks_not_in_scenario_with_add_sequence_api(data_node):
     scenario.add_sequence("sequence_6", [task_1, task_2])
 
 
+def test_adding_existing_sequence_raises_exception(data_node):
+    task_1 = Task("task_1", {}, print, output=[data_node])
+    _TaskManagerFactory._build_manager()._set(task_1)
+    task_2 = Task("task_2", {}, print, input=[data_node])
+    _TaskManagerFactory._build_manager()._set(task_2)
+    scenario = Scenario("scenario", tasks={task_1, task_2}, properties={})
+    _ScenarioManagerFactory._build_manager()._set(scenario)
+
+    scenario.add_sequence("sequence_1", [task_1])
+    with pytest.raises(SequenceAlreadyExists):
+        scenario.add_sequence("sequence_1", [task_2])
+
+
+def test_renaming_existing_sequence_raises_exception(data_node):
+    task_1 = Task("task_1", {}, print, output=[data_node])
+    _TaskManagerFactory._build_manager()._set(task_1)
+    task_2 = Task("task_2", {}, print, input=[data_node])
+    _TaskManagerFactory._build_manager()._set(task_2)
+    scenario = Scenario("scenario", {task_1, task_2}, {})
+    _ScenarioManagerFactory._build_manager()._set(scenario)
+
+    scenario.add_sequence("sequence_1", [task_1])
+    scenario.add_sequence("sequence_2", [task_2])
+    with pytest.raises(SequenceAlreadyExists):
+        scenario.rename_sequence("sequence_1", "sequence_2")
+
+
+def test_add_rename_and_remove_sequences():
+    data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
+    data_node_2 = InMemoryDataNode("bar", Scope.SCENARIO, "s2")
+    data_node_3 = InMemoryDataNode("qux", Scope.SCENARIO, "s3")
+    data_node_4 = InMemoryDataNode("quux", Scope.SCENARIO, "s4")
+    data_node_5 = InMemoryDataNode("quuz", Scope.SCENARIO, "s5")
+    task_1 = Task("grault",{}, print,[data_node_1, data_node_2],[data_node_3], TaskId("t1"))
+    task_2 = Task("garply", {}, print, [data_node_3], id=TaskId("t2"))
+    task_3 = Task("waldo", {}, print, [data_node_3], None, id=TaskId("t3"))
+    task_4 = Task("fred", {}, print, [data_node_3], [data_node_4], TaskId("t4"))
+    task_5 = Task("bob", {}, print, [data_node_5], [data_node_3], TaskId("t5"))
+    scenario = Scenario("quest", {task_1, task_2, task_3, task_4, task_5}, {}, [], scenario_id=ScenarioId("s1"))
+
+    sequence_1 = Sequence({"name": "seq_1"}, [task_1], SequenceId(f"SEQUENCE_seq_1_{scenario.id}"))
+    sequence_2 = Sequence({"name": "seq_2"}, [task_1, task_2], SequenceId(f"SEQUENCE_seq_2_{scenario.id}"))
+    new_seq_2 = Sequence({"name": "seq_2"}, [task_1, task_2], SequenceId(f"SEQUENCE_new_seq_2_{scenario.id}"))
+    sequence_3 = Sequence({"name": "seq_3"}, [task_1, task_5, task_3], SequenceId(f"SEQUENCE_seq_3_{scenario.id}"))
+
+    task_manager = _TaskManagerFactory._build_manager()
+    data_manager = _DataManagerFactory._build_manager()
+    scenario_manager = _ScenarioManagerFactory._build_manager()
+    for dn in [data_node_1, data_node_2, data_node_3, data_node_4, data_node_5]:
+        data_manager._set(dn)
+    for t in [task_1, task_2, task_3, task_4, task_5]:
+        task_manager._set(t)
+    scenario_manager._set(scenario)
+
+    assert scenario.get_inputs() == {data_node_1, data_node_2, data_node_5}
+    assert scenario._get_set_of_tasks() == {task_1, task_2, task_3, task_4, task_5}
+    assert len(scenario.sequences) == 0
+
+    scenario.sequences = {"seq_1": {"tasks": [task_1]}}
+    assert scenario.sequences == {"seq_1": sequence_1}
+
+    scenario.add_sequences({"seq_2": [task_1, task_2]})
+    assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2}
+
+    scenario.remove_sequences(["seq_1"])
+    assert scenario.sequences == {"seq_2": sequence_2}
+
+    scenario.add_sequences({"seq_1": [task_1], "seq 3": [task_1, task_5, task_3]})
+    assert scenario.sequences == {"seq_2": sequence_2, "seq_1": sequence_1, "seq 3": sequence_3}
+
+    scenario.remove_sequences(["seq_2", "seq 3"])
+    assert scenario.sequences == {"seq_1": sequence_1}
+
+    scenario.add_sequence("seq_2", [task_1, task_2])
+    assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2}
+
+    scenario.add_sequence("seq 3", [task_1.id, task_5.id, task_3.id])
+    assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2, "seq 3": sequence_3}
+
+    scenario.remove_sequence("seq_1")
+    assert scenario.sequences == {"seq_2": sequence_2, "seq 3": sequence_3}
+
+    scenario.rename_sequence("seq_2", "new_seq_2")
+    assert scenario.sequences == {"new_seq_2": new_seq_2, "seq 3": sequence_3}
+
+
+def test_update_sequence(data_node):
+    task_1 = Task("foo",{}, print,[data_node],[], TaskId("t1"))
+    task_2 = Task("bar", {}, print, [], [data_node], id=TaskId("t2"))
+    scenario = Scenario("baz", {task_1, task_2}, {})
+    scenario.add_sequence("seq_1", [task_1])
+
+    assert len(scenario.sequences) == 1
+    assert scenario.sequences["seq_1"].tasks == {"foo": task_1}
+    assert scenario.sequences["seq_1"].name == "seq_1"
+    scenario.update_sequence("seq_1", [task_2], {"new_key": "new_value"}, [])
+    assert len(scenario.sequences) == 1
+    assert scenario.sequences["seq_1"].tasks == {"bar": task_2}
+    assert scenario.sequences["seq_1"].name == "seq_1"
+    assert scenario.sequences["seq_1"].properties["new_key"] == "new_value"
+
+
+def test_add_rename_and_remove_sequences_within_context(data_node):
+    task_1 = Task("task_1", {}, print, output=[data_node])
+    task_2 = Task("task_2", {}, print, input=[data_node])
+    _TaskManagerFactory._build_manager()._set(task_1)
+    scenario = Scenario(config_id="scenario", tasks={task_1, task_2}, properties={})
+    _ScenarioManagerFactory._build_manager()._set(scenario)
+
+    with scenario as sc:
+        sc.add_sequence("seq_name", [task_1])
+    assert len(scenario.sequences) == 1
+    assert scenario.sequences["seq_name"].tasks == {"task_1": task_1}
+
+    with scenario as sc:
+        sc.update_sequence("seq_name", [task_2])
+    assert len(scenario.sequences) == 1
+    assert scenario.sequences["seq_name"].tasks == {"task_2": task_2}
+
+    with scenario as sc:
+        sc.rename_sequence("seq_name", "seq name")
+    assert scenario.sequences["seq name"].tasks == {"task_2": task_2}
+
+    with scenario as sc:
+        sc.remove_sequence("seq name")
+    assert len(scenario.sequences) == 0
+
+
+
 def test_add_property_to_scenario():
-    scenario = Scenario("foo", [], {"key": "value"})
+    scenario = Scenario("foo", set(), {"key": "value"})
     assert scenario.properties == {"key": "value"}
     assert scenario.key == "value"
 
@@ -358,7 +486,7 @@ def test_add_property_to_scenario():
 
 
 def test_add_cycle_to_scenario(cycle):
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
     assert scenario.cycle is None
     _CycleManagerFactory._build_manager()._set(cycle)
     scenario.cycle = cycle
@@ -367,7 +495,7 @@ def test_add_cycle_to_scenario(cycle):
 
 
 def test_add_and_remove_subscriber():
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
 
     scenario._add_subscriber(print)
     assert len(scenario.subscribers) == 1
@@ -377,7 +505,7 @@ def test_add_and_remove_subscriber():
 
 
 def test_add_and_remove_tag():
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
 
     assert len(scenario.tags) == 0
     scenario._add_tag("tag")
@@ -705,42 +833,42 @@ def test_auto_set_and_reload_properties():
 
 def test_is_deletable():
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._is_deletable") as mock_submit:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.is_deletable()
         mock_submit.assert_called_once_with(scenario)
 
 
 def test_submit_scenario():
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._submit") as mock_submit:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.submit(force=False)
         mock_submit.assert_called_once_with(scenario, None, False, False, None)
 
 
 def test_subscribe_scenario():
     with mock.patch("taipy.core.subscribe_scenario") as mock_subscribe:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.subscribe(None)
         mock_subscribe.assert_called_once_with(None, None, scenario)
 
 
 def test_unsubscribe_scenario():
     with mock.patch("taipy.core.unsubscribe_scenario") as mock_unsubscribe:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.unsubscribe(None)
         mock_unsubscribe.assert_called_once_with(None, None, scenario)
 
 
 def test_add_tag_scenario():
     with mock.patch("taipy.core.tag") as mock_add_tag:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.add_tag("tag")
         mock_add_tag.assert_called_once_with(scenario, "tag")
 
 
 def test_remove_tag_scenario():
     with mock.patch("taipy.core.untag") as mock_remove_tag:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.remove_tag("tag")
         mock_remove_tag.assert_called_once_with(scenario, "tag")
 
@@ -1227,65 +1355,6 @@ def test_get_sorted_tasks():
     _assert_equal(scenario_8._get_sorted_tasks(), [[task_5, task_2, task_1], [task_3, task_4]])
 
 
-def test_add_and_remove_sequences():
-    data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
-    data_node_2 = InMemoryDataNode("bar", Scope.SCENARIO, "s2")
-    data_node_3 = InMemoryDataNode("qux", Scope.SCENARIO, "s3")
-    data_node_4 = InMemoryDataNode("quux", Scope.SCENARIO, "s4")
-    data_node_5 = InMemoryDataNode("quuz", Scope.SCENARIO, "s5")
-    task_1 = Task(
-        "grault",
-        {},
-        print,
-        [data_node_1, data_node_2],
-        [data_node_3],
-        TaskId("t1"),
-    )
-    task_2 = Task("garply", {}, print, [data_node_3], id=TaskId("t2"))
-    task_3 = Task("waldo", {}, print, [data_node_3], None, id=TaskId("t3"))
-    task_4 = Task("fred", {}, print, [data_node_3], [data_node_4], TaskId("t4"))
-    task_5 = Task("bob", {}, print, [data_node_5], [data_node_3], TaskId("t5"))
-    scenario_1 = Scenario("quest", [task_1, task_2, task_3, task_4, task_5], {}, [], scenario_id=ScenarioId("s1"))
-
-    sequence_1 = Sequence({"name": "sequence_1"}, [task_1], SequenceId(f"SEQUENCE_sequence_1_{scenario_1.id}"))
-    sequence_2 = Sequence({"name": "sequence_2"}, [task_1, task_2], SequenceId(f"SEQUENCE_sequence_2_{scenario_1.id}"))
-    sequence_3 = Sequence(
-        {"name": "sequence_3"}, [task_1, task_5, task_3], SequenceId(f"SEQUENCE_sequence_3_{scenario_1.id}")
-    )
-
-    task_manager = _TaskManagerFactory._build_manager()
-    data_manager = _DataManagerFactory._build_manager()
-    scenario_manager = _ScenarioManagerFactory._build_manager()
-    for dn in [data_node_1, data_node_2, data_node_3, data_node_4, data_node_5]:
-        data_manager._set(dn)
-    for t in [task_1, task_2, task_3, task_4, task_5]:
-        task_manager._set(t)
-    scenario_manager._set(scenario_1)
-
-    assert scenario_1.get_inputs() == {data_node_1, data_node_2, data_node_5}
-    assert scenario_1._get_set_of_tasks() == {task_1, task_2, task_3, task_4, task_5}
-    assert len(scenario_1.sequences) == 0
-
-    scenario_1.sequences = {"sequence_1": {"tasks": [task_1]}}
-    assert scenario_1.sequences == {"sequence_1": sequence_1}
-
-    scenario_1.add_sequences({"sequence_2": [task_1, task_2]})
-    assert scenario_1.sequences == {"sequence_1": sequence_1, "sequence_2": sequence_2}
-
-    scenario_1.remove_sequences(["sequence_1"])
-    assert scenario_1.sequences == {"sequence_2": sequence_2}
-
-    scenario_1.add_sequences({"sequence_1": [task_1], "sequence_3": [task_1, task_5, task_3]})
-    assert scenario_1.sequences == {
-        "sequence_2": sequence_2,
-        "sequence_1": sequence_1,
-        "sequence_3": sequence_3,
-    }
-
-    scenario_1.remove_sequences(["sequence_2", "sequence_3"])
-    assert scenario_1.sequences == {"sequence_1": sequence_1}
-
-
 def test_check_consistency():
     data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
     data_node_2 = InMemoryDataNode("bar", Scope.SCENARIO, "s2")

+ 18 - 29
tests/core/sequence/test_sequence_manager.py

@@ -31,6 +31,7 @@ from taipy.core.exceptions.exceptions import (
     InvalidSequenceId,
     ModelNotFound,
     NonExistingSequence,
+    SequenceAlreadyExists,
     SequenceBelongsToNonExistingScenario,
 )
 from taipy.core.job._job_manager import _JobManager
@@ -123,8 +124,9 @@ def test_set_and_get():
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
     assert _TaskManager._get(task.id).id == task.id
 
-    # We save the first sequence again. We expect nothing to change
-    scenario.add_sequence(sequence_name_1, [])
+    # We save the first sequence again. We expect an exception and nothing to change
+    with pytest.raises(SequenceAlreadyExists):
+       scenario.add_sequence(sequence_name_1, [])
     sequence_1 = scenario.sequences[sequence_name_1]
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert len(_SequenceManager._get(sequence_id_1).tasks) == 0
@@ -136,21 +138,6 @@ def test_set_and_get():
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
     assert _TaskManager._get(task.id).id == task.id
 
-    # We save a third sequence with same name as the first one.
-    # We expect the first sequence to be updated
-    scenario.add_sequences({sequence_name_1: [task]})
-    sequence_3 = scenario.sequences[sequence_name_1]
-    assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
-    assert _SequenceManager._get(sequence_id_1).id == sequence_3.id
-    assert len(_SequenceManager._get(sequence_id_1).tasks) == 1
-    assert _SequenceManager._get(sequence_1).id == sequence_1.id
-    assert len(_SequenceManager._get(sequence_1).tasks) == 1
-    assert _SequenceManager._get(sequence_id_2).id == sequence_2.id
-    assert len(_SequenceManager._get(sequence_id_2).tasks) == 1
-    assert _SequenceManager._get(sequence_2).id == sequence_2.id
-    assert len(_SequenceManager._get(sequence_2).tasks) == 1
-    assert _TaskManager._get(task.id).id == task.id
-
 
 def test_get_all_on_multiple_versions_environment():
     # Create 5 sequences from Scenario with 2 versions each
@@ -272,7 +259,7 @@ def test_submit():
             return super()._lock_dn_output_and_create_job(task, submit_id, submit_entity_id, callbacks, force)
 
     with mock.patch("taipy.core.task._task_manager._TaskManager._orchestrator", new=MockOrchestrator):
-        # sequence does not exists. We expect an exception to be raised
+        # sequence does not exist. We expect an exception to be raised
         with pytest.raises(NonExistingSequence):
             _SequenceManager._submit(sequence_id)
 
@@ -668,31 +655,33 @@ def test_delete():
     with pytest.raises(ModelNotFound):
         _SequenceManager._delete(sequence_id)
 
-    scenario_1 = Scenario("scenario_1", [], {}, scenario_id="SCENARIO_scenario_id_1")
-    scenario_2 = Scenario("scenario_2", [], {}, scenario_id="SCENARIO_scenario_id_2")
+    scenario_1 = Scenario("scenario_1", set(), {}, scenario_id="SCENARIO_scenario_id_1")
+    scenario_2 = Scenario("scenario_2", set(), {}, scenario_id="SCENARIO_scenario_id_2")
     _ScenarioManager._set(scenario_1)
     _ScenarioManager._set(scenario_2)
     with pytest.raises(ModelNotFound):
-        _SequenceManager._delete(sequence_id)
+        _SequenceManager._delete(SequenceId(sequence_id))
 
-    scenario_1.add_sequences({"sequence": {}})
+    scenario_1.add_sequences({"sequence": []})
     assert len(_SequenceManager._get_all()) == 1
-    _SequenceManager._delete(sequence_id)
+    _SequenceManager._delete(SequenceId(sequence_id))
     assert len(_SequenceManager._get_all()) == 0
 
-    scenario_1.add_sequences({"sequence": {}, "sequence_1": {}})
+    scenario_1.add_sequences({"sequence": [], "sequence_1": []})
     assert len(_SequenceManager._get_all()) == 2
-    _SequenceManager._delete(sequence_id)
+    _SequenceManager._delete(SequenceId(sequence_id))
     assert len(_SequenceManager._get_all()) == 1
 
-    scenario_1.add_sequences({"sequence_1": {}, "sequence_2": {}, "sequence_3": {}})
-    scenario_2.add_sequences({"sequence_1_2": {}, "sequence_2_2": {}})
+    with pytest.raises(SequenceAlreadyExists):
+        scenario_1.add_sequences({"sequence_1": [], "sequence_2": [], "sequence_3": []})
+    scenario_1.add_sequences({"sequence_2": [], "sequence_3": []})
+    scenario_2.add_sequences({"sequence_1_2": [], "sequence_2_2": []})
     assert len(_SequenceManager._get_all()) == 5
     _SequenceManager._delete_all()
     assert len(_SequenceManager._get_all()) == 0
 
-    scenario_1.add_sequences({"sequence_1": {}, "sequence_2": {}, "sequence_3": {}, "sequence_4": {}})
-    scenario_2.add_sequences({"sequence_1_2": {}, "sequence_2_2": {}})
+    scenario_1.add_sequences({"sequence_1": [], "sequence_2": [], "sequence_3": [], "sequence_4": []})
+    scenario_2.add_sequences({"sequence_1_2": [], "sequence_2_2": []})
     assert len(_SequenceManager._get_all()) == 6
     _SequenceManager._delete_many(
         [

+ 4 - 17
tests/core/sequence/test_sequence_manager_with_sql_repo.py

@@ -18,6 +18,7 @@ from taipy.core._version._version_manager import _VersionManager
 from taipy.core.config.job_config import JobConfig
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.in_memory import InMemoryDataNode
+from taipy.core.exceptions import SequenceAlreadyExists
 from taipy.core.job._job_manager import _JobManager
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario.scenario import Scenario
@@ -74,8 +75,9 @@ def test_set_and_get_sequence(init_sql_repo, init_managers):
     assert _SequenceManager._get(sequence_2).id == sequence_2.id
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
 
-    # We save the first sequence again. We expect nothing to change
-    scenario.add_sequences({sequence_name_1: {}})
+    # We save the first sequence again. We expect an exception and nothing to change
+    with pytest.raises(SequenceAlreadyExists):
+        scenario.add_sequences({sequence_name_1: {}})
     sequence_1 = scenario.sequences[sequence_name_1]
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert len(_SequenceManager._get(sequence_id_1).tasks) == 0
@@ -86,21 +88,6 @@ def test_set_and_get_sequence(init_sql_repo, init_managers):
     assert _SequenceManager._get(sequence_2).id == sequence_2.id
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
 
-    # We save a third sequence with same id as the first one.
-    # We expect the first sequence to be updated
-    scenario.add_sequences({sequence_name_1: [task]})
-    sequence_3 = scenario.sequences[sequence_name_1]
-    assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
-    assert _SequenceManager._get(sequence_id_1).id == sequence_3.id
-    assert len(_SequenceManager._get(sequence_id_1).tasks) == 1
-    assert _SequenceManager._get(sequence_1).id == sequence_1.id
-    assert len(_SequenceManager._get(sequence_1).tasks) == 1
-    assert _SequenceManager._get(sequence_id_2).id == sequence_2.id
-    assert len(_SequenceManager._get(sequence_id_2).tasks) == 1
-    assert _SequenceManager._get(sequence_2).id == sequence_2.id
-    assert len(_SequenceManager._get(sequence_2).tasks) == 1
-    assert _TaskManager._get(task.id).id == task.id
-
 
 def test_get_all_on_multiple_versions_environment(init_sql_repo, init_managers):
     init_managers()