Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Support for adding arbitrary properties #172

Merged
merged 3 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ The command acts on the current directory, unless the `-c` option is specified.

### Adding items to the crate

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:
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:

```console
$ rocrate add --help
Expand All @@ -320,6 +320,8 @@ Options:
--help Show this message and exit.

Commands:
dataset
file
test-definition
test-instance
test-suite
Expand Down Expand Up @@ -372,6 +374,29 @@ rocrate add test-instance test1 http://example.com -r jobs -i test1_1
rocrate add test-definition test1 test/test1/sort-and-change-case-test.yml -e planemo -v '>=0.70'
```

To add files or directories after crate initialization:

```bash
cp ../sample_file.txt .
rocrate add file sample_file.txt -P name=sample -P description="Sample file"
cp -r ../test_add_dir .
rocrate add dataset test_add_dir
```

The above example also shows how to set arbitrary properties for the entity with `-P`. This is supported by most `rocrate add` subcommands.

```console
$ rocrate add workflow --help
Usage: rocrate add workflow [OPTIONS] PATH

Options:
-l, --language [cwl|galaxy|knime|nextflow|snakemake|compss|autosubmit]
-c, --crate-dir PATH
-P, --property KEY=VALUE
--help Show this message and exit.
```


## License

* Copyright 2019-2024 The University of Manchester, UK
Expand Down
45 changes: 33 additions & 12 deletions rocrate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,20 @@ def convert(self, value, param, ctx):
self.fail(f"{value!r} is not splittable", param, ctx)


class KeyValueParamType(click.ParamType):
name = "key_value"

def convert(self, value, param, ctx):
try:
return tuple(value.split("=", 1)) if value else ()
except AttributeError:
self.fail(f"{value!r} is not splittable", param, ctx)


CSV = CSVParamType()
KeyValue = KeyValueParamType()
OPTION_CRATE_PATH = click.option('-c', '--crate-dir', type=click.Path(), default=os.getcwd)
OPTION_PROPS = click.option('-P', '--property', type=KeyValue, multiple=True, metavar="KEY=VALUE")


@click.group()
Expand All @@ -72,38 +84,41 @@ def add():
@add.command()
@click.argument('path', type=click.Path(exists=True, dir_okay=False))
@OPTION_CRATE_PATH
def file(crate_dir, path):
@OPTION_PROPS
def file(crate_dir, path, property):
crate = ROCrate(crate_dir, init=False, gen_preview=False)
source = Path(path).resolve(strict=True)
try:
dest_path = source.relative_to(crate_dir)
except ValueError:
# For now, only support adding an existing file to the metadata
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
crate.add_file(source, dest_path)
crate.add_file(source, dest_path, properties=dict(property))
crate.metadata.write(crate_dir)


@add.command()
@click.argument('path', type=click.Path(exists=True, file_okay=False))
@OPTION_CRATE_PATH
def dataset(crate_dir, path):
@OPTION_PROPS
def dataset(crate_dir, path, property):
crate = ROCrate(crate_dir, init=False, gen_preview=False)
source = Path(path).resolve(strict=True)
try:
dest_path = source.relative_to(crate_dir)
except ValueError:
# For now, only support adding an existing directory to the metadata
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
crate.add_dataset(source, dest_path)
crate.add_dataset(source, dest_path, properties=dict(property))
crate.metadata.write(crate_dir)


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


Expand All @@ -121,9 +136,13 @@ def workflow(crate_dir, path, language):
@click.option('-n', '--name')
@click.option('-m', '--main-entity')
@OPTION_CRATE_PATH
def suite(crate_dir, identifier, name, main_entity):
@OPTION_PROPS
def suite(crate_dir, identifier, name, main_entity, property):
crate = ROCrate(crate_dir, init=False, gen_preview=False)
suite = crate.add_test_suite(identifier=add_hash(identifier), name=name, main_entity=main_entity)
suite = crate.add_test_suite(
identifier=add_hash(identifier), name=name, main_entity=main_entity,
properties=dict(property)
)
crate.metadata.write(crate_dir)
print(suite.id)

Expand All @@ -136,11 +155,12 @@ def suite(crate_dir, identifier, name, main_entity):
@click.option('-i', '--identifier')
@click.option('-n', '--name')
@OPTION_CRATE_PATH
def instance(crate_dir, suite, url, resource, service, identifier, name):
@OPTION_PROPS
def instance(crate_dir, suite, url, resource, service, identifier, name, property):
crate = ROCrate(crate_dir, init=False, gen_preview=False)
instance_ = crate.add_test_instance(
add_hash(suite), url, resource=resource, service=service,
identifier=add_hash(identifier), name=name
identifier=add_hash(identifier), name=name, properties=dict(property)
)
crate.metadata.write(crate_dir)
print(instance_.id)
Expand All @@ -152,7 +172,8 @@ def instance(crate_dir, suite, url, resource, service, identifier, name):
@click.option('-e', '--engine', type=click.Choice(ENGINE_CHOICES), default="planemo")
@click.option('-v', '--engine-version')
@OPTION_CRATE_PATH
def definition(crate_dir, suite, path, engine, engine_version):
@OPTION_PROPS
def definition(crate_dir, suite, path, engine, engine_version, property):
crate = ROCrate(crate_dir, init=False, gen_preview=False)
source = Path(path).resolve(strict=True)
try:
Expand All @@ -162,7 +183,7 @@ def definition(crate_dir, suite, path, engine, engine_version):
raise ValueError(f"{source} is not in the crate dir {crate_dir}")
crate.add_test_definition(
add_hash(suite), source=source, dest_path=dest_path, engine=engine,
engine_version=engine_version
engine_version=engine_version, properties=dict(property)
)
crate.metadata.write(crate_dir)

Expand Down
14 changes: 8 additions & 6 deletions rocrate/rocrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,23 +497,24 @@ def add_workflow(
workflow.subjectOf = cwl_workflow
return workflow

def add_test_suite(self, identifier=None, name=None, main_entity=None):
def add_test_suite(self, identifier=None, name=None, main_entity=None, properties=None):
test_ref_prop = "mentions"
if not main_entity:
main_entity = self.mainEntity
if not main_entity:
test_ref_prop = "about"
suite = self.add(TestSuite(self, identifier))
suite.name = name or suite.id.lstrip("#")
suite = self.add(TestSuite(self, identifier, properties=properties))
if not properties or "name" not in properties:
suite.name = name or suite.id.lstrip("#")
if main_entity:
suite["mainEntity"] = main_entity
self.root_dataset.append_to(test_ref_prop, suite)
self.metadata.extra_terms.update(TESTING_EXTRA_TERMS)
return suite

def add_test_instance(self, suite, url, resource="", service="jenkins", identifier=None, name=None):
def add_test_instance(self, suite, url, resource="", service="jenkins", identifier=None, name=None, properties=None):
suite = self.__validate_suite(suite)
instance = self.add(TestInstance(self, identifier))
instance = self.add(TestInstance(self, identifier, properties=properties))
instance.url = url
instance.resource = resource
if isinstance(service, TestService):
Expand All @@ -522,7 +523,8 @@ def add_test_instance(self, suite, url, resource="", service="jenkins", identifi
service = get_service(self, service)
self.add(service)
instance.service = service
instance.name = name or instance.id.lstrip("#")
if not properties or "name" not in properties:
instance.name = name or instance.id.lstrip("#")
suite.append_to("instance", instance)
self.metadata.extra_terms.update(TESTING_EXTRA_TERMS)
return instance
Expand Down
29 changes: 21 additions & 8 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,19 @@ def test_cli_add_file(tmpdir, test_data_dir, helpers, monkeypatch, cwd):
# add
shutil.copy(test_data_dir / "sample_file.txt", crate_dir)
file_path = crate_dir / "sample_file.txt"
args = ["add", "file"]
args = ["add", "file", str(file_path), "-P", "name=foo", "-P", "description=foo bar"]
if cwd:
monkeypatch.chdir(str(crate_dir))
file_path = file_path.relative_to(crate_dir)
else:
args.extend(["-c", str(crate_dir)])
args.append(str(file_path))
result = runner.invoke(cli, args)
assert result.exit_code == 0
json_entities = helpers.read_json_entities(crate_dir)
assert "sample_file.txt" in json_entities
assert json_entities["sample_file.txt"]["@type"] == "File"
assert json_entities["sample_file.txt"]["name"] == "foo"
assert json_entities["sample_file.txt"]["description"] == "foo bar"


@pytest.mark.parametrize("cwd", [False, True])
Expand All @@ -171,18 +172,19 @@ def test_cli_add_dataset(tmpdir, test_data_dir, helpers, monkeypatch, cwd):
# add
dataset_path = crate_dir / "test_add_dir"
shutil.copytree(test_data_dir / "test_add_dir", dataset_path)
args = ["add", "dataset"]
args = ["add", "dataset", str(dataset_path), "-P", "name=foo", "-P", "description=foo bar"]
if cwd:
monkeypatch.chdir(str(crate_dir))
dataset_path = dataset_path.relative_to(crate_dir)
else:
args.extend(["-c", str(crate_dir)])
args.append(str(dataset_path))
result = runner.invoke(cli, args)
assert result.exit_code == 0
json_entities = helpers.read_json_entities(crate_dir)
assert "test_add_dir/" in json_entities
assert json_entities["test_add_dir/"]["@type"] == "Dataset"
assert json_entities["test_add_dir/"]["name"] == "foo"
assert json_entities["test_add_dir/"]["description"] == "foo bar"


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


@pytest.mark.parametrize("cwd", [False, True])
Expand All @@ -228,20 +232,27 @@ def test_cli_add_test_metadata(test_data_dir, helpers, monkeypatch, cwd):
wf_path = crate_dir / "sort-and-change-case.ga"
assert runner.invoke(cli, ["add", "workflow", "-c", str(crate_dir), "-l", "galaxy", str(wf_path)]).exit_code == 0
# add test suite
result = runner.invoke(cli, ["add", "test-suite", "-c", str(crate_dir)])
result = runner.invoke(cli, ["add", "test-suite", "-c", str(crate_dir),
"-P", "name=foo", "-P", "description=foo bar"])
assert result.exit_code == 0
suite_id = result.output.strip()
json_entities = helpers.read_json_entities(crate_dir)
assert suite_id in json_entities
assert json_entities[suite_id]["name"] == "foo"
assert json_entities[suite_id]["description"] == "foo bar"
# add test instance
result = runner.invoke(cli, ["add", "test-instance", "-c", str(crate_dir), suite_id, "http://example.com", "-r", "jobs"])
result = runner.invoke(cli, ["add", "test-instance", "-c", str(crate_dir),
suite_id, "http://example.com", "-r", "jobs",
"-P", "name=foo", "-P", "description=foo bar"])
assert result.exit_code == 0
instance_id = result.output.strip()
json_entities = helpers.read_json_entities(crate_dir)
assert instance_id in json_entities
assert json_entities[instance_id]["name"] == "foo"
assert json_entities[instance_id]["description"] == "foo bar"
# add test definition
def_path = crate_dir / def_id
args = ["add", "test-definition"]
args = ["add", "test-definition", "-P", "name=foo", "-P", "description=foo bar"]
if cwd:
monkeypatch.chdir(str(crate_dir))
def_path = def_path.relative_to(crate_dir)
Expand All @@ -253,6 +264,8 @@ def test_cli_add_test_metadata(test_data_dir, helpers, monkeypatch, cwd):
json_entities = helpers.read_json_entities(crate_dir)
assert def_id in json_entities
assert set(json_entities[def_id]["@type"]) == {"File", "TestDefinition"}
assert json_entities[def_id]["name"] == "foo"
assert json_entities[def_id]["description"] == "foo bar"
# check extra terms
metadata_path = crate_dir / helpers.METADATA_FILE_NAME
with open(metadata_path, "rt") as f:
Expand Down
Loading