Przeglądaj źródła

Merge branch 'develop' into 1297-value-format

Nam Nguyen 11 miesięcy temu
rodzic
commit
9fe190a422

+ 3 - 3
taipy/core/_entity/_ready_to_run_property.py

@@ -11,8 +11,8 @@
 
 from typing import TYPE_CHECKING, Dict, Set, Union
 
-from ..common.reason import Reason
 from ..notification import EventOperation, Notifier, _make_event
+from ..reason.reason import Reasons
 
 if TYPE_CHECKING:
     from ..data.data_node import DataNode, DataNodeId
@@ -29,7 +29,7 @@ class _ReadyToRunProperty:
 
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # the data nodes that make it not ready_to_run with the reason(s)
-    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Reason] = {}
+    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Reasons] = {}
 
     @classmethod
     def _add(cls, dn: "DataNode", reason: str) -> None:
@@ -81,7 +81,7 @@ class _ReadyToRunProperty:
             cls.__publish_submittable_property_event(submittable, False)
 
         if submittable.id not in cls._submittable_id_datanodes:
-            cls._submittable_id_datanodes[submittable.id] = Reason(submittable.id)
+            cls._submittable_id_datanodes[submittable.id] = Reasons(submittable.id)
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
 
     @staticmethod

+ 6 - 5
taipy/core/_entity/submittable.py

@@ -17,9 +17,10 @@ import networkx as nx
 
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
-from ..common.reason import Reason
 from ..data.data_node import DataNode
 from ..job.job import Job
+from ..reason._reason_factory import _build_data_node_is_being_edited_reason, _build_data_node_is_not_written
+from ..reason.reason import Reasons
 from ..submission.submission import Submission
 from ..task.task import Task
 from ._dag import _DAG
@@ -82,20 +83,20 @@ class Submittable:
         all_data_nodes_in_dag = {node for node in dag.nodes if isinstance(node, DataNode)}
         return all_data_nodes_in_dag - self.__get_inputs(dag) - self.__get_outputs(dag)
 
-    def is_ready_to_run(self) -> Reason:
+    def is_ready_to_run(self) -> Reasons:
         """Indicate if the entity is ready to be run.
 
         Returns:
             A Reason object that can function as a Boolean value.
             which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
         """
-        reason = Reason(self._submittable_id)
+        reason = Reasons(self._submittable_id)
 
         for node in self.get_inputs():
             if node._edit_in_progress:
-                reason._add_reason(node.id, node._build_edit_in_progress_reason())
+                reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
             if not node._last_edit_date:
-                reason._add_reason(node.id, node._build_not_written_reason())
+                reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
 
         return reason
 

+ 0 - 4
taipy/core/_manager/_manager.py

@@ -27,10 +27,6 @@ class _Manager(Generic[EntityType]):
     _logger = _TaipyLogger._get_logger()
     _ENTITY_NAME: str = "Entity"
 
-    @classmethod
-    def _build_not_submittable_entity_reason(cls, entity_id: str) -> str:
-        return f"Entity {entity_id} is not a submittable entity"
-
     @classmethod
     def _delete_all(cls):
         """

+ 14 - 14
taipy/core/_orchestrator/_orchestrator.py

@@ -164,7 +164,7 @@ class _Orchestrator(_AbstractOrchestrator):
         )
 
     @classmethod
-    def _update_submission_status(cls, job: Job):
+    def _update_submission_status(cls, job: Job) -> None:
         submission_manager = _SubmissionManagerFactory._build_manager()
         if submission := submission_manager._get(job.submit_id):
             submission_manager._update_submission_status(submission, job)
@@ -182,7 +182,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls.__logger.error(f"Job {job.id} status: {job.status}")
 
     @classmethod
-    def _orchestrate_job_to_run_or_block(cls, jobs: List[Job]):
+    def _orchestrate_job_to_run_or_block(cls, jobs: List[Job]) -> None:
         blocked_jobs = []
         pending_jobs = []
 
@@ -199,7 +199,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls.jobs_to_run.put(job)
 
     @classmethod
-    def _wait_until_job_finished(cls, jobs: Union[List[Job], Job], timeout: float = 0):
+    def _wait_until_job_finished(cls, jobs: Union[List[Job], Job], timeout: float = 0) -> None:
         #  Note: this method should be prefixed by two underscores, but it has only one, so it can be mocked in tests.
         def __check_if_timeout(st, to):
             return (datetime.now() - st).seconds < to
@@ -231,13 +231,13 @@ class _Orchestrator(_AbstractOrchestrator):
         return any(not data_manager._get(dn.id).is_ready_for_reading for dn in input_data_nodes)
 
     @staticmethod
-    def _unlock_edit_on_jobs_outputs(jobs: Union[Job, List[Job], Set[Job]]):
+    def _unlock_edit_on_jobs_outputs(jobs: Union[Job, List[Job], Set[Job]]) -> None:
         jobs = [jobs] if isinstance(jobs, Job) else jobs
         for job in jobs:
             job._unlock_edit_on_outputs()
 
     @classmethod
-    def _on_status_change(cls, job: Job):
+    def _on_status_change(cls, job: Job) -> None:
         if job.is_completed() or job.is_skipped():
             cls.__logger.debug(f"{job.id} has been completed or skipped. Unblocking jobs.")
             cls.__unblock_jobs()
@@ -245,7 +245,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls._fail_subsequent_jobs(job)
 
     @classmethod
-    def __unblock_jobs(cls):
+    def __unblock_jobs(cls) -> None:
         with cls.lock:
             cls.__logger.debug("Acquiring lock to unblock jobs.")
             for job in cls.blocked_jobs:
@@ -258,14 +258,14 @@ class _Orchestrator(_AbstractOrchestrator):
                     cls.jobs_to_run.put(job)
 
     @classmethod
-    def __remove_blocked_job(cls, job):
+    def __remove_blocked_job(cls, job: Job) -> None:
         try:  # In case the job has been removed from the list of blocked_jobs.
             cls.blocked_jobs.remove(job)
         except Exception:
             cls.__logger.warning(f"{job.id} is not in the blocked list anymore.")
 
     @classmethod
-    def cancel_job(cls, job: Job):
+    def cancel_job(cls, job: Job) -> None:
         if job.is_canceled():
             cls.__logger.info(f"{job.id} has already been canceled.")
         elif job.is_abandoned():
@@ -298,13 +298,13 @@ class _Orchestrator(_AbstractOrchestrator):
         return subsequent_jobs
 
     @classmethod
-    def __remove_blocked_jobs(cls, jobs):
+    def __remove_blocked_jobs(cls, jobs: Set[Job]) -> None:
         for job in jobs:
             cls.__remove_blocked_job(job)
 
     @classmethod
-    def __remove_jobs_to_run(cls, jobs):
-        new_jobs_to_run = Queue()
+    def __remove_jobs_to_run(cls, jobs: Set[Job]) -> None:
+        new_jobs_to_run: Queue = Queue()
         while not cls.jobs_to_run.empty():
             current_job = cls.jobs_to_run.get()
             if current_job not in jobs:
@@ -312,7 +312,7 @@ class _Orchestrator(_AbstractOrchestrator):
         cls.jobs_to_run = new_jobs_to_run
 
     @classmethod
-    def _fail_subsequent_jobs(cls, failed_job: Job):
+    def _fail_subsequent_jobs(cls, failed_job: Job) -> None:
         with cls.lock:
             cls.__logger.debug("Acquiring lock to fail subsequent jobs.")
             to_fail_or_abandon_jobs = set()
@@ -327,7 +327,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls._unlock_edit_on_jobs_outputs(to_fail_or_abandon_jobs)
 
     @classmethod
-    def _cancel_jobs(cls, job_id_to_cancel: JobId, jobs: Set[Job]):
+    def _cancel_jobs(cls, job_id_to_cancel: JobId, jobs: Set[Job]) -> None:
         for job in jobs:
             if job.is_running():
                 cls.__logger.info(f"{job.id} is running and cannot be canceled.")
@@ -341,7 +341,7 @@ class _Orchestrator(_AbstractOrchestrator):
                 job.abandoned()
 
     @staticmethod
-    def _check_and_execute_jobs_if_development_mode():
+    def _check_and_execute_jobs_if_development_mode() -> None:
         from ._orchestrator_factory import _OrchestratorFactory
 
         if dispatcher := _OrchestratorFactory._dispatcher:

+ 1 - 1
taipy/core/_repository/_filesystem_repository.py

@@ -117,7 +117,7 @@ class _FileSystemRepository(_AbstractRepository[ModelType, Entity]):
     def _search(self, attribute: str, value: Any, filters: Optional[List[Dict]] = None) -> List[Entity]:
         return list(self.__search(attribute, value, filters))
 
-    def _export(self, entity_id: str, folder_path: Union[str, pathlib.Path]):
+    def _export(self, entity_id: str, folder_path: Union[str, pathlib.Path]) -> None:
         if isinstance(folder_path, str):
             folder: pathlib.Path = pathlib.Path(folder_path)
         else:

+ 3 - 3
taipy/core/cycle/_cycle_manager.py

@@ -33,7 +33,7 @@ class _CycleManager(_Manager[Cycle]):
     @classmethod
     def _create(
         cls, frequency: Frequency, name: Optional[str] = None, creation_date: Optional[datetime] = None, **properties
-    ):
+    ) -> Cycle:
         creation_date = creation_date if creation_date else datetime.now()
         start_date = _CycleManager._get_start_date_of_cycle(frequency, creation_date)
         end_date = _CycleManager._get_end_date_of_cycle(frequency, start_date)
@@ -63,7 +63,7 @@ class _CycleManager(_Manager[Cycle]):
             return cls._create(frequency=frequency, creation_date=creation_date, name=name)
 
     @staticmethod
-    def _get_start_date_of_cycle(frequency: Frequency, creation_date: datetime):
+    def _get_start_date_of_cycle(frequency: Frequency, creation_date: datetime) -> datetime:
         start_date = creation_date.date()
         start_time = time()
         if frequency == Frequency.DAILY:
@@ -77,7 +77,7 @@ class _CycleManager(_Manager[Cycle]):
         return datetime.combine(start_date, start_time)
 
     @staticmethod
-    def _get_end_date_of_cycle(frequency: Frequency, start_date: datetime):
+    def _get_end_date_of_cycle(frequency: Frequency, start_date: datetime) -> datetime:
         end_date = start_date
         if frequency == Frequency.DAILY:
             end_date = end_date + timedelta(days=1)

+ 7 - 7
taipy/core/data/_data_manager.py

@@ -111,25 +111,25 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         return cls._repository._load_all(filters)
 
     @classmethod
-    def _clean_generated_file(cls, data_node: DataNode):
+    def _clean_generated_file(cls, data_node: DataNode) -> None:
         if not isinstance(data_node, _FileDataNodeMixin):
             return
         if data_node.is_generated and os.path.exists(data_node.path):
             os.remove(data_node.path)
 
     @classmethod
-    def _clean_generated_files(cls, data_nodes: Iterable[DataNode]):
+    def _clean_generated_files(cls, data_nodes: Iterable[DataNode]) -> None:
         for data_node in data_nodes:
             cls._clean_generated_file(data_node)
 
     @classmethod
-    def _delete(cls, data_node_id: DataNodeId):
+    def _delete(cls, data_node_id: DataNodeId) -> None:
         if data_node := cls._get(data_node_id, None):
             cls._clean_generated_file(data_node)
         super()._delete(data_node_id)
 
     @classmethod
-    def _delete_many(cls, data_node_ids: Iterable[DataNodeId]):
+    def _delete_many(cls, data_node_ids: Iterable[DataNodeId]) -> None:
         data_nodes = []
         for data_node_id in data_node_ids:
             if data_node := cls._get(data_node_id):
@@ -138,13 +138,13 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         super()._delete_many(data_node_ids)
 
     @classmethod
-    def _delete_all(cls):
+    def _delete_all(cls) -> None:
         data_nodes = cls._get_all()
         cls._clean_generated_files(data_nodes)
         super()._delete_all()
 
     @classmethod
-    def _delete_by_version(cls, version_number: str):
+    def _delete_by_version(cls, version_number: str) -> None:
         data_nodes = cls._get_all(version_number)
         cls._clean_generated_files(data_nodes)
         cls._repository._delete_by(attribute="version", value=version_number)
@@ -165,7 +165,7 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         return cls._repository._load_all(filters)
 
     @classmethod
-    def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs):
+    def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs) -> None:
         cls._repository._export(id, folder_path)
 
         if not kwargs.get("include_data"):

+ 0 - 6
taipy/core/data/data_node.py

@@ -229,9 +229,6 @@ class DataNode(_Entity, _Labeled):
     def last_edit_date(self, val):
         self._last_edit_date = val
 
-    def _build_not_written_reason(self) -> str:
-        return f"DataNode {self.id} is not written"
-
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def scope(self):
@@ -297,9 +294,6 @@ class DataNode(_Entity, _Labeled):
     def edit_in_progress(self, val):
         self._edit_in_progress = val
 
-    def _build_edit_in_progress_reason(self) -> str:
-        return f"DataNode {self.id} is being edited"
-
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def editor_id(self):

+ 2 - 2
taipy/core/job/_job_manager.py

@@ -58,7 +58,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
         return job
 
     @classmethod
-    def _delete(cls, job: Union[Job, JobId], force=False):
+    def _delete(cls, job: Union[Job, JobId], force=False) -> None:
         if isinstance(job, str):
             job = cls._get(job)
         if cls._is_deletable(job) or force:
@@ -69,7 +69,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
             raise err
 
     @classmethod
-    def _cancel(cls, job: Union[str, Job]):
+    def _cancel(cls, job: Union[str, Job]) -> None:
         job = cls._get(job) if isinstance(job, str) else job
 
         from .._orchestrator._orchestrator_factory import _OrchestratorFactory

+ 12 - 0
taipy/core/reason/__init__.py

@@ -0,0 +1,12 @@
+# 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 .reason import Reasons

+ 24 - 0
taipy/core/reason/_reason_factory.py

@@ -0,0 +1,24 @@
+# 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 ..data.data_node import DataNodeId
+
+
+def _build_data_node_is_being_edited_reason(dn_id: DataNodeId) -> str:
+    return f"DataNode {dn_id} is being edited"
+
+
+def _build_data_node_is_not_written(dn_id: DataNodeId) -> str:
+    return f"DataNode {dn_id} is not written"
+
+
+def _build_not_submittable_entity_reason(entity_id: str) -> str:
+    return f"Entity {entity_id} is not a submittable entity"

+ 3 - 3
taipy/core/common/reason.py → taipy/core/reason/reason.py

@@ -12,18 +12,18 @@
 from typing import Dict, Set
 
 
-class Reason:
+class Reasons:
     def __init__(self, entity_id: str) -> None:
         self.entity_id: str = entity_id
         self._reasons: Dict[str, Set[str]] = {}
 
-    def _add_reason(self, entity_id: str, reason: str) -> "Reason":
+    def _add_reason(self, entity_id: str, reason: str) -> "Reasons":
         if entity_id not in self._reasons:
             self._reasons[entity_id] = set()
         self._reasons[entity_id].add(reason)
         return self
 
-    def _remove_reason(self, entity_id: str, reason: str) -> "Reason":
+    def _remove_reason(self, entity_id: str, reason: str) -> "Reasons":
         if entity_id in self._reasons and reason in self._reasons[entity_id]:
             self._reasons[entity_id].remove(reason)
             if len(self._reasons[entity_id]) == 0:

+ 20 - 22
taipy/core/scenario/_scenario_manager.py

@@ -23,7 +23,6 @@ from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
-from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.scenario_config import ScenarioConfig
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
@@ -38,7 +37,6 @@ from ..exceptions.exceptions import (
     ImportScenarioDoesntHaveAVersion,
     InsufficientScenarioToCompare,
     InvalidScenario,
-    InvalidSequence,
     NonExistingComparator,
     NonExistingScenario,
     NonExistingScenarioConfig,
@@ -48,6 +46,8 @@ from ..exceptions.exceptions import (
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
+from ..reason._reason_factory import _build_not_submittable_entity_reason
+from ..reason.reason import Reasons
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission.submission import Submission
 from ..task._task_manager_factory import _TaskManagerFactory
@@ -76,7 +76,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         callback: Callable[[Scenario, Job], None],
         params: Optional[List[Any]] = None,
         scenario: Optional[Scenario] = None,
-    ):
+    ) -> None:
         if scenario is None:
             scenarios = cls._get_all()
             for scn in scenarios:
@@ -91,7 +91,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         callback: Callable[[Scenario, Job], None],
         params: Optional[List[Any]] = None,
         scenario: Optional[Scenario] = None,
-    ):
+    ) -> None:
         if scenario is None:
             scenarios = cls._get_all()
             for scn in scenarios:
@@ -101,14 +101,14 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         cls.__remove_subscriber(callback, params, scenario)
 
     @classmethod
-    def __add_subscriber(cls, callback, params, scenario: Scenario):
+    def __add_subscriber(cls, callback, params, scenario: Scenario) -> None:
         scenario._add_subscriber(callback, params)
         Notifier.publish(
             _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params)
         )
 
     @classmethod
-    def __remove_subscriber(cls, callback, params, scenario: Scenario):
+    def __remove_subscriber(cls, callback, params, scenario: Scenario) -> None:
         scenario._remove_subscriber(callback, params)
         Notifier.publish(
             _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params)
@@ -190,24 +190,22 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         if not scenario._is_consistent():
             raise InvalidScenario(scenario.id)
 
-        actual_sequences = scenario._get_sequences()
-        for sequence_name in sequences.keys():
-            if not actual_sequences[sequence_name]._is_consistent():
-                raise InvalidSequence(actual_sequences[sequence_name].id)
-            Notifier.publish(_make_event(actual_sequences[sequence_name], EventOperation.CREATION))
+        from ..sequence._sequence_manager_factory import _SequenceManagerFactory
+
+        _SequenceManagerFactory._build_manager()._bulk_create_from_scenario(scenario)
 
         Notifier.publish(_make_event(scenario, EventOperation.CREATION))
         return scenario
 
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reason:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reasons:
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
 
         if not isinstance(scenario, Scenario):
             scenario = str(scenario)
-            reason = Reason((scenario))
-            reason._add_reason(scenario, cls._build_not_submittable_entity_reason(scenario))
+            reason = Reasons((scenario))
+            reason._add_reason(scenario, _build_not_submittable_entity_reason(scenario))
             return reason
 
         return scenario.is_ready_to_run()
@@ -313,7 +311,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return False
 
     @classmethod
-    def _set_primary(cls, scenario: Scenario):
+    def _set_primary(cls, scenario: Scenario) -> None:
         if not scenario.cycle:
             raise DoesNotBelongToACycle(
                 f"Can't set scenario {scenario.id} to primary because it doesn't belong to a cycle."
@@ -326,7 +324,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         scenario.is_primary = True  # type: ignore
 
     @classmethod
-    def _tag(cls, scenario: Scenario, tag: str):
+    def _tag(cls, scenario: Scenario, tag: str) -> None:
         tags = scenario.properties.get(cls._AUTHORIZED_TAGS_KEY, set())
         if len(tags) > 0 and tag not in tags:
             raise UnauthorizedTagError(f"Tag `{tag}` not authorized by scenario configuration `{scenario.config_id}`")
@@ -341,7 +339,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
 
     @classmethod
-    def _untag(cls, scenario: Scenario, tag: str):
+    def _untag(cls, scenario: Scenario, tag: str) -> None:
         scenario._remove_tag(tag)
         cls._set(scenario)
         Notifier.publish(
@@ -349,14 +347,14 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
 
     @classmethod
-    def _compare(cls, *scenarios: Scenario, data_node_config_id: Optional[str] = None):
+    def _compare(cls, *scenarios: Scenario, data_node_config_id: Optional[str] = None) -> Dict:
         if len(scenarios) < 2:
             raise InsufficientScenarioToCompare("At least two scenarios are required to compare.")
 
         if not all(scenarios[0].config_id == scenario.config_id for scenario in scenarios):
             raise DifferentScenarioConfigs("Scenarios to compare must have the same configuration.")
 
-        if scenario_config := _ScenarioManager.__get_config(scenarios[0]):
+        if scenario_config := cls.__get_config(scenarios[0]):
             results = {}
             if data_node_config_id:
                 if data_node_config_id in scenario_config.comparators.keys():
@@ -391,7 +389,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return True
 
     @classmethod
-    def _delete(cls, scenario_id: ScenarioId):
+    def _delete(cls, scenario_id: ScenarioId) -> None:
         scenario = cls._get(scenario_id)
         if not cls._is_deletable(scenario):
             raise DeletingPrimaryScenario(
@@ -403,7 +401,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         super()._delete(scenario_id)
 
     @classmethod
-    def _hard_delete(cls, scenario_id: ScenarioId):
+    def _hard_delete(cls, scenario_id: ScenarioId) -> None:
         scenario = cls._get(scenario_id)
         if not cls._is_deletable(scenario):
             raise DeletingPrimaryScenario(
@@ -418,7 +416,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
             cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
     @classmethod
-    def _delete_by_version(cls, version_number: str):
+    def _delete_by_version(cls, version_number: str) -> None:
         """
         Deletes scenario by the version number.
 

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

@@ -268,12 +268,11 @@ class Scenario(_Entity, Submittable, _Labeled):
         _scenario_task_ids = {task.id if isinstance(task, Task) else task for task in _scenario._tasks}
         _sequence_task_ids: Set[TaskId] = {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(
@@ -391,7 +390,7 @@ class Scenario(_Entity, Submittable, _Labeled):
         sequence_manager = _SequenceManagerFactory._build_manager()
 
         for sequence_name, sequence_data in self._sequences.items():
-            p = sequence_manager._create(
+            sequence = sequence_manager._build_sequence(
                 sequence_name,
                 sequence_data.get(self._SEQUENCE_TASKS_KEY, []),
                 sequence_data.get(self._SEQUENCE_SUBSCRIBERS_KEY, []),
@@ -399,9 +398,9 @@ class Scenario(_Entity, Submittable, _Labeled):
                 self.id,
                 self.version,
             )
-            if not isinstance(p, Sequence):
+            if not isinstance(sequence, Sequence):
                 raise NonExistingSequence(sequence_name, self.id)
-            _sequences[sequence_name] = p
+            _sequences[sequence_name] = sequence
         return _sequences
 
     @property  # type: ignore

+ 73 - 26
taipy/core/sequence/_sequence_manager.py

@@ -18,9 +18,9 @@ from .._entity._entity_ids import _EntityIds
 from .._manager._manager import _Manager
 from .._version._version_mixin import _VersionMixin
 from ..common._utils import _Subscriber
-from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..exceptions.exceptions import (
+    InvalidSequence,
     InvalidSequenceId,
     ModelNotFound,
     NonExistingSequence,
@@ -31,6 +31,8 @@ from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification.event import _make_event
+from ..reason._reason_factory import _build_not_submittable_entity_reason
+from ..reason.reason import Reasons
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario.scenario import Scenario
 from ..scenario.scenario_id import ScenarioId
@@ -48,7 +50,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
     _model_name = "sequences"
 
     @classmethod
-    def _delete(cls, sequence_id: SequenceId):
+    def _delete(cls, sequence_id: SequenceId) -> None:
         """
         Deletes a Sequence by id.
         """
@@ -63,7 +65,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         raise ModelNotFound(cls._model_name, sequence_id)
 
     @classmethod
-    def _delete_all(cls):
+    def _delete_all(cls) -> None:
         """
         Deletes all Sequences.
         """
@@ -74,7 +76,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             Notifier.publish(Event(cls._EVENT_ENTITY_TYPE, EventOperation.DELETION, metadata={"delete_all": True}))
 
     @classmethod
-    def _delete_many(cls, sequence_ids: Iterable[str]):
+    def _delete_many(cls, sequence_ids: Iterable[SequenceId]) -> None:
         """
         Deletes Sequence entities by a list of Sequence ids.
         """
@@ -103,7 +105,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             raise ModelNotFound(cls._model_name, sequence_id) from None
 
     @classmethod
-    def _delete_by_version(cls, version_number: str):
+    def _delete_by_version(cls, version_number: str) -> None:
         """
         Deletes Sequences by version number.
         """
@@ -111,14 +113,14 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             cls._delete_many(scenario.sequences.values())
 
     @classmethod
-    def _hard_delete(cls, sequence_id: SequenceId):
+    def _hard_delete(cls, sequence_id: SequenceId) -> None:
         sequence = cls._get(sequence_id)
         entity_ids_to_delete = cls._get_children_entity_ids(sequence)
         entity_ids_to_delete.sequence_ids.add(sequence.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
     @classmethod
-    def _set(cls, sequence: Sequence):
+    def _set(cls, sequence: Sequence) -> None:
         """
         Save or update a Sequence.
         """
@@ -137,18 +139,8 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             cls._logger.error(f"Sequence {sequence.id} belongs to a non-existing Scenario {scenario_id}.")
             raise SequenceBelongsToNonExistingScenario(sequence.id, scenario_id)
 
-    @classmethod
-    def _create(
-        cls,
-        sequence_name: str,
-        tasks: Union[List[Task], List[TaskId]],
-        subscribers: Optional[List[_Subscriber]] = None,
-        properties: Optional[Dict] = None,
-        scenario_id: Optional[ScenarioId] = None,
-        version: Optional[str] = None,
-    ) -> Sequence:
-        sequence_id = Sequence._new_id(sequence_name, scenario_id)
-
+    @staticmethod
+    def __get_sequence_tasks(tasks: Union[List[Task], List[TaskId]]) -> List[Task]:
         task_manager = _TaskManagerFactory._build_manager()
         _tasks: List[Task] = []
         for task in tasks:
@@ -158,11 +150,24 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
                 _tasks.append(_task)
             else:
                 raise NonExistingTask(task)
+        return _tasks
 
+    @classmethod
+    def _build_sequence(
+        cls,
+        sequence_name: str,
+        tasks: Union[List[Task], List[TaskId]],
+        subscribers: Optional[List[_Subscriber]] = None,
+        properties: Optional[Dict] = None,
+        scenario_id: Optional[ScenarioId] = None,
+        version: Optional[str] = None,
+    ) -> Sequence:
+        sequence_id = Sequence._new_id(sequence_name, scenario_id)
+        _tasks = cls.__get_sequence_tasks(tasks)
         properties = properties if properties else {}
         properties["name"] = sequence_name
         version = version if version else cls._get_latest_version()
-        sequence = Sequence(
+        return Sequence(
             properties=properties,
             tasks=_tasks,
             sequence_id=sequence_id,
@@ -171,10 +176,52 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             subscribers=subscribers,
             version=version,
         )
+
+    @classmethod
+    def _bulk_create_from_scenario(cls, scenario: Scenario) -> Dict[str, Sequence]:
+        _sequences: Dict[str, Sequence] = {}
+
+        for sequence_name, sequence_data in scenario._sequences.items():
+            sequence = cls._create(
+                sequence_name,
+                sequence_data.get(scenario._SEQUENCE_TASKS_KEY, []),
+                sequence_data.get(scenario._SEQUENCE_SUBSCRIBERS_KEY, []),
+                sequence_data.get(scenario._SEQUENCE_PROPERTIES_KEY, {}),
+                scenario.id,
+                scenario.version,
+            )
+            if not isinstance(sequence, Sequence):
+                raise NonExistingSequence(sequence_name, scenario.id)
+            _sequences[sequence_name] = sequence
+
+            Notifier.publish(_make_event(sequence, EventOperation.CREATION))
+
+        return _sequences
+
+    @classmethod
+    def _create(
+        cls,
+        sequence_name: str,
+        tasks: Union[List[Task], List[TaskId]],
+        subscribers: Optional[List[_Subscriber]] = None,
+        properties: Optional[Dict] = None,
+        scenario_id: Optional[ScenarioId] = None,
+        version: Optional[str] = None,
+    ) -> Sequence:
+        task_manager = _TaskManagerFactory._build_manager()
+        _tasks = cls.__get_sequence_tasks(tasks)
+
+        sequence = cls._build_sequence(sequence_name, _tasks, subscribers, properties, scenario_id, version)
+        sequence_id = sequence.id
+
         for task in _tasks:
             if sequence_id not in task._parent_ids:
                 task._parent_ids.update([sequence_id])
                 task_manager._set(task)
+
+        if not sequence._is_consistent():
+            raise InvalidSequence(sequence_id)
+
         return sequence
 
     @classmethod
@@ -264,7 +311,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         callback: Callable[[Sequence, Job], None],
         params: Optional[List[Any]] = None,
         sequence: Optional[Sequence] = None,
-    ):
+    ) -> None:
         if sequence is None:
             sequences = cls._get_all()
             for pln in sequences:
@@ -278,7 +325,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         callback: Callable[[Sequence, Job], None],
         params: Optional[List[Any]] = None,
         sequence: Optional[Sequence] = None,
-    ):
+    ) -> None:
         if sequence is None:
             sequences = cls._get_all()
             for pln in sequences:
@@ -297,14 +344,14 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
 
     @classmethod
-    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reason:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reasons:
         if isinstance(sequence, str):
             sequence = cls._get(sequence)
 
         if not isinstance(sequence, Sequence):
             sequence = str(sequence)
-            reason = Reason(sequence)
-            reason._add_reason(sequence, cls._build_not_submittable_entity_reason(sequence))
+            reason = Reasons(sequence)
+            reason._add_reason(sequence, _build_not_submittable_entity_reason(sequence))
             return reason
 
         return sequence.is_ready_to_run()
@@ -352,7 +399,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         return True if cls._get(entity_id) else False
 
     @classmethod
-    def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs):
+    def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs) -> None:
         """
         Export a Sequence entity.
         """

+ 13 - 13
taipy/core/submission/_submission_manager.py

@@ -54,7 +54,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
         return submission
 
     @classmethod
-    def _update_submission_status(cls, submission: Submission, job: Job):
+    def _update_submission_status(cls, submission: Submission, job: Job) -> None:
         with cls.__lock:
             submission = cls._get(submission)
 
@@ -95,25 +95,25 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             # The submission_status is set later to make sure notification for updating
             # the submission_status attribute is triggered
             if submission._is_canceled:
-                cls._set_submission_status(submission, SubmissionStatus.CANCELED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.CANCELED, job)
             elif submission._is_abandoned:
-                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
             elif submission._running_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.RUNNING, job)
+                cls.__set_submission_status(submission, SubmissionStatus.RUNNING, job)
             elif submission._pending_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.PENDING, job)
+                cls.__set_submission_status(submission, SubmissionStatus.PENDING, job)
             elif submission._blocked_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.BLOCKED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.BLOCKED, job)
             elif submission._is_completed:
-                cls._set_submission_status(submission, SubmissionStatus.COMPLETED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.COMPLETED, job)
             else:
-                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
             cls.__logger.debug(
                 f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`"
             )
 
     @classmethod
-    def _set_submission_status(cls, submission: Submission, new_submission_status: SubmissionStatus, job: Job):
+    def __set_submission_status(cls, submission: Submission, new_submission_status: SubmissionStatus, job: Job) -> None:
         if not submission._is_in_context:
             submission = cls._get(submission)
         _current_submission_status = submission._submission_status
@@ -147,7 +147,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             return max(submissions_of_task)
 
     @classmethod
-    def _delete(cls, submission: Union[Submission, SubmissionId]):
+    def _delete(cls, submission: Union[Submission, SubmissionId]) -> None:
         if isinstance(submission, str):
             submission = cls._get(submission)
         if cls._is_deletable(submission):
@@ -158,14 +158,14 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             raise err
 
     @classmethod
-    def _hard_delete(cls, submission_id: SubmissionId):
+    def _hard_delete(cls, submission_id: SubmissionId) -> None:
         submission = cls._get(submission_id)
         entity_ids_to_delete = cls._get_children_entity_ids(submission)
         entity_ids_to_delete.submission_ids.add(submission.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
     @classmethod
-    def _get_children_entity_ids(cls, submission: Submission):
+    def _get_children_entity_ids(cls, submission: Submission) -> _EntityIds:
         entity_ids = _EntityIds()
 
         for job in submission.jobs:
@@ -174,7 +174,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
         return entity_ids
 
     @classmethod
-    def _is_deletable(cls, submission: Union[Submission, SubmissionId]) -> bool:  # type: ignore
+    def _is_deletable(cls, submission: Union[Submission, SubmissionId]) -> bool:
         if isinstance(submission, str):
             submission = cls._get(submission)
         return submission.is_finished() or submission.submission_status == SubmissionStatus.UNDEFINED

+ 4 - 3
taipy/core/taipy.py

@@ -33,7 +33,6 @@ from .common._check_instance import (
     _is_task,
 )
 from .common._warnings import _warn_deprecated, _warn_no_core_service
-from .common.reason import Reason
 from .config.data_node_config import DataNodeConfig
 from .config.scenario_config import ScenarioConfig
 from .cycle._cycle_manager_factory import _CycleManagerFactory
@@ -52,6 +51,8 @@ from .exceptions.exceptions import (
 from .job._job_manager_factory import _JobManagerFactory
 from .job.job import Job
 from .job.job_id import JobId
+from .reason._reason_factory import _build_not_submittable_entity_reason
+from .reason.reason import Reasons
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario.scenario import Scenario
 from .scenario.scenario_id import ScenarioId
@@ -90,7 +91,7 @@ def set(entity: Union[DataNode, Task, Sequence, Scenario, Cycle, Submission]):
         return _SubmissionManagerFactory._build_manager()._set(entity)
 
 
-def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> Reason:
+def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> Reasons:
     """Indicate if an entity can be submitted.
 
     This function checks if the given entity can be submitted for execution.
@@ -110,7 +111,7 @@ def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Tas
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
-    return Reason(str(entity))._add_reason(str(entity), _Manager._build_not_submittable_entity_reason(str(entity)))
+    return Reasons(str(entity))._add_reason(str(entity), _build_not_submittable_entity_reason(str(entity)))
 
 
 def is_editable(

+ 16 - 11
taipy/core/task/_task_manager.py

@@ -20,13 +20,18 @@ from .._orchestrator._abstract_orchestrator import _AbstractOrchestrator
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
-from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.task_config import TaskConfig
 from ..cycle.cycle_id import CycleId
 from ..data._data_manager_factory import _DataManagerFactory
 from ..exceptions.exceptions import NonExistingTask
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
+from ..reason._reason_factory import (
+    _build_data_node_is_being_edited_reason,
+    _build_data_node_is_not_written,
+    _build_not_submittable_entity_reason,
+)
+from ..reason.reason import Reasons
 from ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..submission.submission import Submission
@@ -46,7 +51,7 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return _OrchestratorFactory._build_orchestrator()
 
     @classmethod
-    def _set(cls, task: Task):
+    def _set(cls, task: Task) -> None:
         cls.__save_data_nodes(task.input.values())
         cls.__save_data_nodes(task.output.values())
         super()._set(task)
@@ -130,20 +135,20 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return cls._repository._load_all(filters)
 
     @classmethod
-    def __save_data_nodes(cls, data_nodes):
+    def __save_data_nodes(cls, data_nodes) -> None:
         data_manager = _DataManagerFactory._build_manager()
         for i in data_nodes:
             data_manager._set(i)
 
     @classmethod
-    def _hard_delete(cls, task_id: TaskId):
+    def _hard_delete(cls, task_id: TaskId) -> None:
         task = cls._get(task_id)
         entity_ids_to_delete = cls._get_children_entity_ids(task)
         entity_ids_to_delete.task_ids.add(task.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
     @classmethod
-    def _get_children_entity_ids(cls, task: Task):
+    def _get_children_entity_ids(cls, task: Task) -> _EntityIds:
         entity_ids = _EntityIds()
 
         from ..job._job_manager_factory import _JobManagerFactory
@@ -164,22 +169,22 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
 
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reason:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reasons:
         if isinstance(task, str):
             task = cls._get(task)
         if not isinstance(task, Task):
             task = str(task)
-            reason = Reason(task)
-            reason._add_reason(task, cls._build_not_submittable_entity_reason(task))
+            reason = Reasons(task)
+            reason._add_reason(task, _build_not_submittable_entity_reason(task))
         else:
-            reason = Reason(task.id)
+            reason = Reasons(task.id)
             data_manager = _DataManagerFactory._build_manager()
             for node in task.input.values():
                 node = data_manager._get(node)
                 if node._edit_in_progress:
-                    reason._add_reason(node.id, node._build_edit_in_progress_reason())
+                    reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
                 if not node._last_edit_date:
-                    reason._add_reason(node.id, node._build_not_written_reason())
+                    reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
 
         return reason
 

+ 10 - 10
tests/core/_entity/test_ready_to_run_property.py

@@ -14,7 +14,7 @@ from taipy import ScenarioId, SequenceId, TaskId
 from taipy.config.common.frequency import Frequency
 from taipy.config.config import Config
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
-from taipy.core.common.reason import Reason
+from taipy.core.reason.reason import Reasons
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
@@ -33,7 +33,7 @@ def test_scenario_without_input_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
 
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
@@ -46,7 +46,7 @@ def test_scenario_submittable_with_inputs_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
 
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
@@ -61,7 +61,7 @@ def test_scenario_submittable_even_with_output_not_ready_to_run():
     dn_3 = scenario.dn_3
 
     assert not dn_3.is_ready_for_reading
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
@@ -78,7 +78,7 @@ def test_scenario_not_submittable_not_in_property_because_it_is_lazy():
     assert dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
 
     # Since it is a lazy property, the scenario and the datanodes is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
@@ -97,7 +97,7 @@ def test_scenario_not_submittable_if_one_input_edit_in_progress():
 
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
@@ -125,7 +125,7 @@ def test_scenario_not_submittable_for_multiple_reasons():
     assert not dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
@@ -156,7 +156,7 @@ def test_writing_input_remove_reasons():
 
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
     # Since it is a lazy property, the scenario is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
@@ -171,7 +171,7 @@ def test_writing_input_remove_reasons():
 
     dn_1.write(10)
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
 
@@ -197,7 +197,7 @@ def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager,
 
     dn.write("ANY VALUE")
     assert manager._is_submittable(entity)
-    assert isinstance(manager._is_submittable(entity), Reason)
+    assert isinstance(manager._is_submittable(entity), Reasons)
     assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
 

+ 4 - 4
tests/core/common/test_reason.py

@@ -9,11 +9,11 @@
 # 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 taipy.core.common.reason import Reason
+from taipy.core.reason.reason import Reasons
 
 
 def test_create_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     assert reason.entity_id == "entity_id"
     assert reason._reasons == {}
     assert reason
@@ -22,7 +22,7 @@ def test_create_reason():
 
 
 def test_add_and_remove_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     reason._add_reason("entity_id_1", "Some reason")
     assert reason._reasons == {"entity_id_1": {"Some reason"}}
     assert not reason
@@ -55,7 +55,7 @@ def test_add_and_remove_reason():
 
 
 def test_get_reason_string_from_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     reason._add_reason("entity_id_1", "Some reason")
     assert reason.reasons == "Some reason."
 

+ 17 - 14
tests/core/sequence/test_sequence_manager.py

@@ -73,14 +73,15 @@ def test_raise_sequence_does_not_belong_to_scenario():
 def __init():
     input_dn = InMemoryDataNode("foo", Scope.SCENARIO)
     output_dn = InMemoryDataNode("foo", Scope.SCENARIO)
-    task = Task("task", {}, print, [input_dn], [output_dn], TaskId("task_id"))
+    task = Task("task", {}, print, [input_dn], [output_dn], TaskId("Task_task_id"))
+    _TaskManager._set(task)
     scenario = Scenario("scenario", {task}, {}, set())
     _ScenarioManager._set(scenario)
     return scenario, task
 
 
 def test_set_and_get_sequence_no_existing_sequence():
-    scenario, task = __init()
+    scenario, _ = __init()
     sequence_name_1 = "p1"
     sequence_id_1 = SequenceId(f"SEQUENCE_{sequence_name_1}_{scenario.id}")
     sequence_name_2 = "p2"
@@ -135,6 +136,19 @@ def test_set_and_get():
     assert _TaskManager._get(task.id).id == task.id
 
 
+def test_task_parent_id_set_only_when_create():
+    scenario, task = __init()
+    sequence_name_1 = "p1"
+
+    with mock.patch("taipy.core.task._task_manager._TaskManager._set") as mck:
+        scenario.add_sequences({sequence_name_1: [task]})
+        mck.assert_called_once()
+
+    with mock.patch("taipy.core.task._task_manager._TaskManager._set") as mck:
+        scenario.sequences[sequence_name_1]
+        mck.assert_not_called()
+
+
 def test_get_all_on_multiple_versions_environment():
     # Create 5 sequences from Scenario with 2 versions each
     for version in range(1, 3):
@@ -474,18 +488,7 @@ def test_sequence_notification_subscribe(mocker):
     mocker.patch.object(
         _utils,
         "_load_fct",
-        side_effect=[
-            notify_1,
-            notify_1,
-            notify_1,
-            notify_1,
-            notify_2,
-            notify_2,
-            notify_2,
-            notify_2,
-            notify_2,
-            notify_2,
-        ],
+        side_effect=[notify_1, notify_1, notify_2, notify_2, notify_2, notify_2],
     )
 
     # test subscription

+ 4 - 3
tests/gui_core/test_context_is_submitable.py

@@ -13,8 +13,8 @@ from unittest.mock import Mock, patch
 
 from taipy.config.common.scope import Scope
 from taipy.core import Job, JobId, Scenario, Task
-from taipy.core.common.reason import Reason
 from taipy.core.data.pickle import PickleDataNode
+from taipy.core.reason.reason import Reasons
 from taipy.gui_core._context import _GuiCoreContext
 
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
@@ -23,14 +23,15 @@ a_job = Job(JobId("JOB_job_id"), a_task, "submit_id", a_scenario.id)
 a_job.isfinished = lambda s: True  # type: ignore[attr-defined]
 a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 
+
 def mock_is_submittable_reason(entity_id):
-    reason = Reason(entity_id)
+    reason = Reasons(entity_id)
     reason._add_reason(entity_id, "a reason")
     return reason
 
 
 def mock_has_no_reason(entity_id):
-    return Reason(entity_id)
+    return Reasons(entity_id)
 
 
 def mock_core_get(entity_id):