Skip to content

Commit cb0e73d

Browse files
feat(workflow): workflow revert command (#2956)
1 parent 610e88a commit cb0e73d

File tree

28 files changed

+1036
-216
lines changed

28 files changed

+1036
-216
lines changed
143 Bytes
Binary file not shown.

docs/cheatsheet_hash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
3e7d7567c17eba64945b70a3c1009343 cheatsheet.tex
1+
62d4f12d9be4d80c0dfbdef2957fde9a cheatsheet.tex
22
c70c179e07f04186ec05497564165f11 sdsc_cheatsheet.cls

renku/command/checks/activities.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# limitations under the License.
1818
"""Checks needed to determine integrity of datasets."""
1919

20-
from itertools import chain
20+
import itertools
2121

2222
import click
2323

@@ -33,7 +33,7 @@ def check_migrated_activity_ids(
3333
client, fix, activity_gateway: IActivityGateway, database_dispatcher: IDatabaseDispatcher
3434
):
3535
"""Check that activity ids were correctly migrated in the past."""
36-
activities = activity_gateway.get_all_activities()
36+
activities = activity_gateway.get_all_activities(include_deleted=True)
3737

3838
wrong_activities = [a for a in activities if not a.id.startswith("/activities/")]
3939

@@ -42,40 +42,16 @@ def check_migrated_activity_ids(
4242
for activity in wrong_activities:
4343
communication.info(f"Fixing activity '{activity.id}'")
4444

45-
old_id = activity.id
46-
47-
# NOTE: Remove activity relations
48-
tok = current_database["activity-catalog"].tokenizeQuery
49-
relations = chain(
50-
list(current_database["activity-catalog"].findRelationChains(tok(downstream=activity))),
51-
list(current_database["activity-catalog"].findRelationChains(tok(upstream=activity))),
52-
)
53-
for rel_collection in relations:
54-
for r in list(rel_collection):
55-
current_database["activity-catalog"].unindex(r)
56-
57-
current_database["activities"].pop(old_id)
45+
activity_gateway.remove(activity, keep_reference=False)
5846

5947
# NOTE: Modify id on activity and children
6048
activity.unfreeze()
6149
activity.id = f"/activities/{activity.id}"
6250
activity._p_oid = current_database.hash_id(activity.id)
6351
activity.freeze()
6452

65-
for usage in activity.usages:
66-
current_database["activities-by-usage"][usage.entity.path] = [
67-
a for a in current_database["activities-by-usage"][usage.entity.path] if a != activity
68-
]
69-
object.__setattr__(usage, "id", f"/activities/{usage.id}")
70-
71-
for generation in activity.generations:
72-
current_database["activities-by-generation"][generation.entity.path] = [
73-
a for a in current_database["activities-by-generation"][generation.entity.path] if a != activity
74-
]
75-
object.__setattr__(generation, "id", f"/activities/{generation.id}")
76-
77-
for parameter in activity.parameters:
78-
object.__setattr__(parameter, "id", f"/activities/{parameter.id}")
53+
for attribute in itertools.chain(activity.usages, activity.generations, activity.parameters):
54+
object.__setattr__(attribute, "id", f"/activities/{attribute.id}") # type: ignore
7955

8056
activity.association.id = f"/activities/{activity.association.id}"
8157

renku/command/graph.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ def _get_graph_for_all_objects(
160160
List of JSON-LD metadata.
161161
"""
162162
project = project_gateway.get_project()
163-
objects: List[Union[Project, Dataset, DatasetTag, Activity, AbstractPlan]] = activity_gateway.get_all_activities()
163+
# NOTE: Include deleted activities when exporting graph
164+
objects: List[Union[Project, Dataset, DatasetTag, Activity, AbstractPlan]] = activity_gateway.get_all_activities(
165+
include_deleted=True
166+
)
164167

165168
processed_plans = set()
166169

renku/command/update.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _update(
6262
paths = get_relative_paths(base=client.path, paths=[Path.cwd() / p for p in paths])
6363

6464
modified, _ = get_all_modified_and_deleted_activities_and_entities(client.repository)
65-
modified_activities = {a for a, _ in modified if is_activity_valid(a)}
65+
modified_activities = {a for a, _ in modified if not a.deleted and is_activity_valid(a)}
6666
modified_paths = {e.path for _, e in modified}
6767

6868
activities = get_downstream_generating_activities(

renku/command/workflow.py

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
# limitations under the License.
1818
"""Renku workflow commands."""
1919

20-
2120
import itertools
2221
import re
2322
from collections import defaultdict
2423
from functools import reduce
2524
from pathlib import Path
2625
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
2726

27+
from renku.core.workflow.plan import remove_plan
2828
from renku.domain_model.provenance.annotation import Annotation
2929

3030
if TYPE_CHECKING:
@@ -45,7 +45,12 @@
4545
from renku.core.util import communication
4646
from renku.core.util.datetime8601 import local_now
4747
from renku.core.util.os import are_paths_related, get_relative_paths, safe_read_yaml
48-
from renku.core.workflow.activity import create_activity_graph, get_activities_until_paths, sort_activities
48+
from renku.core.workflow.activity import (
49+
create_activity_graph,
50+
get_activities_until_paths,
51+
revert_activity,
52+
sort_activities,
53+
)
4954
from renku.core.workflow.concrete_execution_graph import ExecutionGraph
5055
from renku.core.workflow.plan_factory import delete_indirect_files_list
5156
from renku.core.workflow.value_resolution import CompositePlanValueResolver, ValueResolver
@@ -110,35 +115,9 @@ def list_workflows_command():
110115
return Command().command(_list_workflows).require_migration().with_database(write=False)
111116

112117

113-
@inject.autoparams()
114-
def _remove_workflow(name: str, force: bool, plan_gateway: IPlanGateway):
115-
"""Remove the remote named <name>.
116-
117-
Args:
118-
name (str): The name of the Plan to remove.
119-
force (bool): Whether to force removal or not.
120-
plan_gateway(IPlanGateway): The injected Plan gateway.
121-
Raises:
122-
errors.ParameterError: If the Plan doesn't exist or was already deleted.
123-
"""
124-
workflows = plan_gateway.get_newest_plans_by_names()
125-
plan = None
126-
if name.startswith("/plans/"):
127-
plan = next(filter(lambda x: x.id == name, workflows.values()), None)
128-
if not plan and name not in workflows:
129-
raise errors.ParameterError(f'The specified workflow is "{name}" is not an active workflow.')
130-
131-
if not force:
132-
prompt_text = f'You are about to remove the following workflow "{name}".' + "\n" + "\nDo you wish to continue?"
133-
communication.confirm(prompt_text, abort=True, warning=True)
134-
135-
plan = plan or workflows[name]
136-
plan.delete()
137-
138-
139-
def remove_workflow_command():
118+
def remove_plan_command():
140119
"""Command that removes the workflow named <name>."""
141-
return Command().command(_remove_workflow).require_clean().with_database(write=True).with_commit()
120+
return Command().command(remove_plan).require_clean().with_database(write=True).with_commit()
142121

143122

144123
def _show_workflow(name_or_id: str):
@@ -954,3 +933,10 @@ def iterate_workflow_command():
954933
return (
955934
Command().command(_iterate_workflow).require_migration().require_clean().with_database(write=True).with_commit()
956935
)
936+
937+
938+
def revert_activity_command():
939+
"""Command that reverts an activity."""
940+
return (
941+
Command().command(revert_activity).require_migration().require_clean().with_database(write=True).with_commit()
942+
)

renku/core/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ class RenkuException(Exception):
3333
"""
3434

3535

36+
class ActivityDownstreamNotEmptyError(RenkuException):
37+
"""Raised when an activity cannot be deleted because its downstream is not empty."""
38+
39+
def __init__(self, activity):
40+
self.activity = activity
41+
super().__init__(f"Activity '{activity.id}' has non-empty downstream")
42+
43+
3644
class LockError(RenkuException):
3745
"""Raise when a project cannot be locked."""
3846

renku/core/interface/activity_gateway.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
class IActivityGateway(ABC):
2828
"""Interface for the ActivityGateway."""
2929

30+
def get_by_id(self, id: str) -> Optional[Activity]:
31+
"""Get an activity by id."""
32+
raise NotImplementedError
33+
3034
def get_all_usage_paths(self) -> List[str]:
3135
"""Return all usage paths."""
3236
raise NotImplementedError
@@ -59,7 +63,7 @@ def get_upstream_activity_chains(self, activity: Activity) -> List[Tuple[Activit
5963
"""Get a list of tuples of all upstream paths of this activity."""
6064
raise NotImplementedError
6165

62-
def get_all_activities(self) -> List[Activity]:
66+
def get_all_activities(self, include_deleted: bool = False) -> List[Activity]:
6367
"""Get all activities in the project."""
6468
raise NotImplementedError
6569

@@ -74,3 +78,13 @@ def add_activity_collection(self, activity_collection: ActivityCollection):
7478
def get_all_activity_collections(self) -> List[ActivityCollection]:
7579
"""Get all activity collections in the project."""
7680
raise NotImplementedError
81+
82+
def remove(self, activity: Activity, keep_reference: bool = True, force: bool = False):
83+
"""Remove an activity from the storage.
84+
85+
Args:
86+
activity(Activity): The activity to be removed.
87+
keep_reference(bool): Whether to keep the activity in the ``activities`` index or not.
88+
force(bool): Force-delete the activity even if it has downstream activities.
89+
"""
90+
raise NotImplementedError

renku/core/interface/plan_gateway.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
class IPlanGateway(ABC):
2727
"""Interface for the PlanGateway."""
2828

29-
def get_by_id(self, id: str) -> Optional[AbstractPlan]:
29+
def get_by_id(self, id: Optional[str]) -> Optional[AbstractPlan]:
3030
"""Get a plan by id."""
3131
raise NotImplementedError
3232

@@ -38,8 +38,8 @@ def list_by_name(self, starts_with: str, ends_with: str = None) -> List[str]:
3838
"""Search plans by name."""
3939
raise NotImplementedError
4040

41-
def get_newest_plans_by_names(self, with_invalidated: bool = False) -> Dict[str, AbstractPlan]:
42-
"""Return a list of all newest plans with their names."""
41+
def get_newest_plans_by_names(self, include_deleted: bool = False) -> Dict[str, AbstractPlan]:
42+
"""Return a mapping of all plan names to their newest plans."""
4343
raise NotImplementedError
4444

4545
def get_all_plans(self) -> List[AbstractPlan]:

0 commit comments

Comments
 (0)