Repository: ResearchObject/ro-crate-py
Pull request #41 (2021-01-28 16:32:22)

Fix workflow crate creation
Fixes some problems with Workflow RO-Crate creation (mainly the fact that the `ComputerLanguage` entity was not being added to the crate).


Patch:
diff --git a/rocrate/model/computerlanguage.py b/rocrate/model/computerlanguage.py
new file mode 100644
index 0000000..40ba868
--- /dev/null
+++ b/rocrate/model/computerlanguage.py
@@ -0,0 +1,118 @@
+# Copyright 2019-2020 The University of Manchester, UK
+# Copyright 2020 Vlaams Instituut voor Biotechnologie (VIB), BE
+# Copyright 2020 Barcelona Supercomputing Center (BSC), ES
+# Copyright 2020 Center for Advanced Studies, Research and Development in Sardinia (CRS4), IT
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .contextentity import ContextEntity
+
+
+class ComputerLanguage(ContextEntity):
+
+    def _empty(self):
+        return {
+            "@id": self.id,
+            "@type": 'ComputerLanguage'
+        }
+
+    @property
+    def name(self):
+        return self["name"]
+
+    @name.setter
+    def name(self, name):
+        self["name"] = name
+
+    @property
+    def alternateName(self):
+        return self["alternateName"]
+
+    @alternateName.setter
+    def alternateName(self, alternateName):
+        self["alternateName"] = alternateName
+
+    @property
+    def identifier(self):
+        return self["identifier"]
+
+    @identifier.setter
+    def identifier(self, identifier):
+        self["identifier"] = identifier
+
+    @property
+    def url(self):
+        return self["url"]
+
+    @url.setter
+    def url(self, url):
+        self["url"] = url
+
+    # Not listed as a property in "https://schema.org/ComputerLanguage"
+    @property
+    def version(self):
+        return self["version"]
+
+    @version.setter
+    def version(self, version):
+        self["version"] = version
+
+
+# For workflow ro-crates. Note that
+# https://about.workflowhub.eu/Workflow-RO-Crate/ does not specify versions.
+
+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"
+
+
+def cwl(crate, version=CWL_DEFAULT_VERSION):
+    return ComputerLanguage(crate, identifier="#cwl", properties={
+        "name": "Common Workflow Language",
+        "alternateName": "CWL",
+        "identifier": {
+            "@id": f"https://w3id.org/cwl/{version}/"
+        },
+        "url": {
+            "@id": "https://www.commonwl.org/"
+        },
+        "version": version
+    })
+
+
+def galaxy(crate, version=GALAXY_DEFAULT_VERSION):
+    return ComputerLanguage(crate, identifier="#galaxy", properties={
+        "name": "Galaxy",
+        "identifier": {
+            "@id": "https://galaxyproject.org/"
+        },
+        "url": {
+            "@id": "https://galaxyproject.org/"
+        },
+        "version": version
+    })
+
+
+LANG_MAP = {
+    "cwl": cwl,
+    "galaxy": galaxy,
+}
+
+
+def get_lang(crate, name, version=None):
+    try:
+        func = LANG_MAP[name.lower()]
+    except KeyError:
+        raise ValueError(f"Unknown language: {name}")
+    return func(crate, version=version) if version else func(crate)
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index 7332bb9..1f2361c 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -317,6 +317,14 @@ class ROCrate():
     def CreativeWorkStatus(self, value):
         self.root_dataset['CreativeWorkStatus'] = value
 
+    @property
+    def mainEntity(self):
+        return self.root_dataset['mainEntity']
+
+    @mainEntity.setter
+    def mainEntity(self, value):
+        self.root_dataset['mainEntity'] = value
+
     @property
     def test_dir(self):
         rval = self.dereference("test")
@@ -339,9 +347,6 @@ class ROCrate():
     def get_entities(self):
         return self.__entity_map.values()
 
-    def set_main_entity(self, main_entity):
-        self.root_dataset['mainEntity'] = main_entity
-
     def _get_root_jsonld(self):
         self.root_dataset.properties()
 
diff --git a/rocrate/rocrate_api.py b/rocrate/rocrate_api.py
index abf51c6..d7827ca 100644
--- a/rocrate/rocrate_api.py
+++ b/rocrate/rocrate_api.py
@@ -24,11 +24,20 @@ from contextlib import redirect_stdout
 from pathlib import Path
 
 import rocrate.rocrate as roc
-from rocrate.model import entity
 from rocrate.model.workflow import Workflow
+from rocrate.model.computerlanguage import get_lang
 from galaxy2cwl import get_cwl_interface
 
 
+def galaxy_to_abstract_cwl(crate, workflow_path):
+    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".cwl") as f:
+        with redirect_stdout(f):
+            get_cwl_interface.main(['1', str(workflow_path)])
+    atexit.register(os.unlink, f.name)
+    abstract_wf_id = workflow_path.with_suffix(".cwl").name
+    return Workflow(crate, f.name, abstract_wf_id)
+
+
 def make_workflow_rocrate(workflow_path, wf_type, include_files=[],
                           fetch_remote=False, cwl=None, diagram=None):
 
@@ -65,39 +74,20 @@ def make_workflow_rocrate(workflow_path, wf_type, include_files=[],
     # diagram: an image/graphical workflow representation.
     # If a CWL/CWLAbstract file is provided, this is generated using cwltool
 
+    wf_type = wf_type.lower()
     wf_crate = roc.ROCrate()
-    wf_path = Path(workflow_path)
+    workflow_path = Path(workflow_path)
     # should this be added in a special path within the crate?
-    wf_file = Workflow(wf_crate, str(wf_path), wf_path.name)
+    wf_file = Workflow(wf_crate, str(workflow_path), workflow_path.name)
     wf_crate.add(wf_file)
-    wf_crate.set_main_entity(wf_file)
-    if wf_type == 'CWL':
-        programming_language_entity = entity.Entity(
-            wf_crate,
-            'https://www.commonwl.org/v1.1/',
-            properties={
-                "@type": ["ComputerLanguage", "SoftwareApplication"],
-                'name': 'CWL',
-                'url': 'https://www.commonwl.org/v1.1/',
-                'version': '1.1'
-            }
-        )
-    if wf_type == 'Galaxy':
-        if not cwl:
-            # create cwl_abstract
-            with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".cwl") as f:
-                with redirect_stdout(f):
-                    get_cwl_interface.main(['1', workflow_path])
-            atexit.register(os.unlink, f.name)
-            abstract_wf_id = wf_path.with_suffix(".cwl").name
-            abstract_wf_file = Workflow(wf_crate, f.name, abstract_wf_id)
-            wf_crate.add(abstract_wf_file)
-            wf_file["subjectOf"] = abstract_wf_file
-        programming_language_entity = entity.Entity(
-            wf_crate, 'https://galaxyproject.org/'
-        )
-    if programming_language_entity:
-        wf_file['programmingLanguage'] = programming_language_entity
+    wf_crate.mainEntity = wf_file
+    lang = get_lang(wf_crate, wf_type)
+    wf_crate.add(lang)
+    wf_file['programmingLanguage'] = lang
+    if wf_type == 'galaxy' and not cwl:
+        abstract_wf = galaxy_to_abstract_cwl(wf_crate, workflow_path)
+        wf_crate.add(abstract_wf)
+        wf_file["subjectOf"] = abstract_wf
 
     # if the source is a remote URL then add https://schema.org/codeRepository
     # property to it this can be checked by checking if the source is a URL


Test patch:
diff --git a/test/test_api.py b/test/test_api.py
index f570071..b23dc43 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -20,21 +20,31 @@ from rocrate.rocrate import ROCrate
 
 
 def test_galaxy_wf_crate(test_data_dir, tmpdir, helpers):
-    wf_path = test_data_dir / 'test_galaxy_wf.ga'
+    wf_id = 'test_galaxy_wf.ga'
+    wf_path = test_data_dir / wf_id
     wf_crate = roc_api.make_workflow_rocrate(wf_path, wf_type='Galaxy')
     assert isinstance(wf_crate, ROCrate)
+
+    wf = wf_crate.dereference(wf_id)
+    assert wf_crate.mainEntity is wf
+    lang = wf_crate.dereference("#galaxy")
+    assert hasattr(lang, "name")
+    assert wf["programmingLanguage"] is lang
+    assert wf["subjectOf"] is not None
+    assert helpers.WORKFLOW_TYPES.issubset(wf["subjectOf"].type)
+
     out_path = tmpdir / 'ro_crate_out'
     out_path.mkdir()
     wf_crate.write_crate(out_path)
     json_entities = helpers.read_json_entities(out_path)
-    helpers.check_wf_crate(json_entities, wf_path.name)
-    wf_entity = json_entities[wf_path.name]
+    helpers.check_wf_crate(json_entities, wf_id)
+    wf_entity = json_entities[wf_id]
     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"])
 
-    wf_out_path = out_path / wf_path.name
+    wf_out_path = out_path / wf_id
     assert wf_out_path.exists()
     with open(wf_path) as f1, open(wf_out_path) as f2:
         assert f1.read() == f2.read()
@@ -44,23 +54,33 @@ def test_galaxy_wf_crate(test_data_dir, tmpdir, helpers):
 
 
 def test_cwl_wf_crate(test_data_dir, tmpdir, helpers):
-    wf_path = test_data_dir / 'sample_cwl_wf.cwl'
+    wf_id = 'sample_cwl_wf.cwl'
+    wf_path = test_data_dir / wf_id
     wf_crate = roc_api.make_workflow_rocrate(wf_path, wf_type='CWL')
     assert isinstance(wf_crate, ROCrate)
+
+    wf = wf_crate.dereference(wf_id)
+    assert wf_crate.mainEntity is wf
+    lang = wf_crate.dereference("#cwl")
+    assert hasattr(lang, "name")
+    assert wf["programmingLanguage"] is lang
+    assert wf["subjectOf"] is None
+
     out_path = tmpdir / 'ro_crate_out'
     out_path.mkdir()
     wf_crate.write_crate(out_path)
     json_entities = helpers.read_json_entities(out_path)
-    helpers.check_wf_crate(json_entities, wf_path.name)
+    helpers.check_wf_crate(json_entities, wf_id)
 
-    wf_out_path = out_path / wf_path.name
+    wf_out_path = out_path / wf_id
     assert wf_out_path.exists()
     with open(wf_path) as f1, open(wf_out_path) as f2:
         assert f1.read() == f2.read()
 
 
 def test_create_wf_include(test_data_dir, tmpdir, helpers):
-    wf_path = test_data_dir / 'test_galaxy_wf.ga'
+    wf_id = 'test_galaxy_wf.ga'
+    wf_path = test_data_dir / wf_id
     extra_file1 = test_data_dir / 'test_file_galaxy.txt'
     extra_file2 = test_data_dir / 'test_file_galaxy2.txt'
     files_list = [extra_file1, extra_file2]
@@ -68,13 +88,24 @@ def test_create_wf_include(test_data_dir, tmpdir, helpers):
         wf_path, wf_type='Galaxy', include_files=files_list
     )
     assert isinstance(wf_crate, ROCrate)
+
+    wf = wf_crate.dereference(wf_id)
+    assert wf_crate.mainEntity is wf
+    lang = wf_crate.dereference("#galaxy")
+    assert hasattr(lang, "name")
+    assert wf["programmingLanguage"] is lang
+    assert wf["subjectOf"] is not None
+    assert helpers.WORKFLOW_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
+
     out_path = tmpdir / 'ro_crate_out'
     out_path.mkdir()
     wf_crate.write_crate(out_path)
     json_entities = helpers.read_json_entities(out_path)
-    helpers.check_wf_crate(json_entities, wf_path.name)
+    helpers.check_wf_crate(json_entities, wf_id)
 
-    wf_out_path = out_path / wf_path.name
+    wf_out_path = out_path / wf_id
     file1 = out_path / extra_file1.name
     file2 = out_path / extra_file2.name
     assert wf_out_path.exists()