Repository: ResearchObject/ro-crate-py
Pull request #105 (2022-01-20 11:46:45)

Workflow RO-Crate 1.0 compliance
#100 Added initial support for [Workflow RO-Crate 1.0](https://about.workflowhub.eu/Workflow-RO-Crate/1.0/), mostly for what concerns the reading side. This PR makes additional changes to make the library compliant with the profile when generating crates.


Patch:
diff --git a/rocrate/model/computationalworkflow.py b/rocrate/model/computationalworkflow.py
index 6fd824b..720fa7a 100644
--- a/rocrate/model/computationalworkflow.py
+++ b/rocrate/model/computationalworkflow.py
@@ -59,6 +59,14 @@ class ComputationalWorkflow(File):
         self["subjectOf"] = subjectOf
 
 
+class WorkflowDescription(ComputationalWorkflow):
+    """\
+    Abstract CWL description of the main workflow.
+    """
+    TYPES = ["File", "SoftwareSourceCode", "HowTo"]
+
+
+# Legacy
 class Workflow(ComputationalWorkflow):
 
     TYPES = ["File", "SoftwareSourceCode", "Workflow"]
diff --git a/rocrate/model/computerlanguage.py b/rocrate/model/computerlanguage.py
index 5fbb1e6..3a5491c 100644
--- a/rocrate/model/computerlanguage.py
+++ b/rocrate/model/computerlanguage.py
@@ -74,12 +74,16 @@ class ComputerLanguage(ContextEntity):
 CWL_DEFAULT_VERSION = "1.2"
 # https://github.com/galaxyproject/gxformat2 has some info on gxformat2 versions
 # version can probably be simply ignored for "native" *.ga workflows
-GALAXY_DEFAULT_VERSION = "v19_09"
+GALAXY_DEFAULT_VERSION = "21.09"
+KNIME_DEFAULT_VERSION = "4.5.0"
+NEXTFLOW_DEFAULT_VERSION = "21.10"
+SNAKEMAKE_DEFAULT_VERSION = "6.13"
 COMPSS_DEFAULT_VERSION = "2.10"
 
 
 def cwl(crate, version=CWL_DEFAULT_VERSION):
-    return ComputerLanguage(crate, identifier="#cwl", properties={
+    id_ = "https://w3id.org/workflowhub/workflow-ro-crate#cwl"
+    return ComputerLanguage(crate, identifier=id_, properties={
         "name": "Common Workflow Language",
         "alternateName": "CWL",
         "identifier": {
@@ -93,7 +97,8 @@ def cwl(crate, version=CWL_DEFAULT_VERSION):
 
 
 def galaxy(crate, version=GALAXY_DEFAULT_VERSION):
-    return ComputerLanguage(crate, identifier="#galaxy", properties={
+    id_ = "https://w3id.org/workflowhub/workflow-ro-crate#galaxy"
+    return ComputerLanguage(crate, identifier=id_, properties={
         "name": "Galaxy",
         "identifier": {
             "@id": "https://galaxyproject.org/"
@@ -105,6 +110,48 @@ def galaxy(crate, version=GALAXY_DEFAULT_VERSION):
     })
 
 
+def knime(crate, version=KNIME_DEFAULT_VERSION):
+    id_ = "https://w3id.org/workflowhub/workflow-ro-crate#knime"
+    return ComputerLanguage(crate, identifier=id_, properties={
+        "name": "KNIME",
+        "identifier": {
+            "@id": "https://www.knime.com/"
+        },
+        "url": {
+            "@id": "https://www.knime.com/"
+        },
+        "version": version
+    })
+
+
+def nextflow(crate, version=NEXTFLOW_DEFAULT_VERSION):
+    id_ = "https://w3id.org/workflowhub/workflow-ro-crate#nextflow"
+    return ComputerLanguage(crate, identifier=id_, properties={
+        "name": "Nextflow",
+        "identifier": {
+            "@id": "https://www.nextflow.io/"
+        },
+        "url": {
+            "@id": "https://www.nextflow.io/"
+        },
+        "version": version
+    })
+
+
+def snakemake(crate, version=SNAKEMAKE_DEFAULT_VERSION):
+    id_ = "https://w3id.org/workflowhub/workflow-ro-crate#snakemake"
+    return ComputerLanguage(crate, identifier=id_, properties={
+        "name": "Snakemake",
+        "identifier": {
+            "@id": "https://doi.org/10.1093/bioinformatics/bts480"
+        },
+        "url": {
+            "@id": "https://snakemake.readthedocs.io"
+        },
+        "version": version
+    })
+
+
 def compss(crate, version=COMPSS_DEFAULT_VERSION):
     return ComputerLanguage(crate, identifier="#compss", properties={
         "name": "COMPSs Programming Model",
@@ -118,6 +165,9 @@ def compss(crate, version=COMPSS_DEFAULT_VERSION):
 LANG_MAP = {
     "cwl": cwl,
     "galaxy": galaxy,
+    "knime": knime,
+    "nextflow": nextflow,
+    "snakemake": snakemake,
     "compss": compss,
 }
 
diff --git a/rocrate/model/metadata.py b/rocrate/model/metadata.py
index f71d319..d0ac159 100644
--- a/rocrate/model/metadata.py
+++ b/rocrate/model/metadata.py
@@ -24,6 +24,9 @@ from .file import File
 from .dataset import Dataset
 
 
+WORKFLOW_PROFILE = "https://w3id.org/workflowhub/workflow-ro-crate/1.0"
+
+
 class Metadata(File):
     """\
     RO-Crate metadata file.
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index aa7e03a..8d5befc 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -37,10 +37,10 @@ from .model.data_entity import DataEntity
 from .model.file_or_dir import FileOrDir
 from .model.file import File
 from .model.dataset import Dataset
-from .model.metadata import Metadata, LegacyMetadata, TESTING_EXTRA_TERMS, metadata_class
+from .model.metadata import WORKFLOW_PROFILE, Metadata, LegacyMetadata, TESTING_EXTRA_TERMS, metadata_class
 from .model.preview import Preview
 from .model.testdefinition import TestDefinition
-from .model.computationalworkflow import ComputationalWorkflow, galaxy_to_abstract_cwl
+from .model.computationalworkflow import ComputationalWorkflow, WorkflowDescription, galaxy_to_abstract_cwl
 from .model.computerlanguage import ComputerLanguage, get_lang
 from .model.testinstance import TestInstance
 from .model.testservice import TestService, get_service
@@ -491,9 +491,9 @@ class ROCrate():
 
     def add_workflow(
             self, source=None, dest_path=None, fetch_remote=False, validate_url=True, properties=None,
-            main=False, lang="cwl", lang_version=None, gen_cwl=False
+            main=False, lang="cwl", lang_version=None, gen_cwl=False, cls=ComputationalWorkflow
     ):
-        workflow = self.add(ComputationalWorkflow(
+        workflow = self.add(cls(
             self, source=source, dest_path=dest_path, fetch_remote=fetch_remote,
             validate_url=validate_url, properties=properties
         ))
@@ -503,17 +503,21 @@ class ROCrate():
             kwargs = {"version": lang_version} if lang_version else {}
             lang = get_lang(self, lang, **kwargs)
             self.add(lang)
+        lang_str = lang.id.rsplit("#", 1)[1]
         workflow.lang = lang
         if main:
             self.mainEntity = workflow
-        if gen_cwl and lang.id != "#cwl":
-            if lang.id != "#galaxy":
+            profiles = set(_.rstrip("/") for _ in get_norm_value(self.metadata, "conformsTo"))
+            profiles.add(WORKFLOW_PROFILE)
+            self.metadata["conformsTo"] = [{"@id": _} for _ in sorted(profiles)]
+        if gen_cwl and lang_str != "cwl":
+            if lang_str != "galaxy":
                 raise ValueError(f"conversion from {lang.name} to abstract CWL not supported")
             cwl_source = galaxy_to_abstract_cwl(source)
             cwl_dest_path = Path(source).with_suffix(".cwl").name
             cwl_workflow = self.add_workflow(
                 source=cwl_source, dest_path=cwl_dest_path, fetch_remote=fetch_remote, properties=properties,
-                main=False, lang="cwl", gen_cwl=False
+                main=False, lang="cwl", gen_cwl=False, cls=WorkflowDescription
             )
             workflow.subjectOf = cwl_workflow
         return workflow


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index dfc08ab..1e4e1c1 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -20,6 +20,7 @@ import pathlib
 import shutil
 
 import pytest
+from rocrate.utils import get_norm_value
 
 
 THIS_DIR = pathlib.Path(__file__).absolute().parent
@@ -33,9 +34,11 @@ class Helpers:
 
     PROFILE = f"{BASE_URL}/{VERSION}"
     LEGACY_PROFILE = f"{BASE_URL}/{LEGACY_VERSION}"
+    WORKFLOW_PROFILE = "https://w3id.org/workflowhub/workflow-ro-crate/1.0"
     METADATA_FILE_NAME = 'ro-crate-metadata.json'
     LEGACY_METADATA_FILE_NAME = 'ro-crate-metadata.jsonld'
     WORKFLOW_TYPES = {"File", "SoftwareSourceCode", "ComputationalWorkflow"}
+    WORKFLOW_DESC_TYPES = {"File", "SoftwareSourceCode", "HowTo"}
     LEGACY_WORKFLOW_TYPES = {"File", "SoftwareSourceCode", "Workflow"}
     PREVIEW_FILE_NAME = "ro-crate-preview.html"
 
@@ -54,7 +57,7 @@ class Helpers:
         assert cls.METADATA_FILE_NAME in json_entities
         metadata = json_entities[cls.METADATA_FILE_NAME]
         assert metadata["@type"] == "CreativeWork"
-        assert metadata["conformsTo"] == {"@id": cls.PROFILE}
+        assert cls.PROFILE in get_norm_value(metadata, "conformsTo")
         assert metadata["about"] == {"@id": root_id}
         if data_entity_ids:
             data_entity_ids = set(data_entity_ids)
@@ -71,6 +74,8 @@ class Helpers:
         assert isinstance(wf_entity["@type"], list)
         assert cls.WORKFLOW_TYPES.issubset(wf_entity["@type"])
         assert "programmingLanguage" in wf_entity
+        metadata = json_entities[cls.METADATA_FILE_NAME]
+        assert cls.WORKFLOW_PROFILE in get_norm_value(metadata, "conformsTo")
 
 
 @pytest.fixture
diff --git a/test/test_api.py b/test/test_api.py
index 4f21da8..f5d5229 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -19,6 +19,8 @@ from rocrate import rocrate_api as roc_api
 from rocrate.rocrate import ROCrate
 from rocrate.model.computerlanguage import CWL_DEFAULT_VERSION, GALAXY_DEFAULT_VERSION
 
+WF_CRATE = "https://w3id.org/workflowhub/workflow-ro-crate"
+
 
 def test_galaxy_wf_crate(test_data_dir, tmpdir, helpers):
     wf_id = 'test_galaxy_wf.ga'
@@ -29,12 +31,12 @@ def test_galaxy_wf_crate(test_data_dir, tmpdir, helpers):
     wf = wf_crate.dereference(wf_id)
     assert wf._default_type == "ComputationalWorkflow"
     assert wf_crate.mainEntity is wf
-    lang = wf_crate.dereference("#galaxy")
+    lang = wf_crate.dereference(f"{WF_CRATE}#galaxy")
     assert hasattr(lang, "name")
     assert lang.version == GALAXY_DEFAULT_VERSION
     assert wf.get("programmingLanguage") is lang
     assert wf.get("subjectOf") is not None
-    assert helpers.WORKFLOW_TYPES.issubset(wf["subjectOf"].type)
+    assert helpers.WORKFLOW_DESC_TYPES.issubset(wf["subjectOf"].type)
 
     out_path = tmpdir / 'ro_crate_out'
     out_path.mkdir()
@@ -45,7 +47,7 @@ def test_galaxy_wf_crate(test_data_dir, tmpdir, helpers):
     assert "subjectOf" in wf_entity
     abstract_wf_id = wf_entity["subjectOf"]["@id"]
     abstract_wf_entity = json_entities[abstract_wf_id]
-    assert helpers.WORKFLOW_TYPES.issubset(abstract_wf_entity["@type"])
+    assert helpers.WORKFLOW_DESC_TYPES.issubset(abstract_wf_entity["@type"])
 
     wf_out_path = out_path / wf_id
     assert wf_out_path.exists()
@@ -64,7 +66,7 @@ def test_cwl_wf_crate(test_data_dir, tmpdir, helpers):
 
     wf = wf_crate.dereference(wf_id)
     assert wf_crate.mainEntity is wf
-    lang = wf_crate.dereference("#cwl")
+    lang = wf_crate.dereference(f"{WF_CRATE}#cwl")
     assert hasattr(lang, "name")
     assert lang.version == CWL_DEFAULT_VERSION
     assert wf.get("programmingLanguage") is lang
@@ -95,12 +97,12 @@ def test_create_wf_include(test_data_dir, tmpdir, helpers):
 
     wf = wf_crate.dereference(wf_id)
     assert wf_crate.mainEntity is wf
-    lang = wf_crate.dereference("#galaxy")
+    lang = wf_crate.dereference(f"{WF_CRATE}#galaxy")
     assert hasattr(lang, "name")
     assert lang.version == GALAXY_DEFAULT_VERSION
     assert wf.get("programmingLanguage") is lang
     assert wf.get("subjectOf") is not None
-    assert helpers.WORKFLOW_TYPES.issubset(wf["subjectOf"].type)
+    assert helpers.WORKFLOW_DESC_TYPES.issubset(wf["subjectOf"].type)
     assert wf_crate.dereference(extra_file1.name) is not None
     assert wf_crate.dereference(extra_file2.name) is not None
 
diff --git a/test/test_cli.py b/test/test_cli.py
index e2210f0..31b701d 100644
--- a/test/test_cli.py
+++ b/test/test_cli.py
@@ -76,7 +76,7 @@ def test_cli_add_workflow(test_data_dir, helpers, monkeypatch, cwd):
         json_entities = helpers.read_json_entities(crate_dir)
         helpers.check_wf_crate(json_entities, wf_path.name)
         assert "sort-and-change-case.ga" in json_entities
-        lang_id = f"#{lang}"
+        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
 
diff --git a/test/test_read.py b/test/test_read.py
index bace34b..ba948c8 100644
--- a/test/test_read.py
+++ b/test/test_read.py
@@ -372,6 +372,10 @@ def test_generic_data_entity(tmpdir):
 
 def test_root_conformsto(tmpdir):
     # actually not a valid workflow ro-crate, but here it does not matter
+    profiles = [
+        "https://w3id.org/ro/crate/1.1",
+        "https://w3id.org/workflowhub/workflow-ro-crate/1.0",
+    ]
     metadata = {
         "@context": "https://w3id.org/ro/crate/1.1/context",
         "@graph": [
@@ -379,10 +383,7 @@ def test_root_conformsto(tmpdir):
                 "@id": "ro-crate-metadata.json",
                 "@type": "CreativeWork",
                 "about": {"@id": "./"},
-                "conformsTo": [
-                    {"@id": "https://w3id.org/ro/crate/1.1"},
-                    {"@id": "https://about.workflowhub.eu/Workflow-RO-Crate/"}
-                ]
+                "conformsTo": [{"@id": _} for _ in profiles]
             },
             {
                 "@id": "./",
@@ -395,10 +396,7 @@ def test_root_conformsto(tmpdir):
     with open(crate_dir / "ro-crate-metadata.json", "wt") as f:
         json.dump(metadata, f, indent=4)
     crate = ROCrate(crate_dir)
-    assert crate.metadata["conformsTo"] == [
-        "https://w3id.org/ro/crate/1.1",
-        "https://about.workflowhub.eu/Workflow-RO-Crate/"
-    ]
+    assert crate.metadata["conformsTo"] == profiles
 
 
 def test_multi_type_context_entity(tmpdir):