Browse Source

exposed submission api to taipy core apis

Toan Quach 1 year ago
parent
commit
a40be6d271

+ 2 - 2
taipy/core/_manager/_manager.py

@@ -161,9 +161,9 @@ class _Manager(Generic[EntityType]):
         return cls._repository._export(id, folder_path)
 
     @classmethod
-    def _is_editable(cls, entity: Union[EntityType, _EntityIds]) -> bool:
+    def _is_editable(cls, entity: Union[EntityType, str]) -> bool:
         return True
 
     @classmethod
-    def _is_readable(cls, entity: Union[EntityType, _EntityIds]) -> bool:
+    def _is_readable(cls, entity: Union[EntityType, str]) -> bool:
         return True

+ 10 - 0
taipy/core/exceptions/exceptions.py

@@ -151,6 +151,16 @@ class NonExistingJob(RuntimeError):
         self.message = f"Job: {job_id} does not exist."
 
 
+class SubmissionNotDeletedException(RuntimeError):
+    """Raised if there is an attempt to delete a submission that cannot be deleted.
+
+    This exception can be raised by `taipy.delete_job()^`.
+    """
+
+    def __init__(self, submission_id: str):
+        self.message = f"Submission: {submission_id} cannot be deleted."
+
+
 class DataNodeWritingError(RuntimeError):
     """Raised if an error happens during the writing in a data node."""
 

+ 5 - 1
taipy/core/job/_job_manager.py

@@ -57,7 +57,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
 
     @classmethod
     def _delete(cls, job: Job, force=False):
-        if job.is_finished() or force:
+        if cls._is_deletable(job) or force:
             super()._delete(job.id)
             from .._orchestrator._dispatcher._job_dispatcher import _JobDispatcher
 
@@ -92,3 +92,7 @@ class _JobManager(_Manager[Job], _VersionMixin):
         if job.is_finished():
             return True
         return False
+
+    @classmethod
+    def _is_editable(cls, entity: Union[Job, str]) -> bool:
+        return False

+ 25 - 1
taipy/core/submission/_submission_manager.py

@@ -14,10 +14,11 @@ from typing import List, Optional, Union
 from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_mixin import _VersionMixin
+from ..exceptions.exceptions import SubmissionNotDeletedException
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..scenario.scenario import Scenario
 from ..sequence.sequence import Sequence
-from ..submission.submission import Submission
+from ..submission.submission import Submission, SubmissionId
 from ..task.task import Task
 
 
@@ -53,3 +54,26 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             return submissions_of_task[0]
         else:
             return max(submissions_of_task)
+
+    @classmethod
+    def _is_editable(cls, entity: Union[Submission, str]) -> bool:
+        return False
+
+    @classmethod
+    def _delete(cls, submission: Union[Submission, SubmissionId]):
+        if isinstance(submission, str):
+            submission = cls._get(submission)
+        if cls._is_deletable(submission):
+            super()._delete(submission.id)
+        else:
+            err = SubmissionNotDeletedException(submission.id)
+            cls._logger.warning(err)
+            raise err
+
+    @classmethod
+    def _is_deletable(cls, submission: Union[Submission, SubmissionId]) -> bool:
+        if isinstance(submission, str):
+            submission = cls._get(submission)
+        if submission.is_finished():
+            return True
+        return False

+ 24 - 0
taipy/core/submission/submission.py

@@ -201,6 +201,30 @@ class Submission(_Entity, _Labeled):
         else:
             self.submission_status = SubmissionStatus.UNDEFINED  # type: ignore
 
+    def is_finished(self) -> bool:
+        """Indicate if the submission is finished.
+
+        Returns:
+            True if the submission is finished.
+        """
+        submission_status = self.submission_status
+        return (
+            submission_status == SubmissionStatus.COMPLETED
+            or submission_status == SubmissionStatus.FAILED
+            or submission_status == SubmissionStatus.CANCELED
+            or submission_status == SubmissionStatus.UNDEFINED
+        )
+
+    def is_deletable(self) -> bool:
+        """Indicate if the submission can be deleted.
+
+        Returns:
+            True if the submission can be deleted. False otherwise.
+        """
+        from ... import core as tp
+
+        return tp.is_deletable(self)
+
 
 @_make_event.register(Submission)
 def _make_event_for_submission(

+ 66 - 14
taipy/core/taipy.py

@@ -44,7 +44,7 @@ from .sequence._sequence_manager_factory import _SequenceManagerFactory
 from .sequence.sequence import Sequence
 from .sequence.sequence_id import SequenceId
 from .submission._submission_manager_factory import _SubmissionManagerFactory
-from .submission.submission import Submission
+from .submission.submission import Submission, SubmissionId
 from .task._task_manager_factory import _TaskManagerFactory
 from .task.task import Task
 from .task.task_id import TaskId
@@ -92,7 +92,20 @@ def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Tas
 
 def is_editable(
     entity: Union[
-        DataNode, Task, Job, Sequence, Scenario, Cycle, DataNodeId, TaskId, JobId, SequenceId, ScenarioId, CycleId
+        DataNode,
+        Task,
+        Job,
+        Sequence,
+        Scenario,
+        Cycle,
+        Submission,
+        DataNodeId,
+        TaskId,
+        JobId,
+        SequenceId,
+        ScenarioId,
+        CycleId,
+        SubmissionId,
     ]
 ) -> bool:
     """Indicate if an entity can be edited.
@@ -114,12 +127,27 @@ def is_editable(
         return _JobManagerFactory._build_manager()._is_editable(entity)  # type: ignore
     if isinstance(entity, DataNode) or (isinstance(entity, str) and entity.startswith(DataNode._ID_PREFIX)):
         return _DataManagerFactory._build_manager()._is_editable(entity)  # type: ignore
+    if isinstance(entity, Submission) or (isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX)):
+        return _SubmissionManagerFactory._build_manager()._is_editable(entity)  # type: ignore
     return False
 
 
 def is_readable(
     entity: Union[
-        DataNode, Task, Job, Sequence, Scenario, Cycle, DataNodeId, TaskId, JobId, SequenceId, ScenarioId, CycleId
+        DataNode,
+        Task,
+        Job,
+        Sequence,
+        Scenario,
+        Cycle,
+        Submission,
+        DataNodeId,
+        TaskId,
+        JobId,
+        SequenceId,
+        ScenarioId,
+        CycleId,
+        SubmissionId,
     ]
 ) -> bool:
     """Indicate if an entity can be read.
@@ -141,6 +169,8 @@ def is_readable(
         return _JobManagerFactory._build_manager()._is_readable(entity)  # type: ignore
     if isinstance(entity, DataNode) or (isinstance(entity, str) and entity.startswith(DataNode._ID_PREFIX)):
         return _DataManagerFactory._build_manager()._is_readable(entity)  # type: ignore
+    if isinstance(entity, Submission) or (isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX)):
+        return _SubmissionManagerFactory._build_manager()._is_readable(entity)  # type: ignore
     return False
 
 
@@ -210,17 +240,22 @@ def exists(entity_id: JobId) -> bool:
     ...
 
 
+@overload
+def exists(entity_id: SubmissionId) -> bool:
+    ...
+
+
 @overload
 def exists(entity_id: str) -> bool:
     ...
 
 
-def exists(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, str]) -> bool:
+def exists(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId, str]) -> bool:
     """Check if an entity with the specified identifier exists.
 
     This function checks if an entity with the given identifier exists.
     It supports various types of entity identifiers, including `TaskId^`,
-    `DataNodeId^`, `SequenceId^`, `ScenarioId^`, `JobId^`, `CycleId^`, and string
+    `DataNodeId^`, `SequenceId^`, `ScenarioId^`, `JobId^`, `CycleId^`, `SubmissionId^`, and string
     representations.
 
     Parameters:
@@ -250,6 +285,8 @@ def exists(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, C
         return _TaskManagerFactory._build_manager()._exists(TaskId(entity_id))
     if entity_id.startswith(DataNode._ID_PREFIX):
         return _DataManagerFactory._build_manager()._exists(DataNodeId(entity_id))
+    if entity_id.startswith(Submission._ID_PREFIX):
+        return _SubmissionManagerFactory._build_manager()._exists(SubmissionId(entity_id))
     raise ModelNotFound("NOT_DETERMINED", entity_id)
 
 
@@ -284,18 +321,23 @@ def get(entity_id: JobId) -> Job:
 
 
 @overload
-def get(entity_id: str) -> Union[Task, DataNode, Sequence, Scenario, Job, Cycle]:
+def get(entity_id: SubmissionId) -> Submission:
+    ...
+
+
+@overload
+def get(entity_id: str) -> Union[Task, DataNode, Sequence, Scenario, Job, Cycle, Submission]:
     ...
 
 
 def get(
-    entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, str]
-) -> Union[Task, DataNode, Sequence, Scenario, Job, Cycle]:
+    entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId, str]
+) -> Union[Task, DataNode, Sequence, Scenario, Job, Cycle, Submission]:
     """Retrieve an entity by its unique identifier.
 
     This function allows you to retrieve an entity by specifying its identifier.
     The identifier must match the pattern of one of the supported entity types:
-    Task^, DataNode^, Sequence^, Job^, Cycle^, or Scenario^.
+    Task^, DataNode^, Sequence^, Job^, Cycle^, Submission^, or Scenario^.
 
 
     Parameters:
@@ -322,6 +364,8 @@ def get(
         return _TaskManagerFactory._build_manager()._get(TaskId(entity_id))
     if entity_id.startswith(DataNode._ID_PREFIX):
         return _DataManagerFactory._build_manager()._get(DataNodeId(entity_id))
+    if entity_id.startswith(Submission._ID_PREFIX):
+        return _SubmissionManagerFactory._build_manager()._get(SubmissionId(entity_id))
     raise ModelNotFound("NOT_DETERMINED", entity_id)
 
 
@@ -336,26 +380,28 @@ def get_tasks() -> List[Task]:
     return _TaskManagerFactory._build_manager()._get_all()
 
 
-def is_deletable(entity: Union[Scenario, Job, ScenarioId, JobId]) -> bool:
-    """Check if a `Scenario^` or a `Job^` can be deleted.
+def is_deletable(entity: Union[Scenario, Job, Submission, ScenarioId, JobId, SubmissionId]) -> bool:
+    """Check if a `Scenario^`, a `Job^` or a `Submission^` can be deleted.
 
     This function determines whether a scenario or a job can be safely
     deleted without causing conflicts or issues.
 
     Parameters:
-        entity (Union[Scenario, Job, ScenarioId, JobId]): The scenario or job to check.
+        entity (Union[Scenario, Job, Submission, ScenarioId, JobId, SubmissionId]): The scenario, job or submission to check.
 
     Returns:
-        True if the given scenario or job can be deleted. False otherwise.
+        True if the given scenario, job or submission can be deleted. False otherwise.
     """
     if isinstance(entity, str) and entity.startswith(Job._ID_PREFIX) or isinstance(entity, Job):
         return _JobManagerFactory._build_manager()._is_deletable(entity)  # type: ignore
     if isinstance(entity, str) and entity.startswith(Scenario._ID_PREFIX) or isinstance(entity, Scenario):
         return _ScenarioManagerFactory._build_manager()._is_deletable(entity)  # type: ignore
+    if isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX) or isinstance(entity, Submission):
+        return _SubmissionManagerFactory._build_manager()._is_deletable(entity)  # type: ignore
     return True
 
 
-def delete(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId]):
+def delete(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId]):
     """Delete an entity and its nested entities.
 
     This function deletes the specified entity and recursively deletes all its nested entities.
@@ -368,6 +414,8 @@ def delete(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, C
       the scenario can be deleted.
     - If a `SequenceId` is provided, the related jobs are deleted.
     - If a `TaskId` is provided, the related data nodes, and jobs are deleted.
+    - If a `SubmissionId^` or a `JobId^` is provided, the submission or job entity can only be deleted if
+      the execution has been finished.
 
     Parameters:
         entity_id (Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId]):
@@ -389,6 +437,9 @@ def delete(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, C
         return _TaskManagerFactory._build_manager()._hard_delete(TaskId(entity_id))
     if entity_id.startswith(DataNode._ID_PREFIX):
         return _DataManagerFactory._build_manager()._delete(DataNodeId(entity_id))
+    if entity_id.startswith(Submission._ID_PREFIX):
+        submission_manager = _SubmissionManagerFactory._build_manager()
+        return submission_manager._delete(submission_manager._get(SubmissionId(entity_id)))  # type: ignore
     raise ModelNotFound("NOT_DETERMINED", entity_id)
 
 
@@ -774,6 +825,7 @@ def clean_all_entities_by_version(version_number=None) -> bool:
     _SequenceManagerFactory._build_manager()._delete_by_version(version_number)
     _TaskManagerFactory._build_manager()._delete_by_version(version_number)
     _DataManagerFactory._build_manager()._delete_by_version(version_number)
+    _SubmissionManagerFactory._build_manager()._delete_by_version(version_number)
 
     version_manager._delete(version_number)
     try:

+ 2 - 0
tests/core/job/test_job_manager.py

@@ -60,6 +60,7 @@ def test_create_jobs():
     assert job_1.submit_id == "submit_id"
     assert job_1.submit_entity_id == "secnario_id"
     assert job_1.force
+    assert not _JobManager._is_editable(job_1)
 
     job_2 = _JobManager._create(task, [print], "submit_id_1", "secnario_id", False)
     assert _JobManager._get(job_2.id) == job_2
@@ -69,6 +70,7 @@ def test_create_jobs():
     assert job_2.submit_id == "submit_id_1"
     assert job_2.submit_entity_id == "secnario_id"
     assert not job_2.force
+    assert not _JobManager._is_editable(job_2)
 
 
 def test_get_job():

+ 40 - 0
tests/core/submission/test_submission.py

@@ -736,3 +736,43 @@ def test_update_submission_status_with_two_jobs_failed(job_ids, job_statuses, ex
 )
 def test_update_submission_status_with_two_jobs_canceled(job_ids, job_statuses, expected_submission_statuses):
     __test_update_submission_status_with_two_jobs(job_ids, job_statuses, expected_submission_statuses)
+
+
+def test_is_finished():
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
+    submission = Submission("entity_id", "submission_id", "entity_config_id")
+    submission_manager._set(submission)
+
+    assert len(submission_manager._get_all()) == 1
+
+    assert submission._submission_status == SubmissionStatus.SUBMITTED
+    assert not submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.UNDEFINED
+    assert submission.submission_status == SubmissionStatus.UNDEFINED
+    assert submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.CANCELED
+    assert submission.submission_status == SubmissionStatus.CANCELED
+    assert submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.FAILED
+    assert submission.submission_status == SubmissionStatus.FAILED
+    assert submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.BLOCKED
+    assert submission.submission_status == SubmissionStatus.BLOCKED
+    assert not submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.RUNNING
+    assert submission.submission_status == SubmissionStatus.RUNNING
+    assert not submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.PENDING
+    assert submission.submission_status == SubmissionStatus.PENDING
+    assert not submission.is_finished()
+
+    submission.submission_status = SubmissionStatus.COMPLETED
+    assert submission.submission_status == SubmissionStatus.COMPLETED
+    assert submission.is_finished()

+ 64 - 0
tests/core/submission/test_submission_manager.py

@@ -12,7 +12,10 @@
 from datetime import datetime
 from time import sleep
 
+import pytest
+
 from taipy.core._version._version_manager_factory import _VersionManagerFactory
+from taipy.core.exceptions.exceptions import SubmissionNotDeletedException
 from taipy.core.submission._submission_manager_factory import _SubmissionManagerFactory
 from taipy.core.submission.submission import Submission
 from taipy.core.submission.submission_status import SubmissionStatus
@@ -99,6 +102,11 @@ def test_delete_submission():
     submission = Submission("entity_id", "submission_id", "entity_config_id")
     submission_manager._set(submission)
 
+    with pytest.raises(SubmissionNotDeletedException):
+        submission_manager._delete(submission.id)
+
+    submission.submission_status = SubmissionStatus.COMPLETED
+
     for i in range(10):
         submission_manager._set(Submission("entity_id", f"submission_{i}", "entity_config_id"))
 
@@ -111,3 +119,59 @@ def test_delete_submission():
 
     submission_manager._delete_all()
     assert len(submission_manager._get_all()) == 0
+
+
+def test_is_deletable():
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
+    submission = Submission("entity_id", "submission_id", "entity_config_id")
+    submission_manager._set(submission)
+
+    assert len(submission_manager._get_all()) == 1
+
+    assert submission._submission_status == SubmissionStatus.SUBMITTED
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.UNDEFINED
+    assert submission.submission_status == SubmissionStatus.UNDEFINED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.CANCELED
+    assert submission.submission_status == SubmissionStatus.CANCELED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.FAILED
+    assert submission.submission_status == SubmissionStatus.FAILED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.BLOCKED
+    assert submission.submission_status == SubmissionStatus.BLOCKED
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.RUNNING
+    assert submission.submission_status == SubmissionStatus.RUNNING
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.PENDING
+    assert submission.submission_status == SubmissionStatus.PENDING
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.COMPLETED
+    assert submission.submission_status == SubmissionStatus.COMPLETED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)

+ 66 - 0
tests/core/submission/test_submission_manager_with_sql_repo.py

@@ -12,8 +12,11 @@
 from datetime import datetime
 from time import sleep
 
+import pytest
+
 from taipy.core import Task
 from taipy.core._version._version_manager_factory import _VersionManagerFactory
+from taipy.core.exceptions.exceptions import SubmissionNotDeletedException
 from taipy.core.submission._submission_manager_factory import _SubmissionManagerFactory
 from taipy.core.submission.submission import Submission
 from taipy.core.submission.submission_status import SubmissionStatus
@@ -111,6 +114,11 @@ def test_delete_submission(init_sql_repo):
     submission = Submission("entity_id", "submission_id", "entity_config_id")
     submission_manager._set(submission)
 
+    with pytest.raises(SubmissionNotDeletedException):
+        submission_manager._delete(submission.id)
+
+    submission.submission_status = SubmissionStatus.COMPLETED
+
     for i in range(10):
         submission_manager._set(Submission("entity_id", f"submission_{i}", "entity_config_id"))
 
@@ -123,3 +131,61 @@ def test_delete_submission(init_sql_repo):
 
     submission_manager._delete_all()
     assert len(submission_manager._get_all()) == 0
+
+
+def test_is_deletable(init_sql_repo):
+    init_managers()
+
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
+    submission = Submission("entity_id", "submission_id", "entity_config_id")
+    submission_manager._set(submission)
+
+    assert len(submission_manager._get_all()) == 1
+
+    assert submission._submission_status == SubmissionStatus.SUBMITTED
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.UNDEFINED
+    assert submission.submission_status == SubmissionStatus.UNDEFINED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.CANCELED
+    assert submission.submission_status == SubmissionStatus.CANCELED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.FAILED
+    assert submission.submission_status == SubmissionStatus.FAILED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.BLOCKED
+    assert submission.submission_status == SubmissionStatus.BLOCKED
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.RUNNING
+    assert submission.submission_status == SubmissionStatus.RUNNING
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.PENDING
+    assert submission.submission_status == SubmissionStatus.PENDING
+    assert not submission.is_deletable()
+    assert not submission_manager._is_deletable(submission)
+    assert not submission_manager._is_deletable(submission.id)
+
+    submission.submission_status = SubmissionStatus.COMPLETED
+    assert submission.submission_status == SubmissionStatus.COMPLETED
+    assert submission.is_deletable()
+    assert submission_manager._is_deletable(submission)
+    assert submission_manager._is_deletable(submission.id)