Ver código fonte

Merge branch 'develop' into gmarabout/improve_dispatcher_shutdown

Grégoire Marabout 1 ano atrás
pai
commit
a3217b6278

+ 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):
 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):
     if name.isidentifier() and not keyword.iskeyword(name):
         return name
         return name
 
 
-    raise InvalidConfigurationId(f"{name} is not a valid identifier.")
+    raise InvalidConfigurationId(f"'{name}' is not a valid identifier.")

+ 31 - 0
taipy/core/common/_check_dependencies.py

@@ -0,0 +1,31 @@
+# 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.
+from importlib import util
+
+
+def _check_dependency_is_installed(module_name: str, package_name: str) -> None:
+    """
+        Check if a package is installed.
+
+        Args:
+            module_name: Name of the taipy module importing the package.
+            package_name: Name of the package.
+    .
+    """
+    extras = {
+        "boto3": "s3",
+        "pymongo": "mongo",
+    }
+    if not util.find_spec(package_name):
+        raise RuntimeError(
+            f"Cannot use {module_name} as {package_name} package is not installed. Please install it  "
+            f"using `pip install taipy[{extras.get(package_name)}]`."
+        )

+ 7 - 2
taipy/core/data/aws_s3.py

@@ -10,9 +10,13 @@
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
+from importlib import util
 from typing import Any, Dict, List, Optional, Set
 from typing import Any, Dict, List, Optional, Set
 
 
-import boto3
+from ..common._check_dependencies import _check_dependency_is_installed
+
+if util.find_spec("boto3"):
+    import boto3
 
 
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 
 
@@ -94,6 +98,7 @@ class S3ObjectDataNode(DataNode):
         editor_expiration_date: Optional[datetime] = None,
         editor_expiration_date: Optional[datetime] = None,
         properties: Optional[Dict] = None,
         properties: Optional[Dict] = None,
     ):
     ):
+        _check_dependency_is_installed("S3 Data Node", "boto3")
         if properties is None:
         if properties is None:
             properties = {}
             properties = {}
         required = self._REQUIRED_PROPERTIES
         required = self._REQUIRED_PROPERTIES
@@ -123,7 +128,7 @@ class S3ObjectDataNode(DataNode):
             aws_secret_access_key=properties.get(self.__AWS_SECRET_ACCESS_KEY),
             aws_secret_access_key=properties.get(self.__AWS_SECRET_ACCESS_KEY),
         )
         )
 
 
-        if not self._last_edit_date:
+        if not self._last_edit_date:  # type: ignore
             self._last_edit_date = datetime.now()
             self._last_edit_date = datetime.now()
 
 
         self._TAIPY_PROPERTIES.update(
         self._TAIPY_PROPERTIES.update(

+ 8 - 2
taipy/core/data/mongo.py

@@ -10,13 +10,18 @@
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
+from importlib import util
 from inspect import isclass
 from inspect import isclass
 from typing import Any, Dict, List, Optional, Set, Tuple, Union
 from typing import Any, Dict, List, Optional, Set, Tuple, Union
 
 
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 
 
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
-from ..common._mongo_connector import _connect_mongodb
+from ..common._check_dependencies import _check_dependency_is_installed
+
+if util.find_spec("pymongo"):
+    from ..common._mongo_connector import _connect_mongodb
+
 from ..data.operator import JoinOperator, Operator
 from ..data.operator import JoinOperator, Operator
 from ..exceptions.exceptions import InvalidCustomDocument, MissingRequiredProperty
 from ..exceptions.exceptions import InvalidCustomDocument, MissingRequiredProperty
 from .data_node import DataNode
 from .data_node import DataNode
@@ -99,6 +104,7 @@ class MongoCollectionDataNode(DataNode):
         editor_expiration_date: Optional[datetime] = None,
         editor_expiration_date: Optional[datetime] = None,
         properties: Dict = None,
         properties: Dict = None,
     ):
     ):
+        _check_dependency_is_installed("Mongo Data Node", "pymongo")
         if properties is None:
         if properties is None:
             properties = {}
             properties = {}
         required = self._REQUIRED_PROPERTIES
         required = self._REQUIRED_PROPERTIES
@@ -149,7 +155,7 @@ class MongoCollectionDataNode(DataNode):
         if callable(custom_encoder):
         if callable(custom_encoder):
             self._encoder = custom_encoder
             self._encoder = custom_encoder
 
 
-        if not self._last_edit_date:
+        if not self._last_edit_date:  # type: ignore
             self._last_edit_date = datetime.now()
             self._last_edit_date = datetime.now()
 
 
         self._TAIPY_PROPERTIES.update(
         self._TAIPY_PROPERTIES.update(

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

@@ -186,8 +186,18 @@ class InvalidSequence(Exception):
 class NonExistingSequence(Exception):
 class NonExistingSequence(Exception):
     """Raised if a requested Sequence is not known by the Sequence Manager."""
     """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):
 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
 # 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
 # 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.
-
 from __future__ import annotations
 from __future__ import annotations
 
 
 import pathlib
 import pathlib
@@ -37,6 +36,7 @@ from ..exceptions.exceptions import (
     NonExistingDataNode,
     NonExistingDataNode,
     NonExistingSequence,
     NonExistingSequence,
     NonExistingTask,
     NonExistingTask,
+    SequenceAlreadyExists,
     SequenceTaskDoesNotExistInScenario,
     SequenceTaskDoesNotExistInScenario,
 )
 )
 from ..job.job import Job
 from ..job.job import Job
@@ -194,11 +194,54 @@ class Scenario(_Entity, Submittable, _Labeled):
 
 
         Raises:
         Raises:
             SequenceTaskDoesNotExistInScenario^: If a task in the sequence does not exist in the scenario.
             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 = _Reloader()._reload(self._MANAGER_NAME, self)
         _scenario_task_ids = set(task.id if isinstance(task, Task) else task for task in _scenario._tasks)
         _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)
         _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)
         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 = _Reloader()._reload(self._MANAGER_NAME, self)._sequences
         _sequences.update(
         _sequences.update(
             {
             {
@@ -210,9 +253,7 @@ class Scenario(_Entity, Submittable, _Labeled):
             }
             }
         )
         )
         self.sequences = _sequences  # type: ignore
         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]]]):
     def add_sequences(self, sequences: Dict[str, Union[List[Task], List[TaskId]]]):
         """Add multiple sequences to the scenario.
         """Add multiple sequences to the scenario.
@@ -269,6 +310,29 @@ class Scenario(_Entity, Submittable, _Labeled):
             )
             )
         self.sequences = _sequences  # type: ignore
         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
     @staticmethod
     def __check_sequence_tasks_exist_in_scenario_tasks(
     def __check_sequence_tasks_exist_in_scenario_tasks(
         sequence_name: str, sequence_task_ids: Set[TaskId], scenario_id: ScenarioId, scenario_task_ids: Set[TaskId]
         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,
                 self.version,
             )
             )
             if not isinstance(p, Sequence):
             if not isinstance(p, Sequence):
-                raise NonExistingSequence(sequence_name)
+                raise NonExistingSequence(sequence_name, self.id)
             _sequences[sequence_name] = p
             _sequences[sequence_name] = p
         return _sequences
         return _sequences
 
 

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

@@ -74,7 +74,8 @@ class Sequence(_Entity, Submittable, _Labeled):
 
 
     @staticmethod
     @staticmethod
     def _new_id(sequence_name: str, scenario_id) -> SequenceId:
     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):
     def __hash__(self):
         return hash(self.id)
         return hash(self.id)

+ 3 - 0
taipy/core/setup.py

@@ -45,6 +45,9 @@ extras_require = {
     "mssql": ["pyodbc>=4,<4.1"],
     "mssql": ["pyodbc>=4,<4.1"],
     "mysql": ["pymysql>1,<1.1"],
     "mysql": ["pymysql>1,<1.1"],
     "postgresql": ["psycopg2>2.9,<2.10"],
     "postgresql": ["psycopg2>2.9,<2.10"],
+    "parquet": ["fastparquet==2022.11.0"],
+    "s3": ["boto3==1.29.1"],
+    "mongo": ["pymongo[srv]>=4.2.0,<5.0"],
 }
 }
 
 
 setup(
 setup(

+ 4 - 5
taipy/gui/viselements.json

@@ -561,8 +561,8 @@
             "doc": "Allows dynamic config refresh if set to True."
             "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."
             "doc": "A figure as produced by plotly."
           }
           }
         ]
         ]
@@ -768,14 +768,13 @@
                 "name": "title",
                 "name": "title",
                 "default_property": true,
                 "default_property": true,
                 "type": "str",
                 "type": "str",
-                "default_value": "Log in",
+                "default_value": "\"Log in\"",
                 "doc": "The title of the login dialog."
                 "doc": "The title of the login dialog."
             },
             },
             {
             {
                 "name": "on_action",
                 "name": "on_action",
                 "type": "Callback",
                 "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"]]
                 "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
 # 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
 # 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.
-
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 from unittest import mock
 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._data_manager_factory import _DataManagerFactory
 from taipy.core.data.in_memory import DataNode, InMemoryDataNode
 from taipy.core.data.in_memory import DataNode, InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
 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_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario_id import ScenarioId
 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)
     dn_2 = PickleDataNode("abc", Scope.SCENARIO)
     task = Task("qux", {}, print, [dn_1])
     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"]
     sequence = scenario.sequences["acb"]
     assert scenario.id is not None
     assert scenario.id is not None
     assert scenario.config_id == "quux"
     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():
 def test_create_scenario_invalid_config_id():
     with pytest.raises(InvalidConfigurationId):
     with pytest.raises(InvalidConfigurationId):
-        Scenario("foo bar", [], {})
+        Scenario("foo bar", set(), {})
 
 
 
 
 def test_create_scenario_and_add_sequences():
 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_1)
     task_manager._set(task_2)
     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": []}}
     scenario.sequences = {"sequence_1": {"tasks": [task_1]}, "sequence_2": {"tasks": []}}
     assert scenario.id is not None
     assert scenario.id is not None
     assert scenario.config_id == "scenario"
     assert scenario.config_id == "scenario"
@@ -160,7 +159,7 @@ def test_create_scenario_overlapping_sequences():
     task_manager._set(task_1)
     task_manager._set(task_1)
     task_manager._set(task_2)
     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_1", [task_1])
     scenario.add_sequence("sequence_2", [task_1, task_2])
     scenario.add_sequence("sequence_2", [task_1, task_2])
     assert scenario.id is not None
     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_1)
     task_manager._set(task_2)
     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.id is not None
     assert scenario.config_id == "scenario"
     assert scenario.config_id == "scenario"
     assert len(scenario.tasks) == 0
     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_1)
     task_manager._set(task_2)
     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.id is not None
     assert scenario.config_id == "scenario"
     assert scenario.config_id == "scenario"
     assert len(scenario.tasks) == 0
     assert len(scenario.tasks) == 0
@@ -251,7 +250,7 @@ def test_create_scenario_wth_additional_dns():
         additional_dn_2.config_id: additional_dn_2,
         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.id is not None
     assert scenario_1.config_id == "scenario_1"
     assert scenario_1.config_id == "scenario_1"
     assert len(scenario_1.tasks) == 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,
         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.id is not None
     assert scenario_2.config_id == "scenario_2"
     assert scenario_2.config_id == "scenario_2"
     assert len(scenario_2.tasks) == 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])
     task_2 = Task("task_2", {}, print, input=[data_node])
 
 
     with pytest.raises(SequenceTaskDoesNotExistInScenario) as err:
     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")
     assert err.value.args == ([task_1.id], "sequence", "SCENARIO_scenario")
 
 
     with pytest.raises(SequenceTaskDoesNotExistInScenario) as err:
     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")
     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(
         "scenario",
         "scenario",
         [task_1, task_2],
         [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_1 = Task("task_1", {}, print, output=[data_node])
     task_2 = Task("task_2", {}, print, input=[data_node])
     task_2 = Task("task_2", {}, print, input=[data_node])
     scenario = Scenario("scenario", [task_1], {})
     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])
     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():
 def test_add_property_to_scenario():
-    scenario = Scenario("foo", [], {"key": "value"})
+    scenario = Scenario("foo", set(), {"key": "value"})
     assert scenario.properties == {"key": "value"}
     assert scenario.properties == {"key": "value"}
     assert scenario.key == "value"
     assert scenario.key == "value"
 
 
@@ -358,7 +486,7 @@ def test_add_property_to_scenario():
 
 
 
 
 def test_add_cycle_to_scenario(cycle):
 def test_add_cycle_to_scenario(cycle):
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
     assert scenario.cycle is None
     assert scenario.cycle is None
     _CycleManagerFactory._build_manager()._set(cycle)
     _CycleManagerFactory._build_manager()._set(cycle)
     scenario.cycle = cycle
     scenario.cycle = cycle
@@ -367,7 +495,7 @@ def test_add_cycle_to_scenario(cycle):
 
 
 
 
 def test_add_and_remove_subscriber():
 def test_add_and_remove_subscriber():
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
 
 
     scenario._add_subscriber(print)
     scenario._add_subscriber(print)
     assert len(scenario.subscribers) == 1
     assert len(scenario.subscribers) == 1
@@ -377,7 +505,7 @@ def test_add_and_remove_subscriber():
 
 
 
 
 def test_add_and_remove_tag():
 def test_add_and_remove_tag():
-    scenario = Scenario("foo", [], {})
+    scenario = Scenario("foo", set(), {})
 
 
     assert len(scenario.tags) == 0
     assert len(scenario.tags) == 0
     scenario._add_tag("tag")
     scenario._add_tag("tag")
@@ -705,42 +833,42 @@ def test_auto_set_and_reload_properties():
 
 
 def test_is_deletable():
 def test_is_deletable():
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._is_deletable") as mock_submit:
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._is_deletable") as mock_submit:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.is_deletable()
         scenario.is_deletable()
         mock_submit.assert_called_once_with(scenario)
         mock_submit.assert_called_once_with(scenario)
 
 
 
 
 def test_submit_scenario():
 def test_submit_scenario():
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._submit") as mock_submit:
     with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._submit") as mock_submit:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.submit(force=False)
         scenario.submit(force=False)
         mock_submit.assert_called_once_with(scenario, None, False, False, None)
         mock_submit.assert_called_once_with(scenario, None, False, False, None)
 
 
 
 
 def test_subscribe_scenario():
 def test_subscribe_scenario():
     with mock.patch("taipy.core.subscribe_scenario") as mock_subscribe:
     with mock.patch("taipy.core.subscribe_scenario") as mock_subscribe:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.subscribe(None)
         scenario.subscribe(None)
         mock_subscribe.assert_called_once_with(None, None, scenario)
         mock_subscribe.assert_called_once_with(None, None, scenario)
 
 
 
 
 def test_unsubscribe_scenario():
 def test_unsubscribe_scenario():
     with mock.patch("taipy.core.unsubscribe_scenario") as mock_unsubscribe:
     with mock.patch("taipy.core.unsubscribe_scenario") as mock_unsubscribe:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.unsubscribe(None)
         scenario.unsubscribe(None)
         mock_unsubscribe.assert_called_once_with(None, None, scenario)
         mock_unsubscribe.assert_called_once_with(None, None, scenario)
 
 
 
 
 def test_add_tag_scenario():
 def test_add_tag_scenario():
     with mock.patch("taipy.core.tag") as mock_add_tag:
     with mock.patch("taipy.core.tag") as mock_add_tag:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.add_tag("tag")
         scenario.add_tag("tag")
         mock_add_tag.assert_called_once_with(scenario, "tag")
         mock_add_tag.assert_called_once_with(scenario, "tag")
 
 
 
 
 def test_remove_tag_scenario():
 def test_remove_tag_scenario():
     with mock.patch("taipy.core.untag") as mock_remove_tag:
     with mock.patch("taipy.core.untag") as mock_remove_tag:
-        scenario = Scenario("foo", [], {})
+        scenario = Scenario("foo", set(), {})
         scenario.remove_tag("tag")
         scenario.remove_tag("tag")
         mock_remove_tag.assert_called_once_with(scenario, "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]])
     _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():
 def test_check_consistency():
     data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
     data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
     data_node_2 = InMemoryDataNode("bar", Scope.SCENARIO, "s2")
     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,
     InvalidSequenceId,
     ModelNotFound,
     ModelNotFound,
     NonExistingSequence,
     NonExistingSequence,
+    SequenceAlreadyExists,
     SequenceBelongsToNonExistingScenario,
     SequenceBelongsToNonExistingScenario,
 )
 )
 from taipy.core.job._job_manager import _JobManager
 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 len(_SequenceManager._get(sequence_2).tasks) == 1
     assert _TaskManager._get(task.id).id == task.id
     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]
     sequence_1 = scenario.sequences[sequence_name_1]
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert len(_SequenceManager._get(sequence_id_1).tasks) == 0
     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 len(_SequenceManager._get(sequence_2).tasks) == 1
     assert _TaskManager._get(task.id).id == task.id
     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():
 def test_get_all_on_multiple_versions_environment():
     # Create 5 sequences from Scenario with 2 versions each
     # 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)
             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):
     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):
         with pytest.raises(NonExistingSequence):
             _SequenceManager._submit(sequence_id)
             _SequenceManager._submit(sequence_id)
 
 
@@ -668,31 +655,33 @@ def test_delete():
     with pytest.raises(ModelNotFound):
     with pytest.raises(ModelNotFound):
         _SequenceManager._delete(sequence_id)
         _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_1)
     _ScenarioManager._set(scenario_2)
     _ScenarioManager._set(scenario_2)
     with pytest.raises(ModelNotFound):
     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
     assert len(_SequenceManager._get_all()) == 1
-    _SequenceManager._delete(sequence_id)
+    _SequenceManager._delete(SequenceId(sequence_id))
     assert len(_SequenceManager._get_all()) == 0
     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
     assert len(_SequenceManager._get_all()) == 2
-    _SequenceManager._delete(sequence_id)
+    _SequenceManager._delete(SequenceId(sequence_id))
     assert len(_SequenceManager._get_all()) == 1
     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
     assert len(_SequenceManager._get_all()) == 5
     _SequenceManager._delete_all()
     _SequenceManager._delete_all()
     assert len(_SequenceManager._get_all()) == 0
     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
     assert len(_SequenceManager._get_all()) == 6
     _SequenceManager._delete_many(
     _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.config.job_config import JobConfig
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.in_memory import InMemoryDataNode
 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.job._job_manager import _JobManager
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario.scenario import Scenario
 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 _SequenceManager._get(sequence_2).id == sequence_2.id
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
     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]
     sequence_1 = scenario.sequences[sequence_name_1]
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert _SequenceManager._get(sequence_id_1).id == sequence_1.id
     assert len(_SequenceManager._get(sequence_id_1).tasks) == 0
     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 _SequenceManager._get(sequence_2).id == sequence_2.id
     assert len(_SequenceManager._get(sequence_2).tasks) == 1
     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):
 def test_get_all_on_multiple_versions_environment(init_sql_repo, init_managers):
     init_managers()
     init_managers()