Skip to content

Commit 0b375c7

Browse files
authored
Merge pull request #172 from simleo/cli_add_properties
CLI: Support for adding arbitrary properties
2 parents 2ea7fae + 5e2802e commit 0b375c7

File tree

4 files changed

+88
-27
lines changed

4 files changed

+88
-27
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ The command acts on the current directory, unless the `-c` option is specified.
310310

311311
### Adding items to the crate
312312

313-
The `rocrate add` command allows to add workflows and other entity types (currently [testing-related metadata](https://crs4.github.io/life_monitor/workflow_testing_ro_crate)) to an RO-Crate:
313+
The `rocrate add` command allows to add file, datasets (directories), workflows and other entity types (currently [testing-related metadata](https://crs4.github.io/life_monitor/workflow_testing_ro_crate)) to an RO-Crate:
314314

315315
```console
316316
$ rocrate add --help
@@ -320,6 +320,8 @@ Options:
320320
--help Show this message and exit.
321321

322322
Commands:
323+
dataset
324+
file
323325
test-definition
324326
test-instance
325327
test-suite
@@ -372,6 +374,29 @@ rocrate add test-instance test1 http://example.com -r jobs -i test1_1
372374
rocrate add test-definition test1 test/test1/sort-and-change-case-test.yml -e planemo -v '>=0.70'
373375
```
374376

377+
To add files or directories after crate initialization:
378+
379+
```bash
380+
cp ../sample_file.txt .
381+
rocrate add file sample_file.txt -P name=sample -P description="Sample file"
382+
cp -r ../test_add_dir .
383+
rocrate add dataset test_add_dir
384+
```
385+
386+
The above example also shows how to set arbitrary properties for the entity with `-P`. This is supported by most `rocrate add` subcommands.
387+
388+
```console
389+
$ rocrate add workflow --help
390+
Usage: rocrate add workflow [OPTIONS] PATH
391+
392+
Options:
393+
-l, --language [cwl|galaxy|knime|nextflow|snakemake|compss|autosubmit]
394+
-c, --crate-dir PATH
395+
-P, --property KEY=VALUE
396+
--help Show this message and exit.
397+
```
398+
399+
375400
## License
376401

377402
* Copyright 2019-2024 The University of Manchester, UK

rocrate/cli.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,20 @@ def convert(self, value, param, ctx):
4444
self.fail(f"{value!r} is not splittable", param, ctx)
4545

4646

47+
class KeyValueParamType(click.ParamType):
48+
name = "key_value"
49+
50+
def convert(self, value, param, ctx):
51+
try:
52+
return tuple(value.split("=", 1)) if value else ()
53+
except AttributeError:
54+
self.fail(f"{value!r} is not splittable", param, ctx)
55+
56+
4757
CSV = CSVParamType()
58+
KeyValue = KeyValueParamType()
4859
OPTION_CRATE_PATH = click.option('-c', '--crate-dir', type=click.Path(), default=os.getcwd)
60+
OPTION_PROPS = click.option('-P', '--property', type=KeyValue, multiple=True, metavar="KEY=VALUE")
4961

5062

5163
@click.group()
@@ -72,38 +84,41 @@ def add():
7284
@add.command()
7385
@click.argument('path', type=click.Path(exists=True, dir_okay=False))
7486
@OPTION_CRATE_PATH
75-
def file(crate_dir, path):
87+
@OPTION_PROPS
88+
def file(crate_dir, path, property):
7689
crate = ROCrate(crate_dir, init=False, gen_preview=False)
7790
source = Path(path).resolve(strict=True)
7891
try:
7992
dest_path = source.relative_to(crate_dir)
8093
except ValueError:
8194
# For now, only support adding an existing file to the metadata
8295
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
83-
crate.add_file(source, dest_path)
96+
crate.add_file(source, dest_path, properties=dict(property))
8497
crate.metadata.write(crate_dir)
8598

8699

87100
@add.command()
88101
@click.argument('path', type=click.Path(exists=True, file_okay=False))
89102
@OPTION_CRATE_PATH
90-
def dataset(crate_dir, path):
103+
@OPTION_PROPS
104+
def dataset(crate_dir, path, property):
91105
crate = ROCrate(crate_dir, init=False, gen_preview=False)
92106
source = Path(path).resolve(strict=True)
93107
try:
94108
dest_path = source.relative_to(crate_dir)
95109
except ValueError:
96110
# For now, only support adding an existing directory to the metadata
97111
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
98-
crate.add_dataset(source, dest_path)
112+
crate.add_dataset(source, dest_path, properties=dict(property))
99113
crate.metadata.write(crate_dir)
100114

101115

102116
@add.command()
103117
@click.argument('path', type=click.Path(exists=True))
104118
@click.option('-l', '--language', type=click.Choice(LANG_CHOICES), default="cwl")
105119
@OPTION_CRATE_PATH
106-
def workflow(crate_dir, path, language):
120+
@OPTION_PROPS
121+
def workflow(crate_dir, path, language, property):
107122
crate = ROCrate(crate_dir, init=False, gen_preview=False)
108123
source = Path(path).resolve(strict=True)
109124
try:
@@ -112,7 +127,7 @@ def workflow(crate_dir, path, language):
112127
# For now, only support marking an existing file as a workflow
113128
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
114129
# TODO: add command options for main and gen_cwl
115-
crate.add_workflow(source, dest_path, main=True, lang=language, gen_cwl=False)
130+
crate.add_workflow(source, dest_path, main=True, lang=language, gen_cwl=False, properties=dict(property))
116131
crate.metadata.write(crate_dir)
117132

118133

@@ -121,9 +136,13 @@ def workflow(crate_dir, path, language):
121136
@click.option('-n', '--name')
122137
@click.option('-m', '--main-entity')
123138
@OPTION_CRATE_PATH
124-
def suite(crate_dir, identifier, name, main_entity):
139+
@OPTION_PROPS
140+
def suite(crate_dir, identifier, name, main_entity, property):
125141
crate = ROCrate(crate_dir, init=False, gen_preview=False)
126-
suite = crate.add_test_suite(identifier=add_hash(identifier), name=name, main_entity=main_entity)
142+
suite = crate.add_test_suite(
143+
identifier=add_hash(identifier), name=name, main_entity=main_entity,
144+
properties=dict(property)
145+
)
127146
crate.metadata.write(crate_dir)
128147
print(suite.id)
129148

@@ -136,11 +155,12 @@ def suite(crate_dir, identifier, name, main_entity):
136155
@click.option('-i', '--identifier')
137156
@click.option('-n', '--name')
138157
@OPTION_CRATE_PATH
139-
def instance(crate_dir, suite, url, resource, service, identifier, name):
158+
@OPTION_PROPS
159+
def instance(crate_dir, suite, url, resource, service, identifier, name, property):
140160
crate = ROCrate(crate_dir, init=False, gen_preview=False)
141161
instance_ = crate.add_test_instance(
142162
add_hash(suite), url, resource=resource, service=service,
143-
identifier=add_hash(identifier), name=name
163+
identifier=add_hash(identifier), name=name, properties=dict(property)
144164
)
145165
crate.metadata.write(crate_dir)
146166
print(instance_.id)
@@ -152,7 +172,8 @@ def instance(crate_dir, suite, url, resource, service, identifier, name):
152172
@click.option('-e', '--engine', type=click.Choice(ENGINE_CHOICES), default="planemo")
153173
@click.option('-v', '--engine-version')
154174
@OPTION_CRATE_PATH
155-
def definition(crate_dir, suite, path, engine, engine_version):
175+
@OPTION_PROPS
176+
def definition(crate_dir, suite, path, engine, engine_version, property):
156177
crate = ROCrate(crate_dir, init=False, gen_preview=False)
157178
source = Path(path).resolve(strict=True)
158179
try:
@@ -162,7 +183,7 @@ def definition(crate_dir, suite, path, engine, engine_version):
162183
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
163184
crate.add_test_definition(
164185
add_hash(suite), source=source, dest_path=dest_path, engine=engine,
165-
engine_version=engine_version
186+
engine_version=engine_version, properties=dict(property)
166187
)
167188
crate.metadata.write(crate_dir)
168189

rocrate/rocrate.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -497,23 +497,24 @@ def add_workflow(
497497
workflow.subjectOf = cwl_workflow
498498
return workflow
499499

500-
def add_test_suite(self, identifier=None, name=None, main_entity=None):
500+
def add_test_suite(self, identifier=None, name=None, main_entity=None, properties=None):
501501
test_ref_prop = "mentions"
502502
if not main_entity:
503503
main_entity = self.mainEntity
504504
if not main_entity:
505505
test_ref_prop = "about"
506-
suite = self.add(TestSuite(self, identifier))
507-
suite.name = name or suite.id.lstrip("#")
506+
suite = self.add(TestSuite(self, identifier, properties=properties))
507+
if not properties or "name" not in properties:
508+
suite.name = name or suite.id.lstrip("#")
508509
if main_entity:
509510
suite["mainEntity"] = main_entity
510511
self.root_dataset.append_to(test_ref_prop, suite)
511512
self.metadata.extra_terms.update(TESTING_EXTRA_TERMS)
512513
return suite
513514

514-
def add_test_instance(self, suite, url, resource="", service="jenkins", identifier=None, name=None):
515+
def add_test_instance(self, suite, url, resource="", service="jenkins", identifier=None, name=None, properties=None):
515516
suite = self.__validate_suite(suite)
516-
instance = self.add(TestInstance(self, identifier))
517+
instance = self.add(TestInstance(self, identifier, properties=properties))
517518
instance.url = url
518519
instance.resource = resource
519520
if isinstance(service, TestService):
@@ -522,7 +523,8 @@ def add_test_instance(self, suite, url, resource="", service="jenkins", identifi
522523
service = get_service(self, service)
523524
self.add(service)
524525
instance.service = service
525-
instance.name = name or instance.id.lstrip("#")
526+
if not properties or "name" not in properties:
527+
instance.name = name or instance.id.lstrip("#")
526528
suite.append_to("instance", instance)
527529
self.metadata.extra_terms.update(TESTING_EXTRA_TERMS)
528530
return instance

test/test_cli.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,19 @@ def test_cli_add_file(tmpdir, test_data_dir, helpers, monkeypatch, cwd):
145145
# add
146146
shutil.copy(test_data_dir / "sample_file.txt", crate_dir)
147147
file_path = crate_dir / "sample_file.txt"
148-
args = ["add", "file"]
148+
args = ["add", "file", str(file_path), "-P", "name=foo", "-P", "description=foo bar"]
149149
if cwd:
150150
monkeypatch.chdir(str(crate_dir))
151151
file_path = file_path.relative_to(crate_dir)
152152
else:
153153
args.extend(["-c", str(crate_dir)])
154-
args.append(str(file_path))
155154
result = runner.invoke(cli, args)
156155
assert result.exit_code == 0
157156
json_entities = helpers.read_json_entities(crate_dir)
158157
assert "sample_file.txt" in json_entities
159158
assert json_entities["sample_file.txt"]["@type"] == "File"
159+
assert json_entities["sample_file.txt"]["name"] == "foo"
160+
assert json_entities["sample_file.txt"]["description"] == "foo bar"
160161

161162

162163
@pytest.mark.parametrize("cwd", [False, True])
@@ -171,18 +172,19 @@ def test_cli_add_dataset(tmpdir, test_data_dir, helpers, monkeypatch, cwd):
171172
# add
172173
dataset_path = crate_dir / "test_add_dir"
173174
shutil.copytree(test_data_dir / "test_add_dir", dataset_path)
174-
args = ["add", "dataset"]
175+
args = ["add", "dataset", str(dataset_path), "-P", "name=foo", "-P", "description=foo bar"]
175176
if cwd:
176177
monkeypatch.chdir(str(crate_dir))
177178
dataset_path = dataset_path.relative_to(crate_dir)
178179
else:
179180
args.extend(["-c", str(crate_dir)])
180-
args.append(str(dataset_path))
181181
result = runner.invoke(cli, args)
182182
assert result.exit_code == 0
183183
json_entities = helpers.read_json_entities(crate_dir)
184184
assert "test_add_dir/" in json_entities
185185
assert json_entities["test_add_dir/"]["@type"] == "Dataset"
186+
assert json_entities["test_add_dir/"]["name"] == "foo"
187+
assert json_entities["test_add_dir/"]["description"] == "foo bar"
186188

187189

188190
@pytest.mark.parametrize("cwd", [False, True])
@@ -196,7 +198,7 @@ def test_cli_add_workflow(test_data_dir, helpers, monkeypatch, cwd):
196198
assert json_entities["sort-and-change-case.ga"]["@type"] == "File"
197199
# add
198200
wf_path = crate_dir / "sort-and-change-case.ga"
199-
args = ["add", "workflow"]
201+
args = ["add", "workflow", "-P", "name=foo", "-P", "description=foo bar"]
200202
if cwd:
201203
monkeypatch.chdir(str(crate_dir))
202204
wf_path = wf_path.relative_to(crate_dir)
@@ -212,6 +214,8 @@ def test_cli_add_workflow(test_data_dir, helpers, monkeypatch, cwd):
212214
lang_id = f"https://w3id.org/workflowhub/workflow-ro-crate#{lang}"
213215
assert lang_id in json_entities
214216
assert json_entities["sort-and-change-case.ga"]["programmingLanguage"]["@id"] == lang_id
217+
assert json_entities["sort-and-change-case.ga"]["name"] == "foo"
218+
assert json_entities["sort-and-change-case.ga"]["description"] == "foo bar"
215219

216220

217221
@pytest.mark.parametrize("cwd", [False, True])
@@ -228,20 +232,27 @@ def test_cli_add_test_metadata(test_data_dir, helpers, monkeypatch, cwd):
228232
wf_path = crate_dir / "sort-and-change-case.ga"
229233
assert runner.invoke(cli, ["add", "workflow", "-c", str(crate_dir), "-l", "galaxy", str(wf_path)]).exit_code == 0
230234
# add test suite
231-
result = runner.invoke(cli, ["add", "test-suite", "-c", str(crate_dir)])
235+
result = runner.invoke(cli, ["add", "test-suite", "-c", str(crate_dir),
236+
"-P", "name=foo", "-P", "description=foo bar"])
232237
assert result.exit_code == 0
233238
suite_id = result.output.strip()
234239
json_entities = helpers.read_json_entities(crate_dir)
235240
assert suite_id in json_entities
241+
assert json_entities[suite_id]["name"] == "foo"
242+
assert json_entities[suite_id]["description"] == "foo bar"
236243
# add test instance
237-
result = runner.invoke(cli, ["add", "test-instance", "-c", str(crate_dir), suite_id, "http://example.com", "-r", "jobs"])
244+
result = runner.invoke(cli, ["add", "test-instance", "-c", str(crate_dir),
245+
suite_id, "http://example.com", "-r", "jobs",
246+
"-P", "name=foo", "-P", "description=foo bar"])
238247
assert result.exit_code == 0
239248
instance_id = result.output.strip()
240249
json_entities = helpers.read_json_entities(crate_dir)
241250
assert instance_id in json_entities
251+
assert json_entities[instance_id]["name"] == "foo"
252+
assert json_entities[instance_id]["description"] == "foo bar"
242253
# add test definition
243254
def_path = crate_dir / def_id
244-
args = ["add", "test-definition"]
255+
args = ["add", "test-definition", "-P", "name=foo", "-P", "description=foo bar"]
245256
if cwd:
246257
monkeypatch.chdir(str(crate_dir))
247258
def_path = def_path.relative_to(crate_dir)
@@ -253,6 +264,8 @@ def test_cli_add_test_metadata(test_data_dir, helpers, monkeypatch, cwd):
253264
json_entities = helpers.read_json_entities(crate_dir)
254265
assert def_id in json_entities
255266
assert set(json_entities[def_id]["@type"]) == {"File", "TestDefinition"}
267+
assert json_entities[def_id]["name"] == "foo"
268+
assert json_entities[def_id]["description"] == "foo bar"
256269
# check extra terms
257270
metadata_path = crate_dir / helpers.METADATA_FILE_NAME
258271
with open(metadata_path, "rt") as f:

0 commit comments

Comments
 (0)