Skip to content

Commit 3daa1ec

Browse files
authored
feat(core): allow partial updates on dataset and project edit (#2949)
1 parent add433e commit 3daa1ec

File tree

19 files changed

+471
-90
lines changed

19 files changed

+471
-90
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@
356356
("py:class", "IDatasetGateway"),
357357
("py:class", "IPlanGateway"),
358358
("py:class", "LocalClient"),
359+
("py:class", "NoValueType"),
359360
("py:class", "OID_TYPE"),
360361
("py:class", "Path"),
361362
("py:class", "Persistent"),

renku/command/project.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,34 @@
1717
# limitations under the License.
1818
"""Project management."""
1919

20+
from typing import Dict, List, Optional, Union, cast
21+
2022
from renku.command.command_builder import inject
2123
from renku.command.command_builder.command import Command
2224
from renku.command.view_model.project import ProjectViewModel
2325
from renku.core.interface.client_dispatcher import IClientDispatcher
2426
from renku.core.interface.project_gateway import IProjectGateway
2527
from renku.core.management.repository import DATABASE_METADATA_PATH
2628
from renku.core.util.metadata import construct_creator
29+
from renku.core.util.util import NO_VALUE, NoValueType
30+
from renku.domain_model.provenance.agent import Person
2731

2832

2933
@inject.autoparams()
30-
def _edit_project(description, creator, keywords, custom_metadata, project_gateway: IProjectGateway):
34+
def _edit_project(
35+
description: Optional[Union[str, NoValueType]],
36+
creator: Union[Dict, str, NoValueType],
37+
keywords: Optional[Union[List[str], NoValueType]],
38+
custom_metadata: Optional[Union[Dict, NoValueType]],
39+
project_gateway: IProjectGateway,
40+
):
3141
"""Edit dataset metadata.
3242
3343
Args:
34-
description: New description.
35-
creator: New creators.
36-
keywords: New keywords.
37-
custom_metadata: Custom JSON-LD metadata.
44+
description(Union[Optional[str], NoValueType]): New description.
45+
creator(Union[Dict, str, NoValueType]): New creators.
46+
keywords(Union[Optional[List[str]]): New keywords.
47+
custom_metadata(Union[Optional[Dict]): Custom JSON-LD metadata.
3848
project_gateway(IProjectGateway): Injected project gateway.
3949
4050
Returns:
@@ -47,14 +57,18 @@ def _edit_project(description, creator, keywords, custom_metadata, project_gatew
4757
"custom_metadata": custom_metadata,
4858
}
4959

50-
creator, no_email_warnings = construct_creator(creator, ignore_email=True)
60+
no_email_warnings: Optional[Union[Dict, str]] = None
61+
parsed_creator: Optional[Union[NoValueType, Person]] = NO_VALUE
62+
63+
if creator is not NO_VALUE:
64+
parsed_creator, no_email_warnings = construct_creator(cast(Union[Dict, str], creator), ignore_email=True)
5165

52-
updated = {k: v for k, v in possible_updates.items() if v}
66+
updated = {k: v for k, v in possible_updates.items() if v is not NO_VALUE}
5367

5468
if updated:
5569
project = project_gateway.get_project()
5670
project.update_metadata(
57-
creator=creator, description=description, keywords=keywords, custom_metadata=custom_metadata
71+
creator=parsed_creator, description=description, keywords=keywords, custom_metadata=custom_metadata
5872
)
5973
project_gateway.update_project(project)
6074

renku/core/dataset/dataset.py

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import urllib
2424
from collections import OrderedDict
2525
from pathlib import Path
26-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
26+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
2727

2828
import patoolib
2929

@@ -48,6 +48,7 @@
4848
from renku.core.util.os import delete_file
4949
from renku.core.util.tabulate import tabulate
5050
from renku.core.util.urls import get_slug, remove_credentials
51+
from renku.core.util.util import NO_VALUE, NoValueType
5152
from renku.domain_model.dataset import (
5253
Dataset,
5354
DatasetDetailsJson,
@@ -175,63 +176,68 @@ def create_dataset(
175176
@inject.autoparams("client_dispatcher")
176177
def edit_dataset(
177178
name: str,
178-
title: str,
179-
description: str,
180-
creators: List[Person],
179+
title: Optional[Union[str, NoValueType]],
180+
description: Optional[Union[str, NoValueType]],
181+
creators: Optional[Union[List[Person], NoValueType]],
181182
client_dispatcher: IClientDispatcher,
182-
keywords: Optional[List[str]] = None,
183-
images: Optional[List[ImageRequestModel]] = None,
184-
skip_image_update: bool = False,
185-
custom_metadata: Optional[Dict] = None,
183+
keywords: Optional[Union[List[str], NoValueType]] = NO_VALUE,
184+
images: Optional[Union[List[ImageRequestModel], NoValueType]] = NO_VALUE,
185+
custom_metadata: Optional[Union[Dict, NoValueType]] = NO_VALUE,
186186
):
187187
"""Edit dataset metadata.
188188
189189
Args:
190190
name(str): Name of the dataset to edit
191-
title(str): New title for the dataset.
192-
description(str): New description for the dataset.
193-
creators(List[Person]): New creators for the dataset.
191+
title(Optional[Union[str, NoValueType]]): New title for the dataset.
192+
description(Optional[Union[str, NoValueType]]): New description for the dataset.
193+
creators(Optional[Union[List[Person], NoValueType]]): New creators for the dataset.
194194
client_dispatcher(IClientDispatcher): Injected client dispatcher.
195-
keywords(List[str], optional): New keywords for dataset (Default value = None).
196-
images(List[ImageRequestModel], optional): New images for dataset (Default value = None).
197-
skip_image_update(bool, optional): Whether or not to skip updating dataset images (Default value = False).
198-
custom_metadata(Dict, optional): Custom JSON-LD metadata (Default value = None).
195+
keywords(Optional[Union[List[str], NoValueType]]): New keywords for dataset (Default value = ``NO_VALUE``).
196+
images(Optional[Union[List[ImageRequestModel], NoValueType]]): New images for dataset
197+
(Default value = ``NO_VALUE``).
198+
custom_metadata(Optional[Union[Dict, NoValueType]]): Custom JSON-LD metadata (Default value = ``NO_VALUE``).
199199
200200
Returns:
201201
bool: True if updates were performed.
202202
"""
203203
client = client_dispatcher.current_client
204204

205+
if isinstance(title, str):
206+
title = title.strip()
207+
208+
if title is None:
209+
title = ""
210+
205211
possible_updates = {
206212
"creators": creators,
207213
"description": description,
208214
"keywords": keywords,
209215
"title": title,
210216
}
211217

212-
title = title.strip() if isinstance(title, str) else ""
213-
214218
dataset_provenance = DatasetsProvenance()
215219
dataset = dataset_provenance.get_by_name(name=name)
216220

217221
if dataset is None:
218222
raise errors.ParameterError("Dataset does not exist.")
219223

220-
updated: Dict[str, Any] = {k: v for k, v in possible_updates.items() if v}
224+
updated: Dict[str, Any] = {k: v for k, v in possible_updates.items() if v != NO_VALUE}
221225

222226
if updated:
223227
dataset.update_metadata(creators=creators, description=description, keywords=keywords, title=title)
224228

225-
if skip_image_update:
229+
if images == NO_VALUE:
226230
images_updated = False
227231
else:
228-
images_updated = set_dataset_images(client, dataset, images)
232+
images_updated = set_dataset_images(client, dataset, cast(Optional[List[ImageRequestModel]], images))
229233

230234
if images_updated:
231-
updated["images"] = [{"content_url": i.content_url, "position": i.position} for i in dataset.images]
235+
updated["images"] = (
236+
None if images is None else [{"content_url": i.content_url, "position": i.position} for i in dataset.images]
237+
)
232238

233-
if custom_metadata:
234-
update_dataset_custom_metadata(dataset, custom_metadata)
239+
if custom_metadata is not NO_VALUE:
240+
update_dataset_custom_metadata(dataset, cast(Optional[Dict], custom_metadata))
235241
updated["custom_metadata"] = custom_metadata
236242

237243
if not updated:
@@ -840,7 +846,7 @@ def set_dataset_images(client: "LocalClient", dataset: Dataset, images: Optional
840846
return images_updated or dataset.images != previous_images
841847

842848

843-
def update_dataset_custom_metadata(dataset: Dataset, custom_metadata: Dict):
849+
def update_dataset_custom_metadata(dataset: Dataset, custom_metadata: Optional[Dict]):
844850
"""Update custom metadata on a dataset.
845851
846852
Args:
@@ -850,7 +856,8 @@ def update_dataset_custom_metadata(dataset: Dataset, custom_metadata: Dict):
850856

851857
existing_metadata = [a for a in dataset.annotations if a.source != "renku"]
852858

853-
existing_metadata.append(Annotation(id=Annotation.generate_id(), body=custom_metadata, source="renku"))
859+
if custom_metadata is not None:
860+
existing_metadata.append(Annotation(id=Annotation.generate_id(), body=custom_metadata, source="renku"))
854861

855862
dataset.annotations = existing_metadata
856863

renku/core/util/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from renku.domain_model.provenance.agent import Person
3535

3636

37-
def construct_creators(creators: List[Union[dict, str]], ignore_email=False):
37+
def construct_creators(creators: Optional[List[Union[dict, str]]], ignore_email=False):
3838
"""Parse input and return a list of Person."""
3939
creators = creators or []
4040

renku/core/util/util.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# limitations under the License.
1818
"""General utility functions."""
1919

20-
from typing import Any, Optional
20+
from typing import Any, NewType, Optional
2121

2222
from packaging.version import Version
2323

@@ -33,3 +33,10 @@ def to_semantic_version(value: str) -> Optional[Version]:
3333
return Version(value)
3434
except ValueError:
3535
return None
36+
37+
38+
NoValueType = NewType("NoValueType", object)
39+
"""Type to represent a value not being set in cases where ``None`` is a valid value."""
40+
41+
NO_VALUE = NoValueType(object())
42+
"""Sentinel to represent a value not being set in cases where ``None`` is a valid value."""

renku/domain_model/dataset.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from renku.core.util.git import get_entity_from_revision
3434
from renku.core.util.metadata import is_external_file
3535
from renku.core.util.urls import get_path, get_slug
36+
from renku.core.util.util import NO_VALUE
3637
from renku.infrastructure.immutable import Immutable, Slots
3738
from renku.infrastructure.persistent import Persistent
3839

@@ -554,7 +555,7 @@ def update_metadata(self, **kwargs):
554555
for name, value in kwargs.items():
555556
if name not in editable_attributes:
556557
raise errors.ParameterError(f"Cannot edit field: '{name}'")
557-
if value and value != getattr(self, name):
558+
if value is not NO_VALUE and value != getattr(self, name):
558559
setattr(self, name, value)
559560

560561
def unlink_file(self, path, missing_ok=False) -> Optional[DatasetFile]:

renku/domain_model/project.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from renku.core.util.datetime8601 import fix_datetime, local_now, parse_date
2828
from renku.core.util.git import get_git_user
2929
from renku.core.util.os import normalize_to_ascii
30+
from renku.core.util.util import NO_VALUE
3031
from renku.domain_model.provenance.agent import Person
3132
from renku.domain_model.provenance.annotation import Annotation
3233
from renku.version import __minimum_project_version__
@@ -155,12 +156,13 @@ def update_metadata(self, custom_metadata=None, **kwargs):
155156
for name, value in kwargs.items():
156157
if name not in editable_attributes:
157158
raise errors.ParameterError(f"Cannot edit field: '{name}'")
158-
if value and value != getattr(self, name):
159+
if value is not NO_VALUE and value != getattr(self, name):
159160
setattr(self, name, value)
160161

161-
if custom_metadata:
162+
if custom_metadata is not NO_VALUE:
162163
existing_metadata = [a for a in self.annotations if a.source != "renku"]
163164

164-
existing_metadata.append(Annotation(id=Annotation.generate_id(), body=custom_metadata, source="renku"))
165+
if custom_metadata is not None:
166+
existing_metadata.append(Annotation(id=Annotation.generate_id(), body=custom_metadata, source="renku"))
165167

166168
self.annotations = existing_metadata

renku/ui/cli/dataset.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@
6767
6868
Use the ``edit`` sub-command to change metadata of a dataset. You can edit the same
6969
set of metadata as the create command by passing the options described in the
70-
table above.
70+
table above. You can also use the ``-u/--unset`` options with one of the values
71+
from ``creators`` (short ``c``), ``keywords`` (short ``k``) or ``metadata``
72+
(short ``m``) to delete the respective values from the dataset.
7173
7274
.. code-block:: console
7375
74-
$ renku dataset edit my-dataset --title 'New title'
76+
$ renku dataset edit my-dataset --title 'New title' --unset keywords
7577
Successfully updated: title.
7678
7779
Listing all datasets:
@@ -543,6 +545,7 @@
543545
from renku.command.format.dataset_files import DATASET_FILES_COLUMNS, DATASET_FILES_FORMATS
544546
from renku.command.format.dataset_tags import DATASET_TAGS_FORMATS
545547
from renku.command.format.datasets import DATASETS_COLUMNS, DATASETS_FORMATS
548+
from renku.core.util.util import NO_VALUE
546549

547550

548551
def _complete_datasets(ctx, param, incomplete):
@@ -644,40 +647,79 @@ def create(name, title, description, creators, metadata, keyword):
644647

645648
@dataset.command()
646649
@click.argument("name", shell_complete=_complete_datasets)
647-
@click.option("-t", "--title", default=None, type=click.STRING, help="Title of the dataset.")
648-
@click.option("-d", "--description", default=None, type=click.STRING, help="Dataset's description.")
650+
@click.option("-t", "--title", default=NO_VALUE, type=click.UNPROCESSED, help="Title of the dataset.")
651+
@click.option("-d", "--description", default=NO_VALUE, type=click.UNPROCESSED, help="Dataset's description.")
649652
@click.option(
650653
"-c",
651654
"--creator",
652655
"creators",
653-
default=None,
656+
default=[NO_VALUE],
654657
multiple=True,
658+
type=click.UNPROCESSED,
655659
help="Creator's name, email, and affiliation. " "Accepted format is 'Forename Surname <email> [affiliation]'.",
656660
)
657661
@click.option(
658662
"-m",
659663
"--metadata",
660-
default=None,
661-
type=click.Path(exists=True, dir_okay=False),
664+
default=NO_VALUE,
665+
type=click.UNPROCESSED,
662666
help="Custom metadata to be associated with the dataset.",
663667
)
664-
@click.option("-k", "--keyword", default=None, multiple=True, type=click.STRING, help="List of keywords or tags.")
665-
def edit(name, title, description, creators, metadata, keyword):
668+
@click.option(
669+
"-k",
670+
"--keyword",
671+
"keywords",
672+
default=[NO_VALUE],
673+
multiple=True,
674+
type=click.UNPROCESSED,
675+
help="List of keywords or tags.",
676+
)
677+
@click.option(
678+
"-u",
679+
"--unset",
680+
default=[],
681+
multiple=True,
682+
type=click.Choice(["keywords", "k", "images", "i", "metadata", "m"]),
683+
help="Remove keywords from dataset.",
684+
)
685+
def edit(name, title, description, creators, metadata, keywords, unset):
666686
"""Edit dataset metadata."""
667687
from renku.command.dataset import edit_dataset_command
668688
from renku.core.util.metadata import construct_creators
669689
from renku.ui.cli.utils.callback import ClickCallback
670690

671-
creators = creators or ()
672-
keywords = keyword or ()
691+
images = NO_VALUE
673692

674-
custom_metadata = None
693+
if list(creators) == [NO_VALUE]:
694+
creators = NO_VALUE
695+
696+
if list(keywords) == [NO_VALUE]:
697+
keywords = NO_VALUE
698+
699+
if "k" in unset or "keywords" in unset:
700+
if keywords is not NO_VALUE:
701+
raise click.UsageError("Cant use '--keyword' together with unsetting keyword")
702+
keywords = None
703+
704+
if "m" in unset or "metadata" in unset:
705+
if metadata is not NO_VALUE:
706+
raise click.UsageError("Cant use '--metadata' together with unsetting metadata")
707+
metadata = None
708+
709+
if "i" in unset or "images" in unset:
710+
images = None
711+
712+
custom_metadata = metadata
675713
no_email_warnings = False
676714

677-
if creators:
715+
if creators and creators is not NO_VALUE:
678716
creators, no_email_warnings = construct_creators(creators, ignore_email=True)
679717

680-
if metadata:
718+
if metadata and metadata is not NO_VALUE:
719+
path = Path(metadata)
720+
721+
if not path.exists():
722+
raise click.UsageError(f"Path {path} does not exist.")
681723
custom_metadata = json.loads(Path(metadata).read_text())
682724

683725
updated = (
@@ -689,7 +731,7 @@ def edit(name, title, description, creators, metadata, keyword):
689731
description=description,
690732
creators=creators,
691733
keywords=keywords,
692-
skip_image_update=True,
734+
images=images,
693735
custom_metadata=custom_metadata,
694736
)
695737
).output

0 commit comments

Comments
 (0)