Переглянути джерело

Merge branch 'develop' into 1297-value-format

Nam Nguyen 11 місяців тому
батько
коміт
9fe190a422

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

@@ -11,8 +11,8 @@
 
 
 from typing import TYPE_CHECKING, Dict, Set, Union
 from typing import TYPE_CHECKING, Dict, Set, Union
 
 
-from ..common.reason import Reason
 from ..notification import EventOperation, Notifier, _make_event
 from ..notification import EventOperation, Notifier, _make_event
+from ..reason.reason import Reasons
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from ..data.data_node import DataNode, DataNodeId
     from ..data.data_node import DataNode, DataNodeId
@@ -29,7 +29,7 @@ class _ReadyToRunProperty:
 
 
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # 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)
     # 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
     @classmethod
     def _add(cls, dn: "DataNode", reason: str) -> None:
     def _add(cls, dn: "DataNode", reason: str) -> None:
@@ -81,7 +81,7 @@ class _ReadyToRunProperty:
             cls.__publish_submittable_property_event(submittable, False)
             cls.__publish_submittable_property_event(submittable, False)
 
 
         if submittable.id not in cls._submittable_id_datanodes:
         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)
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
 
 
     @staticmethod
     @staticmethod

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

@@ -17,9 +17,10 @@ import networkx as nx
 
 
 from ..common._listattributes import _ListAttributes
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
-from ..common.reason import Reason
 from ..data.data_node import DataNode
 from ..data.data_node import DataNode
 from ..job.job import Job
 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 ..submission.submission import Submission
 from ..task.task import Task
 from ..task.task import Task
 from ._dag import _DAG
 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)}
         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)
         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.
         """Indicate if the entity is ready to be run.
 
 
         Returns:
         Returns:
             A Reason object that can function as a Boolean value.
             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.
             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():
         for node in self.get_inputs():
             if node._edit_in_progress:
             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:
             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
         return reason
 
 

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

@@ -27,10 +27,6 @@ class _Manager(Generic[EntityType]):
     _logger = _TaipyLogger._get_logger()
     _logger = _TaipyLogger._get_logger()
     _ENTITY_NAME: str = "Entity"
     _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
     @classmethod
     def _delete_all(cls):
     def _delete_all(cls):
         """
         """

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

@@ -164,7 +164,7 @@ class _Orchestrator(_AbstractOrchestrator):
         )
         )
 
 
     @classmethod
     @classmethod
-    def _update_submission_status(cls, job: Job):
+    def _update_submission_status(cls, job: Job) -> None:
         submission_manager = _SubmissionManagerFactory._build_manager()
         submission_manager = _SubmissionManagerFactory._build_manager()
         if submission := submission_manager._get(job.submit_id):
         if submission := submission_manager._get(job.submit_id):
             submission_manager._update_submission_status(submission, job)
             submission_manager._update_submission_status(submission, job)
@@ -182,7 +182,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls.__logger.error(f"Job {job.id} status: {job.status}")
             cls.__logger.error(f"Job {job.id} status: {job.status}")
 
 
     @classmethod
     @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 = []
         blocked_jobs = []
         pending_jobs = []
         pending_jobs = []
 
 
@@ -199,7 +199,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls.jobs_to_run.put(job)
             cls.jobs_to_run.put(job)
 
 
     @classmethod
     @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.
         #  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):
         def __check_if_timeout(st, to):
             return (datetime.now() - st).seconds < 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)
         return any(not data_manager._get(dn.id).is_ready_for_reading for dn in input_data_nodes)
 
 
     @staticmethod
     @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
         jobs = [jobs] if isinstance(jobs, Job) else jobs
         for job in jobs:
         for job in jobs:
             job._unlock_edit_on_outputs()
             job._unlock_edit_on_outputs()
 
 
     @classmethod
     @classmethod
-    def _on_status_change(cls, job: Job):
+    def _on_status_change(cls, job: Job) -> None:
         if job.is_completed() or job.is_skipped():
         if job.is_completed() or job.is_skipped():
             cls.__logger.debug(f"{job.id} has been completed or skipped. Unblocking jobs.")
             cls.__logger.debug(f"{job.id} has been completed or skipped. Unblocking jobs.")
             cls.__unblock_jobs()
             cls.__unblock_jobs()
@@ -245,7 +245,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls._fail_subsequent_jobs(job)
             cls._fail_subsequent_jobs(job)
 
 
     @classmethod
     @classmethod
-    def __unblock_jobs(cls):
+    def __unblock_jobs(cls) -> None:
         with cls.lock:
         with cls.lock:
             cls.__logger.debug("Acquiring lock to unblock jobs.")
             cls.__logger.debug("Acquiring lock to unblock jobs.")
             for job in cls.blocked_jobs:
             for job in cls.blocked_jobs:
@@ -258,14 +258,14 @@ class _Orchestrator(_AbstractOrchestrator):
                     cls.jobs_to_run.put(job)
                     cls.jobs_to_run.put(job)
 
 
     @classmethod
     @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.
         try:  # In case the job has been removed from the list of blocked_jobs.
             cls.blocked_jobs.remove(job)
             cls.blocked_jobs.remove(job)
         except Exception:
         except Exception:
             cls.__logger.warning(f"{job.id} is not in the blocked list anymore.")
             cls.__logger.warning(f"{job.id} is not in the blocked list anymore.")
 
 
     @classmethod
     @classmethod
-    def cancel_job(cls, job: Job):
+    def cancel_job(cls, job: Job) -> None:
         if job.is_canceled():
         if job.is_canceled():
             cls.__logger.info(f"{job.id} has already been canceled.")
             cls.__logger.info(f"{job.id} has already been canceled.")
         elif job.is_abandoned():
         elif job.is_abandoned():
@@ -298,13 +298,13 @@ class _Orchestrator(_AbstractOrchestrator):
         return subsequent_jobs
         return subsequent_jobs
 
 
     @classmethod
     @classmethod
-    def __remove_blocked_jobs(cls, jobs):
+    def __remove_blocked_jobs(cls, jobs: Set[Job]) -> None:
         for job in jobs:
         for job in jobs:
             cls.__remove_blocked_job(job)
             cls.__remove_blocked_job(job)
 
 
     @classmethod
     @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():
         while not cls.jobs_to_run.empty():
             current_job = cls.jobs_to_run.get()
             current_job = cls.jobs_to_run.get()
             if current_job not in jobs:
             if current_job not in jobs:
@@ -312,7 +312,7 @@ class _Orchestrator(_AbstractOrchestrator):
         cls.jobs_to_run = new_jobs_to_run
         cls.jobs_to_run = new_jobs_to_run
 
 
     @classmethod
     @classmethod
-    def _fail_subsequent_jobs(cls, failed_job: Job):
+    def _fail_subsequent_jobs(cls, failed_job: Job) -> None:
         with cls.lock:
         with cls.lock:
             cls.__logger.debug("Acquiring lock to fail subsequent jobs.")
             cls.__logger.debug("Acquiring lock to fail subsequent jobs.")
             to_fail_or_abandon_jobs = set()
             to_fail_or_abandon_jobs = set()
@@ -327,7 +327,7 @@ class _Orchestrator(_AbstractOrchestrator):
             cls._unlock_edit_on_jobs_outputs(to_fail_or_abandon_jobs)
             cls._unlock_edit_on_jobs_outputs(to_fail_or_abandon_jobs)
 
 
     @classmethod
     @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:
         for job in jobs:
             if job.is_running():
             if job.is_running():
                 cls.__logger.info(f"{job.id} is running and cannot be canceled.")
                 cls.__logger.info(f"{job.id} is running and cannot be canceled.")
@@ -341,7 +341,7 @@ class _Orchestrator(_AbstractOrchestrator):
                 job.abandoned()
                 job.abandoned()
 
 
     @staticmethod
     @staticmethod
-    def _check_and_execute_jobs_if_development_mode():
+    def _check_and_execute_jobs_if_development_mode() -> None:
         from ._orchestrator_factory import _OrchestratorFactory
         from ._orchestrator_factory import _OrchestratorFactory
 
 
         if dispatcher := _OrchestratorFactory._dispatcher:
         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]:
     def _search(self, attribute: str, value: Any, filters: Optional[List[Dict]] = None) -> List[Entity]:
         return list(self.__search(attribute, value, filters))
         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):
         if isinstance(folder_path, str):
             folder: pathlib.Path = pathlib.Path(folder_path)
             folder: pathlib.Path = pathlib.Path(folder_path)
         else:
         else:

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

@@ -33,7 +33,7 @@ class _CycleManager(_Manager[Cycle]):
     @classmethod
     @classmethod
     def _create(
     def _create(
         cls, frequency: Frequency, name: Optional[str] = None, creation_date: Optional[datetime] = None, **properties
         cls, frequency: Frequency, name: Optional[str] = None, creation_date: Optional[datetime] = None, **properties
-    ):
+    ) -> Cycle:
         creation_date = creation_date if creation_date else datetime.now()
         creation_date = creation_date if creation_date else datetime.now()
         start_date = _CycleManager._get_start_date_of_cycle(frequency, creation_date)
         start_date = _CycleManager._get_start_date_of_cycle(frequency, creation_date)
         end_date = _CycleManager._get_end_date_of_cycle(frequency, start_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)
             return cls._create(frequency=frequency, creation_date=creation_date, name=name)
 
 
     @staticmethod
     @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_date = creation_date.date()
         start_time = time()
         start_time = time()
         if frequency == Frequency.DAILY:
         if frequency == Frequency.DAILY:
@@ -77,7 +77,7 @@ class _CycleManager(_Manager[Cycle]):
         return datetime.combine(start_date, start_time)
         return datetime.combine(start_date, start_time)
 
 
     @staticmethod
     @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
         end_date = start_date
         if frequency == Frequency.DAILY:
         if frequency == Frequency.DAILY:
             end_date = end_date + timedelta(days=1)
             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)
         return cls._repository._load_all(filters)
 
 
     @classmethod
     @classmethod
-    def _clean_generated_file(cls, data_node: DataNode):
+    def _clean_generated_file(cls, data_node: DataNode) -> None:
         if not isinstance(data_node, _FileDataNodeMixin):
         if not isinstance(data_node, _FileDataNodeMixin):
             return
             return
         if data_node.is_generated and os.path.exists(data_node.path):
         if data_node.is_generated and os.path.exists(data_node.path):
             os.remove(data_node.path)
             os.remove(data_node.path)
 
 
     @classmethod
     @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:
         for data_node in data_nodes:
             cls._clean_generated_file(data_node)
             cls._clean_generated_file(data_node)
 
 
     @classmethod
     @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):
         if data_node := cls._get(data_node_id, None):
             cls._clean_generated_file(data_node)
             cls._clean_generated_file(data_node)
         super()._delete(data_node_id)
         super()._delete(data_node_id)
 
 
     @classmethod
     @classmethod
-    def _delete_many(cls, data_node_ids: Iterable[DataNodeId]):
+    def _delete_many(cls, data_node_ids: Iterable[DataNodeId]) -> None:
         data_nodes = []
         data_nodes = []
         for data_node_id in data_node_ids:
         for data_node_id in data_node_ids:
             if data_node := cls._get(data_node_id):
             if data_node := cls._get(data_node_id):
@@ -138,13 +138,13 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         super()._delete_many(data_node_ids)
         super()._delete_many(data_node_ids)
 
 
     @classmethod
     @classmethod
-    def _delete_all(cls):
+    def _delete_all(cls) -> None:
         data_nodes = cls._get_all()
         data_nodes = cls._get_all()
         cls._clean_generated_files(data_nodes)
         cls._clean_generated_files(data_nodes)
         super()._delete_all()
         super()._delete_all()
 
 
     @classmethod
     @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)
         data_nodes = cls._get_all(version_number)
         cls._clean_generated_files(data_nodes)
         cls._clean_generated_files(data_nodes)
         cls._repository._delete_by(attribute="version", value=version_number)
         cls._repository._delete_by(attribute="version", value=version_number)
@@ -165,7 +165,7 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         return cls._repository._load_all(filters)
         return cls._repository._load_all(filters)
 
 
     @classmethod
     @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)
         cls._repository._export(id, folder_path)
 
 
         if not kwargs.get("include_data"):
         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):
     def last_edit_date(self, val):
         self._last_edit_date = val
         self._last_edit_date = val
 
 
-    def _build_not_written_reason(self) -> str:
-        return f"DataNode {self.id} is not written"
-
     @property  # type: ignore
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     @_self_reload(_MANAGER_NAME)
     def scope(self):
     def scope(self):
@@ -297,9 +294,6 @@ class DataNode(_Entity, _Labeled):
     def edit_in_progress(self, val):
     def edit_in_progress(self, val):
         self._edit_in_progress = 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
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     @_self_reload(_MANAGER_NAME)
     def editor_id(self):
     def editor_id(self):

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

@@ -58,7 +58,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
         return job
         return job
 
 
     @classmethod
     @classmethod
-    def _delete(cls, job: Union[Job, JobId], force=False):
+    def _delete(cls, job: Union[Job, JobId], force=False) -> None:
         if isinstance(job, str):
         if isinstance(job, str):
             job = cls._get(job)
             job = cls._get(job)
         if cls._is_deletable(job) or force:
         if cls._is_deletable(job) or force:
@@ -69,7 +69,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
             raise err
             raise err
 
 
     @classmethod
     @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
         job = cls._get(job) if isinstance(job, str) else job
 
 
         from .._orchestrator._orchestrator_factory import _OrchestratorFactory
         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
 from typing import Dict, Set
 
 
 
 
-class Reason:
+class Reasons:
     def __init__(self, entity_id: str) -> None:
     def __init__(self, entity_id: str) -> None:
         self.entity_id: str = entity_id
         self.entity_id: str = entity_id
         self._reasons: Dict[str, Set[str]] = {}
         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:
         if entity_id not in self._reasons:
             self._reasons[entity_id] = set()
             self._reasons[entity_id] = set()
         self._reasons[entity_id].add(reason)
         self._reasons[entity_id].add(reason)
         return self
         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]:
         if entity_id in self._reasons and reason in self._reasons[entity_id]:
             self._reasons[entity_id].remove(reason)
             self._reasons[entity_id].remove(reason)
             if len(self._reasons[entity_id]) == 0:
             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 .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 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 ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.scenario_config import ScenarioConfig
 from ..config.scenario_config import ScenarioConfig
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
@@ -38,7 +37,6 @@ from ..exceptions.exceptions import (
     ImportScenarioDoesntHaveAVersion,
     ImportScenarioDoesntHaveAVersion,
     InsufficientScenarioToCompare,
     InsufficientScenarioToCompare,
     InvalidScenario,
     InvalidScenario,
-    InvalidSequence,
     NonExistingComparator,
     NonExistingComparator,
     NonExistingScenario,
     NonExistingScenario,
     NonExistingScenarioConfig,
     NonExistingScenarioConfig,
@@ -48,6 +46,8 @@ from ..exceptions.exceptions import (
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 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_manager_factory import _SubmissionManagerFactory
 from ..submission.submission import Submission
 from ..submission.submission import Submission
 from ..task._task_manager_factory import _TaskManagerFactory
 from ..task._task_manager_factory import _TaskManagerFactory
@@ -76,7 +76,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         callback: Callable[[Scenario, Job], None],
         callback: Callable[[Scenario, Job], None],
         params: Optional[List[Any]] = None,
         params: Optional[List[Any]] = None,
         scenario: Optional[Scenario] = None,
         scenario: Optional[Scenario] = None,
-    ):
+    ) -> None:
         if scenario is None:
         if scenario is None:
             scenarios = cls._get_all()
             scenarios = cls._get_all()
             for scn in scenarios:
             for scn in scenarios:
@@ -91,7 +91,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         callback: Callable[[Scenario, Job], None],
         callback: Callable[[Scenario, Job], None],
         params: Optional[List[Any]] = None,
         params: Optional[List[Any]] = None,
         scenario: Optional[Scenario] = None,
         scenario: Optional[Scenario] = None,
-    ):
+    ) -> None:
         if scenario is None:
         if scenario is None:
             scenarios = cls._get_all()
             scenarios = cls._get_all()
             for scn in scenarios:
             for scn in scenarios:
@@ -101,14 +101,14 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         cls.__remove_subscriber(callback, params, scenario)
         cls.__remove_subscriber(callback, params, scenario)
 
 
     @classmethod
     @classmethod
-    def __add_subscriber(cls, callback, params, scenario: Scenario):
+    def __add_subscriber(cls, callback, params, scenario: Scenario) -> None:
         scenario._add_subscriber(callback, params)
         scenario._add_subscriber(callback, params)
         Notifier.publish(
         Notifier.publish(
             _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params)
             _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params)
         )
         )
 
 
     @classmethod
     @classmethod
-    def __remove_subscriber(cls, callback, params, scenario: Scenario):
+    def __remove_subscriber(cls, callback, params, scenario: Scenario) -> None:
         scenario._remove_subscriber(callback, params)
         scenario._remove_subscriber(callback, params)
         Notifier.publish(
         Notifier.publish(
             _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params)
             _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():
         if not scenario._is_consistent():
             raise InvalidScenario(scenario.id)
             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))
         Notifier.publish(_make_event(scenario, EventOperation.CREATION))
         return scenario
         return scenario
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reason:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reasons:
         if isinstance(scenario, str):
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
             scenario = cls._get(scenario)
 
 
         if not isinstance(scenario, Scenario):
         if not isinstance(scenario, Scenario):
             scenario = str(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 reason
 
 
         return scenario.is_ready_to_run()
         return scenario.is_ready_to_run()
@@ -313,7 +311,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return False
         return False
 
 
     @classmethod
     @classmethod
-    def _set_primary(cls, scenario: Scenario):
+    def _set_primary(cls, scenario: Scenario) -> None:
         if not scenario.cycle:
         if not scenario.cycle:
             raise DoesNotBelongToACycle(
             raise DoesNotBelongToACycle(
                 f"Can't set scenario {scenario.id} to primary because it doesn't belong to a cycle."
                 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
         scenario.is_primary = True  # type: ignore
 
 
     @classmethod
     @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())
         tags = scenario.properties.get(cls._AUTHORIZED_TAGS_KEY, set())
         if len(tags) > 0 and tag not in tags:
         if len(tags) > 0 and tag not in tags:
             raise UnauthorizedTagError(f"Tag `{tag}` not authorized by scenario configuration `{scenario.config_id}`")
             raise UnauthorizedTagError(f"Tag `{tag}` not authorized by scenario configuration `{scenario.config_id}`")
@@ -341,7 +339,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
         )
 
 
     @classmethod
     @classmethod
-    def _untag(cls, scenario: Scenario, tag: str):
+    def _untag(cls, scenario: Scenario, tag: str) -> None:
         scenario._remove_tag(tag)
         scenario._remove_tag(tag)
         cls._set(scenario)
         cls._set(scenario)
         Notifier.publish(
         Notifier.publish(
@@ -349,14 +347,14 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
         )
 
 
     @classmethod
     @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:
         if len(scenarios) < 2:
             raise InsufficientScenarioToCompare("At least two scenarios are required to compare.")
             raise InsufficientScenarioToCompare("At least two scenarios are required to compare.")
 
 
         if not all(scenarios[0].config_id == scenario.config_id for scenario in scenarios):
         if not all(scenarios[0].config_id == scenario.config_id for scenario in scenarios):
             raise DifferentScenarioConfigs("Scenarios to compare must have the same configuration.")
             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 = {}
             results = {}
             if data_node_config_id:
             if data_node_config_id:
                 if data_node_config_id in scenario_config.comparators.keys():
                 if data_node_config_id in scenario_config.comparators.keys():
@@ -391,7 +389,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return True
         return True
 
 
     @classmethod
     @classmethod
-    def _delete(cls, scenario_id: ScenarioId):
+    def _delete(cls, scenario_id: ScenarioId) -> None:
         scenario = cls._get(scenario_id)
         scenario = cls._get(scenario_id)
         if not cls._is_deletable(scenario):
         if not cls._is_deletable(scenario):
             raise DeletingPrimaryScenario(
             raise DeletingPrimaryScenario(
@@ -403,7 +401,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         super()._delete(scenario_id)
         super()._delete(scenario_id)
 
 
     @classmethod
     @classmethod
-    def _hard_delete(cls, scenario_id: ScenarioId):
+    def _hard_delete(cls, scenario_id: ScenarioId) -> None:
         scenario = cls._get(scenario_id)
         scenario = cls._get(scenario_id)
         if not cls._is_deletable(scenario):
         if not cls._is_deletable(scenario):
             raise DeletingPrimaryScenario(
             raise DeletingPrimaryScenario(
@@ -418,7 +416,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
             cls._delete_entities_of_multiple_types(entity_ids_to_delete)
             cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
 
     @classmethod
     @classmethod
-    def _delete_by_version(cls, version_number: str):
+    def _delete_by_version(cls, version_number: str) -> None:
         """
         """
         Deletes scenario by the version number.
         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}
         _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}
         _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)
         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
         from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 
 
         seq_manager = _SequenceManagerFactory._build_manager()
         seq_manager = _SequenceManagerFactory._build_manager()
         seq = seq_manager._create(name, tasks, subscribers or [], properties or {}, self.id, self.version)
         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(
@@ -391,7 +390,7 @@ class Scenario(_Entity, Submittable, _Labeled):
         sequence_manager = _SequenceManagerFactory._build_manager()
         sequence_manager = _SequenceManagerFactory._build_manager()
 
 
         for sequence_name, sequence_data in self._sequences.items():
         for sequence_name, sequence_data in self._sequences.items():
-            p = sequence_manager._create(
+            sequence = sequence_manager._build_sequence(
                 sequence_name,
                 sequence_name,
                 sequence_data.get(self._SEQUENCE_TASKS_KEY, []),
                 sequence_data.get(self._SEQUENCE_TASKS_KEY, []),
                 sequence_data.get(self._SEQUENCE_SUBSCRIBERS_KEY, []),
                 sequence_data.get(self._SEQUENCE_SUBSCRIBERS_KEY, []),
@@ -399,9 +398,9 @@ class Scenario(_Entity, Submittable, _Labeled):
                 self.id,
                 self.id,
                 self.version,
                 self.version,
             )
             )
-            if not isinstance(p, Sequence):
+            if not isinstance(sequence, Sequence):
                 raise NonExistingSequence(sequence_name, self.id)
                 raise NonExistingSequence(sequence_name, self.id)
-            _sequences[sequence_name] = p
+            _sequences[sequence_name] = sequence
         return _sequences
         return _sequences
 
 
     @property  # type: ignore
     @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 .._manager._manager import _Manager
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
-from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..exceptions.exceptions import (
 from ..exceptions.exceptions import (
+    InvalidSequence,
     InvalidSequenceId,
     InvalidSequenceId,
     ModelNotFound,
     ModelNotFound,
     NonExistingSequence,
     NonExistingSequence,
@@ -31,6 +31,8 @@ from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification.event import _make_event
 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_manager_factory import _ScenarioManagerFactory
 from ..scenario.scenario import Scenario
 from ..scenario.scenario import Scenario
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
@@ -48,7 +50,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
     _model_name = "sequences"
     _model_name = "sequences"
 
 
     @classmethod
     @classmethod
-    def _delete(cls, sequence_id: SequenceId):
+    def _delete(cls, sequence_id: SequenceId) -> None:
         """
         """
         Deletes a Sequence by id.
         Deletes a Sequence by id.
         """
         """
@@ -63,7 +65,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         raise ModelNotFound(cls._model_name, sequence_id)
         raise ModelNotFound(cls._model_name, sequence_id)
 
 
     @classmethod
     @classmethod
-    def _delete_all(cls):
+    def _delete_all(cls) -> None:
         """
         """
         Deletes all Sequences.
         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}))
             Notifier.publish(Event(cls._EVENT_ENTITY_TYPE, EventOperation.DELETION, metadata={"delete_all": True}))
 
 
     @classmethod
     @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.
         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
             raise ModelNotFound(cls._model_name, sequence_id) from None
 
 
     @classmethod
     @classmethod
-    def _delete_by_version(cls, version_number: str):
+    def _delete_by_version(cls, version_number: str) -> None:
         """
         """
         Deletes Sequences by version number.
         Deletes Sequences by version number.
         """
         """
@@ -111,14 +113,14 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             cls._delete_many(scenario.sequences.values())
             cls._delete_many(scenario.sequences.values())
 
 
     @classmethod
     @classmethod
-    def _hard_delete(cls, sequence_id: SequenceId):
+    def _hard_delete(cls, sequence_id: SequenceId) -> None:
         sequence = cls._get(sequence_id)
         sequence = cls._get(sequence_id)
         entity_ids_to_delete = cls._get_children_entity_ids(sequence)
         entity_ids_to_delete = cls._get_children_entity_ids(sequence)
         entity_ids_to_delete.sequence_ids.add(sequence.id)
         entity_ids_to_delete.sequence_ids.add(sequence.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
 
     @classmethod
     @classmethod
-    def _set(cls, sequence: Sequence):
+    def _set(cls, sequence: Sequence) -> None:
         """
         """
         Save or update a Sequence.
         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}.")
             cls._logger.error(f"Sequence {sequence.id} belongs to a non-existing Scenario {scenario_id}.")
             raise SequenceBelongsToNonExistingScenario(sequence.id, 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()
         task_manager = _TaskManagerFactory._build_manager()
         _tasks: List[Task] = []
         _tasks: List[Task] = []
         for task in tasks:
         for task in tasks:
@@ -158,11 +150,24 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
                 _tasks.append(_task)
                 _tasks.append(_task)
             else:
             else:
                 raise NonExistingTask(task)
                 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 = properties if properties else {}
         properties["name"] = sequence_name
         properties["name"] = sequence_name
         version = version if version else cls._get_latest_version()
         version = version if version else cls._get_latest_version()
-        sequence = Sequence(
+        return Sequence(
             properties=properties,
             properties=properties,
             tasks=_tasks,
             tasks=_tasks,
             sequence_id=sequence_id,
             sequence_id=sequence_id,
@@ -171,10 +176,52 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             subscribers=subscribers,
             subscribers=subscribers,
             version=version,
             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:
         for task in _tasks:
             if sequence_id not in task._parent_ids:
             if sequence_id not in task._parent_ids:
                 task._parent_ids.update([sequence_id])
                 task._parent_ids.update([sequence_id])
                 task_manager._set(task)
                 task_manager._set(task)
+
+        if not sequence._is_consistent():
+            raise InvalidSequence(sequence_id)
+
         return sequence
         return sequence
 
 
     @classmethod
     @classmethod
@@ -264,7 +311,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         callback: Callable[[Sequence, Job], None],
         callback: Callable[[Sequence, Job], None],
         params: Optional[List[Any]] = None,
         params: Optional[List[Any]] = None,
         sequence: Optional[Sequence] = None,
         sequence: Optional[Sequence] = None,
-    ):
+    ) -> None:
         if sequence is None:
         if sequence is None:
             sequences = cls._get_all()
             sequences = cls._get_all()
             for pln in sequences:
             for pln in sequences:
@@ -278,7 +325,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         callback: Callable[[Sequence, Job], None],
         callback: Callable[[Sequence, Job], None],
         params: Optional[List[Any]] = None,
         params: Optional[List[Any]] = None,
         sequence: Optional[Sequence] = None,
         sequence: Optional[Sequence] = None,
-    ):
+    ) -> None:
         if sequence is None:
         if sequence is None:
             sequences = cls._get_all()
             sequences = cls._get_all()
             for pln in sequences:
             for pln in sequences:
@@ -297,14 +344,14 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reason:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reasons:
         if isinstance(sequence, str):
         if isinstance(sequence, str):
             sequence = cls._get(sequence)
             sequence = cls._get(sequence)
 
 
         if not isinstance(sequence, Sequence):
         if not isinstance(sequence, Sequence):
             sequence = str(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 reason
 
 
         return sequence.is_ready_to_run()
         return sequence.is_ready_to_run()
@@ -352,7 +399,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         return True if cls._get(entity_id) else False
         return True if cls._get(entity_id) else False
 
 
     @classmethod
     @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.
         Export a Sequence entity.
         """
         """

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

@@ -54,7 +54,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
         return submission
         return submission
 
 
     @classmethod
     @classmethod
-    def _update_submission_status(cls, submission: Submission, job: Job):
+    def _update_submission_status(cls, submission: Submission, job: Job) -> None:
         with cls.__lock:
         with cls.__lock:
             submission = cls._get(submission)
             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 is set later to make sure notification for updating
             # the submission_status attribute is triggered
             # the submission_status attribute is triggered
             if submission._is_canceled:
             if submission._is_canceled:
-                cls._set_submission_status(submission, SubmissionStatus.CANCELED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.CANCELED, job)
             elif submission._is_abandoned:
             elif submission._is_abandoned:
-                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
             elif submission._running_jobs:
             elif submission._running_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.RUNNING, job)
+                cls.__set_submission_status(submission, SubmissionStatus.RUNNING, job)
             elif submission._pending_jobs:
             elif submission._pending_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.PENDING, job)
+                cls.__set_submission_status(submission, SubmissionStatus.PENDING, job)
             elif submission._blocked_jobs:
             elif submission._blocked_jobs:
-                cls._set_submission_status(submission, SubmissionStatus.BLOCKED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.BLOCKED, job)
             elif submission._is_completed:
             elif submission._is_completed:
-                cls._set_submission_status(submission, SubmissionStatus.COMPLETED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.COMPLETED, job)
             else:
             else:
-                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+                cls.__set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
             cls.__logger.debug(
             cls.__logger.debug(
                 f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`"
                 f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`"
             )
             )
 
 
     @classmethod
     @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:
         if not submission._is_in_context:
             submission = cls._get(submission)
             submission = cls._get(submission)
         _current_submission_status = submission._submission_status
         _current_submission_status = submission._submission_status
@@ -147,7 +147,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             return max(submissions_of_task)
             return max(submissions_of_task)
 
 
     @classmethod
     @classmethod
-    def _delete(cls, submission: Union[Submission, SubmissionId]):
+    def _delete(cls, submission: Union[Submission, SubmissionId]) -> None:
         if isinstance(submission, str):
         if isinstance(submission, str):
             submission = cls._get(submission)
             submission = cls._get(submission)
         if cls._is_deletable(submission):
         if cls._is_deletable(submission):
@@ -158,14 +158,14 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             raise err
             raise err
 
 
     @classmethod
     @classmethod
-    def _hard_delete(cls, submission_id: SubmissionId):
+    def _hard_delete(cls, submission_id: SubmissionId) -> None:
         submission = cls._get(submission_id)
         submission = cls._get(submission_id)
         entity_ids_to_delete = cls._get_children_entity_ids(submission)
         entity_ids_to_delete = cls._get_children_entity_ids(submission)
         entity_ids_to_delete.submission_ids.add(submission.id)
         entity_ids_to_delete.submission_ids.add(submission.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
 
     @classmethod
     @classmethod
-    def _get_children_entity_ids(cls, submission: Submission):
+    def _get_children_entity_ids(cls, submission: Submission) -> _EntityIds:
         entity_ids = _EntityIds()
         entity_ids = _EntityIds()
 
 
         for job in submission.jobs:
         for job in submission.jobs:
@@ -174,7 +174,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
         return entity_ids
         return entity_ids
 
 
     @classmethod
     @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):
         if isinstance(submission, str):
             submission = cls._get(submission)
             submission = cls._get(submission)
         return submission.is_finished() or submission.submission_status == SubmissionStatus.UNDEFINED
         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,
     _is_task,
 )
 )
 from .common._warnings import _warn_deprecated, _warn_no_core_service
 from .common._warnings import _warn_deprecated, _warn_no_core_service
-from .common.reason import Reason
 from .config.data_node_config import DataNodeConfig
 from .config.data_node_config import DataNodeConfig
 from .config.scenario_config import ScenarioConfig
 from .config.scenario_config import ScenarioConfig
 from .cycle._cycle_manager_factory import _CycleManagerFactory
 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_manager_factory import _JobManagerFactory
 from .job.job import Job
 from .job.job import Job
 from .job.job_id import JobId
 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_manager_factory import _ScenarioManagerFactory
 from .scenario.scenario import Scenario
 from .scenario.scenario import Scenario
 from .scenario.scenario_id import ScenarioId
 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)
         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.
     """Indicate if an entity can be submitted.
 
 
     This function checks if the given entity can be submitted for execution.
     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)
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
         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(
 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 .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 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 ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.task_config import TaskConfig
 from ..config.task_config import TaskConfig
 from ..cycle.cycle_id import CycleId
 from ..cycle.cycle_id import CycleId
 from ..data._data_manager_factory import _DataManagerFactory
 from ..data._data_manager_factory import _DataManagerFactory
 from ..exceptions.exceptions import NonExistingTask
 from ..exceptions.exceptions import NonExistingTask
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 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 ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..sequence.sequence_id import SequenceId
 from ..submission.submission import Submission
 from ..submission.submission import Submission
@@ -46,7 +51,7 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return _OrchestratorFactory._build_orchestrator()
         return _OrchestratorFactory._build_orchestrator()
 
 
     @classmethod
     @classmethod
-    def _set(cls, task: Task):
+    def _set(cls, task: Task) -> None:
         cls.__save_data_nodes(task.input.values())
         cls.__save_data_nodes(task.input.values())
         cls.__save_data_nodes(task.output.values())
         cls.__save_data_nodes(task.output.values())
         super()._set(task)
         super()._set(task)
@@ -130,20 +135,20 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return cls._repository._load_all(filters)
         return cls._repository._load_all(filters)
 
 
     @classmethod
     @classmethod
-    def __save_data_nodes(cls, data_nodes):
+    def __save_data_nodes(cls, data_nodes) -> None:
         data_manager = _DataManagerFactory._build_manager()
         data_manager = _DataManagerFactory._build_manager()
         for i in data_nodes:
         for i in data_nodes:
             data_manager._set(i)
             data_manager._set(i)
 
 
     @classmethod
     @classmethod
-    def _hard_delete(cls, task_id: TaskId):
+    def _hard_delete(cls, task_id: TaskId) -> None:
         task = cls._get(task_id)
         task = cls._get(task_id)
         entity_ids_to_delete = cls._get_children_entity_ids(task)
         entity_ids_to_delete = cls._get_children_entity_ids(task)
         entity_ids_to_delete.task_ids.add(task.id)
         entity_ids_to_delete.task_ids.add(task.id)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
         cls._delete_entities_of_multiple_types(entity_ids_to_delete)
 
 
     @classmethod
     @classmethod
-    def _get_children_entity_ids(cls, task: Task):
+    def _get_children_entity_ids(cls, task: Task) -> _EntityIds:
         entity_ids = _EntityIds()
         entity_ids = _EntityIds()
 
 
         from ..job._job_manager_factory import _JobManagerFactory
         from ..job._job_manager_factory import _JobManagerFactory
@@ -164,22 +169,22 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
         return entity_ids
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reason:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reasons:
         if isinstance(task, str):
         if isinstance(task, str):
             task = cls._get(task)
             task = cls._get(task)
         if not isinstance(task, Task):
         if not isinstance(task, Task):
             task = str(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:
         else:
-            reason = Reason(task.id)
+            reason = Reasons(task.id)
             data_manager = _DataManagerFactory._build_manager()
             data_manager = _DataManagerFactory._build_manager()
             for node in task.input.values():
             for node in task.input.values():
                 node = data_manager._get(node)
                 node = data_manager._get(node)
                 if node._edit_in_progress:
                 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:
                 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
         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.common.frequency import Frequency
 from taipy.config.config import Config
 from taipy.config.config import Config
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
 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.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
 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)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     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 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)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     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 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
     dn_3 = scenario.dn_3
 
 
     assert not dn_3.is_ready_for_reading
     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
     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 dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     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
     # 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
     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 dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     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 scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     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_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     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 scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     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 dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     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
     # Since it is a lazy property, the scenario is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
@@ -171,7 +171,7 @@ def test_writing_input_remove_reasons():
 
 
     dn_1.write(10)
     dn_1.write(10)
     assert scenario_manager._is_submittable(scenario)
     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 scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
     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")
     dn.write("ANY VALUE")
     assert manager._is_submittable(entity)
     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 entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
     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
 # 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 taipy.core.common.reason import Reason
+from taipy.core.reason.reason import Reasons
 
 
 
 
 def test_create_reason():
 def test_create_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     assert reason.entity_id == "entity_id"
     assert reason.entity_id == "entity_id"
     assert reason._reasons == {}
     assert reason._reasons == {}
     assert reason
     assert reason
@@ -22,7 +22,7 @@ def test_create_reason():
 
 
 
 
 def test_add_and_remove_reason():
 def test_add_and_remove_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     reason._add_reason("entity_id_1", "Some reason")
     reason._add_reason("entity_id_1", "Some reason")
     assert reason._reasons == {"entity_id_1": {"Some reason"}}
     assert reason._reasons == {"entity_id_1": {"Some reason"}}
     assert not reason
     assert not reason
@@ -55,7 +55,7 @@ def test_add_and_remove_reason():
 
 
 
 
 def test_get_reason_string_from_reason():
 def test_get_reason_string_from_reason():
-    reason = Reason("entity_id")
+    reason = Reasons("entity_id")
     reason._add_reason("entity_id_1", "Some reason")
     reason._add_reason("entity_id_1", "Some reason")
     assert reason.reasons == "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():
 def __init():
     input_dn = InMemoryDataNode("foo", Scope.SCENARIO)
     input_dn = InMemoryDataNode("foo", Scope.SCENARIO)
     output_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())
     scenario = Scenario("scenario", {task}, {}, set())
     _ScenarioManager._set(scenario)
     _ScenarioManager._set(scenario)
     return scenario, task
     return scenario, task
 
 
 
 
 def test_set_and_get_sequence_no_existing_sequence():
 def test_set_and_get_sequence_no_existing_sequence():
-    scenario, task = __init()
+    scenario, _ = __init()
     sequence_name_1 = "p1"
     sequence_name_1 = "p1"
     sequence_id_1 = SequenceId(f"SEQUENCE_{sequence_name_1}_{scenario.id}")
     sequence_id_1 = SequenceId(f"SEQUENCE_{sequence_name_1}_{scenario.id}")
     sequence_name_2 = "p2"
     sequence_name_2 = "p2"
@@ -135,6 +136,19 @@ def test_set_and_get():
     assert _TaskManager._get(task.id).id == task.id
     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():
 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
     for version in range(1, 3):
     for version in range(1, 3):
@@ -474,18 +488,7 @@ def test_sequence_notification_subscribe(mocker):
     mocker.patch.object(
     mocker.patch.object(
         _utils,
         _utils,
         "_load_fct",
         "_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
     # 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.config.common.scope import Scope
 from taipy.core import Job, JobId, Scenario, Task
 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.data.pickle import PickleDataNode
+from taipy.core.reason.reason import Reasons
 from taipy.gui_core._context import _GuiCoreContext
 from taipy.gui_core._context import _GuiCoreContext
 
 
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 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_job.isfinished = lambda s: True  # type: ignore[attr-defined]
 a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 
 
+
 def mock_is_submittable_reason(entity_id):
 def mock_is_submittable_reason(entity_id):
-    reason = Reason(entity_id)
+    reason = Reasons(entity_id)
     reason._add_reason(entity_id, "a reason")
     reason._add_reason(entity_id, "a reason")
     return reason
     return reason
 
 
 
 
 def mock_has_no_reason(entity_id):
 def mock_has_no_reason(entity_id):
-    return Reason(entity_id)
+    return Reasons(entity_id)
 
 
 
 
 def mock_core_get(entity_id):
 def mock_core_get(entity_id):