Repository: fair-workflows/nanopub

Issue:
Allow pubinfo to be optionally attributed to a different id than in profile
```Nanotate``` tool has an unusual requirement (with regards to the normal way of signing nanopubs) in which it will sign with its own keys, but still wishes to attribute the publication to other ORCIDs. At present this is not possible, as only the provenance ```wasAttributedTo``` default can be overridden. Add an option to allow the same to be done for pubinfo. By default, this should keep the present behaviour (use id in profile).

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65a94fe..e2fcc2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.2.3] - 2021-02-05
+
+### Added
+* Added new `publication_attributed_to` argument to `Publication.from_assertion()`. Allows the `pubinfo` attribution to be manually set if desired.
+
 ## [1.2.2] - 2021-01-29
 
 ### Fixed
diff --git a/CITATION.cff b/CITATION.cff
index e8a9c4d..ce0dc70 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -25,5 +25,5 @@ keywords:
 license: "Apache-2.0"
 message: "If you use this software, please cite it using these metadata."
 title: "nanopub: A python library for searching, publishing and modifying nanopublications"
-version: "1.2.2"
-date-released: "2021-01-29"
+version: "1.2.3"
+date-released: "2021-02-05"
diff --git a/nanopub/_version.py b/nanopub/_version.py
index bc86c94..10aa336 100644
--- a/nanopub/_version.py
+++ b/nanopub/_version.py
@@ -1,1 +1,1 @@
-__version__ = "1.2.2"
+__version__ = "1.2.3"
diff --git a/nanopub/publication.py b/nanopub/publication.py
index 0117b47..2375e9f 100644
--- a/nanopub/publication.py
+++ b/nanopub/publication.py
@@ -79,6 +79,7 @@ class Publication:
     @staticmethod
     def _validate_from_assertion_arguments(
             introduces_concept: rdflib.term.BNode, derived_from, assertion_attributed_to,
+            publication_attributed_to,
             attribute_assertion_to_profile: bool, provenance_rdf: rdflib.Graph,
             pubinfo_rdf: rdflib.Graph):
         """
@@ -102,28 +103,36 @@ class Publication:
         if provenance_rdf:
             if derived_from and (None, namespaces.PROV.wasDerivedFrom, None) in provenance_rdf:
                 raise ValueError('The provenance_rdf that you passed already contains the '
-                                 'prov:wasDerivedFrom predicate, so you can not also use the '
+                                 'prov:wasDerivedFrom predicate, so you cannot also use the '
                                  'derived_from argument')
             if (assertion_attributed_to
                     and (None, namespaces.PROV.wasAttributedTo, None) in provenance_rdf):
                 raise ValueError('The provenance_rdf that you passed already contains the '
-                                 'prov:wasAttributedTo predicate, so you can not also use the '
+                                 'prov:wasAttributedTo predicate, so you cannot also use the '
                                  'assertion_attributed_to argument')
             if (attribute_assertion_to_profile
                     and (None, namespaces.PROV.wasAttributedTo, None) in provenance_rdf):
                 raise ValueError('The provenance_rdf that you passed already contains the '
-                                 'prov:wasAttributedTo predicate, so you can not also use the '
+                                 'prov:wasAttributedTo predicate, so you cannot also use the '
                                  'attribute_assertion_to_profile argument')
         if pubinfo_rdf:
             if introduces_concept and (None, namespaces.NPX.introduces, None) in pubinfo_rdf:
                 raise ValueError('The pubinfo_rdf that you passed already contains the '
-                                 'npx:introduces predicate, so you can not also use the '
+                                 'npx:introduces predicate, so you cannot also use the '
                                  'introduces_concept argument')
+            if (None, namespaces.PROV.wasAttributedTo, None) in pubinfo_rdf:
+                raise ValueError('The pubinfo_rdf that you passed should not contain the '
+                                 'prov:wasAttributedTo predicate. If you wish to change '
+                                 'who the publication is attributed to, please use the '
+                                 'publication_attributed_to argument instead. By default '
+                                 'this is the ORCID set in your profile, but you can set '
+                                 'it to another URI if desired.')
 
     @classmethod
     def from_assertion(cls, assertion_rdf: rdflib.Graph,
                        introduces_concept: rdflib.term.BNode = None,
                        derived_from=None, assertion_attributed_to=None,
+                       publication_attributed_to=None,
                        attribute_assertion_to_profile: bool = False,
                        provenance_rdf: rdflib.Graph = None,
                        pubinfo_rdf: rdflib.Graph = None
@@ -147,6 +156,9 @@ class Publication:
                 If a list of URIs is passed, a provenance triple will be generated for each.
             assertion_attributed_to (rdflib.URIRef or str): the provenance graph will note that
                 this nanopub's assertion prov:wasAttributedTo the given URI.
+            publication_attributed_to (rdflib.URIRef or str): the pubInfo graph will note that
+                this nanopub itself prov:wasAttributedTo the given URI. If 'None' then this
+                defaults to using the ORCID id provided in the user's profile.
             attribute_assertion_to_profile (bool): Attribute the assertion to the ORCID iD in the
                 profile
             provenance_rdf (rdflib.Graph): RDF triples to be added to provenance graph of the
@@ -159,6 +171,7 @@ class Publication:
         """
         cls._validate_from_assertion_arguments(introduces_concept, derived_from,
                                                assertion_attributed_to,
+                                               publication_attributed_to,
                                                attribute_assertion_to_profile, provenance_rdf,
                                                pubinfo_rdf)
         if attribute_assertion_to_profile:
@@ -208,10 +221,7 @@ class Publication:
         if introduces_concept:
             cls._handle_introduces_concept(introduces_concept, pubinfo)
 
-        # Always attribute the nanopublication (not the assertion) to the ORCID iD in user profile
-        pubinfo.add((DUMMY_NAMESPACE[''],
-                     namespaces.PROV.wasAttributedTo,
-                     rdflib.URIRef(profile.get_orcid_id())))
+        cls._handle_publication_attributed_to(publication_attributed_to, pubinfo)
 
         return cls(rdf=main_graph)
 
@@ -223,6 +233,17 @@ class Publication:
                         namespaces.PROV.wasAttributedTo,
                         assertion_attributed_to))
 
+    @staticmethod
+    def _handle_publication_attributed_to(publication_attributed_to, pubinfo):
+        """Handler for `from_assertion` method."""
+        if publication_attributed_to is None:
+            publication_attributed_to = rdflib.URIRef(profile.get_orcid_id())
+        else:
+            publication_attributed_to = rdflib.URIRef(publication_attributed_to)
+        pubinfo.add((DUMMY_NAMESPACE[''],
+                     namespaces.PROV.wasAttributedTo,
+                     publication_attributed_to))
+
     @staticmethod
     def _handle_derived_from(derived_from, provenance):
         """Handler for `from_assertion` method."""


Test patch:
diff --git a/tests/test_publication.py b/tests/test_publication.py
index c50d7a0..99d4790 100644
--- a/tests/test_publication.py
+++ b/tests/test_publication.py
@@ -72,6 +72,12 @@ class TestPublication:
                                                  pubinfo_rdf=self.test_rdf)
         assert self.test_triple in publication.pubinfo
 
+    def test_from_assertion_publication_attributed_to_predicate(self):
+        exampleorcid = rdflib.URIRef('exampleorcid')
+        publication = Publication.from_assertion(assertion_rdf=self.test_rdf,
+                                                 publication_attributed_to=exampleorcid)
+        assert (None, namespaces.PROV.wasAttributedTo, exampleorcid) in publication.pubinfo
+
     def test_from_assertion_double_derived_from_predicate(self):
         triple = (rdflib.term.BNode(''), namespaces.PROV.wasDerivedFrom, rdflib.URIRef('example'))
         provenance_rdf = rdflib.Graph()