diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09ba1b74..88f856b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,33 +16,33 @@ repos: name: đŸĒš Fix end of files - id: trailing-whitespace name: ✂ī¸ Trim trailing whitespaces -- repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 - hooks: - - id: pyupgrade - name: âĢ Running pyupgrade - args: - - --py3-plus - - --keep-runtime-typing -- repo: https://github.com/myint/autoflake - rev: v1.5.3 - hooks: - - id: autoflake - name: ❄ī¸ Running autoflake - args: - - --recursive - - --in-place - - --remove-all-unused-imports - - --remove-unused-variables - - --expand-star-imports - - --exclude - - __init__.py - - --remove-duplicate-keys -- repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - name: 🔄 Formatting imports with isort (python) +# - repo: https://github.com/asottile/pyupgrade +# rev: v2.37.3 +# hooks: +# - id: pyupgrade +# name: âĢ Running pyupgrade +# args: +# - --py3-plus +# - --keep-runtime-typing +# - repo: https://github.com/myint/autoflake +# rev: v1.5.3 +# hooks: +# - id: autoflake +# name: ❄ī¸ Running autoflake +# args: +# - --recursive +# - --in-place +# - --remove-all-unused-imports +# - --remove-unused-variables +# - --expand-star-imports +# - --exclude +# - __init__.py +# - --remove-duplicate-keys +# - repo: https://github.com/pycqa/isort +# rev: 5.10.1 +# hooks: +# - id: isort +# name: 🔄 Formatting imports with isort (python) ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: âŦ† [pre-commit.ci] pre-commit autoupdate diff --git a/docs/getting-started/test-server.md b/docs/getting-started/test-server.md index b8ba0814..e844850a 100644 --- a/docs/getting-started/test-server.md +++ b/docs/getting-started/test-server.md @@ -1,6 +1,6 @@ # The nanopub test server Throughout this documentation we make use of the -[nanopub test server](http://test-server.nanopubs.lod.labs.vu.nl/) +[nanopub test server](https://np.test.knowledgepixels.com/) by setting `use_test_server=True` when instantiating `NanopubConf` or `NanopubClient`: ```python from nanopub import NanopubClient, NanopubConf @@ -8,7 +8,7 @@ from nanopub import NanopubClient, NanopubConf client = NanopubClient(use_test_server=True) np_conf = NanopubConf(use_test_server=True) ``` -This will search and fetch from, and publish to the [nanopub test server](http://test-server.nanopubs.lod.labs.vu.nl/). +This will search and fetch from, and publish to the [nanopub test server](https://np.test.knowledgepixels.com/). When learning about nanopub using the testserver is a good idea, because: * You are free to experiment with publishing without polluting the production server. @@ -25,6 +25,6 @@ A manual workaround is: 1. Open [http://purl.org/np/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8](http://purl.org/np/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8) in your browser 2. Notice that the URL changed to [http://server.nanopubs.lod.labs.vu.nl/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8](http://server.nanopubs.lod.labs.vu.nl/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8). -3. Replace 'server' with 'test-server': [http://test-server.nanopubs.lod.labs.vu.nl/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8](http://test-server.nanopubs.lod.labs.vu.nl/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8). +3. Replace 'server' with 'test-server': [https://np.test.knowledgepixels.com/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8](https://np.test.knowledgepixels.com/RA71u9tYPd7ZQifE_6hXjqVim6pkweuvjoi-8ehvLvzg8). > **NB**: `NanopubClient.fetch()` does this for you if `use_test_server=True`. diff --git a/mkdocs.yml b/mkdocs.yml index c280614c..4780af4f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,13 +53,13 @@ theme: scheme: default primary: light blue toggle: - icon: material/toggle-switch-off-outline + icon: material/weather-night name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: light blue toggle: - icon: material/toggle-switch + icon: material/weather-sunny name: Switch to light mode features: - content.code.annotate @@ -86,10 +86,12 @@ plugins: default_handler: python handlers: python: - rendering: + options: show_source: true - watch: - - nanopub + +watch: + - nanopub + - docs # Styled blocks: https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types diff --git a/nanopub/__main__.py b/nanopub/__main__.py index 656c0f48..131bc29c 100755 --- a/nanopub/__main__.py +++ b/nanopub/__main__.py @@ -47,7 +47,8 @@ def profile(): p = load_profile() print(f' 👤 User profile in \033[1m{DEFAULT_PROFILE_PATH}\033[0m') print(str(p)) - except ProfileError: + except ProfileError as e: + print(e) print(f" ⚠ī¸ No profile could be loaded from {DEFAULT_PROFILE_PATH}") print(" ℹī¸ Use \033[1mnp setup\033[0m to setup your nanopub profile locally with the interactive CLI") @@ -163,7 +164,7 @@ def setup( prompt = ('đŸ“Ŧī¸ Would you like to publish your profile to the nanopub servers? ' 'This links your ORCID iD to your RSA key, thereby making all your ' 'publications linkable to you') - publish_resp = typer.prompt(prompt, type=Path, default="") + publish_resp = typer.prompt(prompt, type=str, default="") if publish_resp and publish_resp.lower().startswith("y"): publish = True else: @@ -171,13 +172,15 @@ def setup( if not keypair and not newkeys: prompt = '🔓ī¸ Provide the path to your public RSA key: ' \ - f'Leave empty for using the one in {USER_CONFIG_DIR}' - public_key = typer.prompt(prompt, type=Path, default="") + 'Leave empty for using the one in: ' + public_key = typer.prompt(prompt, type=Path, + default=DEFAULT_PUBLIC_KEY_PATH) if not public_key: keypair = None else: prompt = '🔑 Provide the path to your private RSA key: ' - private_key = typer.prompt(prompt, type=Path) + private_key = typer.prompt(prompt, type=Path, + default=DEFAULT_PRIVATE_KEY_PATH) keypair = public_key, private_key if not keypair: @@ -194,8 +197,10 @@ def setup( public_key_path, private_key = keypair # Copy the keypair to the default location - shutil.copy(public_key_path, USER_CONFIG_DIR / PUBLIC_KEY_FILE) - shutil.copy(private_key, USER_CONFIG_DIR / PRIVATE_KEY_FILE) + if not os.path.exists(DEFAULT_PUBLIC_KEY_PATH): + shutil.copy(public_key_path, USER_CONFIG_DIR / PUBLIC_KEY_FILE) + if not os.path.exists(DEFAULT_PRIVATE_KEY_PATH): + shutil.copy(private_key, USER_CONFIG_DIR / PRIVATE_KEY_FILE) print(f'🚚 Your RSA keys have been copied to {USER_CONFIG_DIR}') diff --git a/nanopub/_version.py b/nanopub/_version.py index 8c0d5d5b..159d48b8 100644 --- a/nanopub/_version.py +++ b/nanopub/_version.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" diff --git a/nanopub/client.py b/nanopub/client.py index d0c9db87..bd0a504e 100644 --- a/nanopub/client.py +++ b/nanopub/client.py @@ -1,4 +1,5 @@ -"""This module includes a client for the nanopub server. +""" +This module includes a client for the nanopub server. """ import random @@ -64,10 +65,10 @@ def find_nanopubs_with_text( retracted. Default is True, returning only publications that are not retracted. Yields: - dicts depicting matching nanopublications. - Each dict holds: 'np': the nanopublication uri, - 'date': date of creation of the nanopublication, - 'description': A description of the nanopublication (if found in RDF). + results (dict): dicts depicting matching nanopublications. + Each dict holds: 'np': the nanopublication uri, + 'date': date of creation of the nanopublication, + 'description': A description of the nanopublication (if found in RDF). """ if len(text) == 0: @@ -103,10 +104,10 @@ def find_nanopubs_with_pattern( retracted. Default is True, returning only publications that are not retracted. Yields: - dicts depicting matching nanopublications. - Each dict holds: 'np': the nanopublication uri, - 'date': date of creation of the nanopublication, - 'description': A description of the nanopublication (if found in RDF). + results (dict): dicts depicting matching nanopublications. + Each dict holds: 'np': the nanopublication uri, + 'date': date of creation of the nanopublication, + 'description': A description of the nanopublication (if found in RDF). """ params = {} @@ -145,10 +146,10 @@ def find_things( retracted. Default is True, returning only publications that are not retracted. Yields: - dicts depicting matching nanopublications. - Each dict holds: 'np': the nanopublication uri, - 'date': date of creation of the nanopublication, - 'description': A description of the nanopublication (if found in RDF). + results (dict): dicts depicting matching nanopublications. + Each dict holds: 'np': the nanopublication uri, + 'date': date of creation of the nanopublication, + 'description': A description of the nanopublication (if found in RDF). """ if searchterm == "": diff --git a/nanopub/definitions.py b/nanopub/definitions.py index 2a8ea13a..6d73034d 100644 --- a/nanopub/definitions.py +++ b/nanopub/definitions.py @@ -8,7 +8,7 @@ USER_CONFIG_DIR = Path.home() / ".nanopub" DEFAULT_PROFILE_PATH = USER_CONFIG_DIR / "profile.yml" -NANOPUB_TEST_SERVER = 'http://test-server.nanopubs.lod.labs.vu.nl/' +NANOPUB_TEST_SERVER = 'https://np.test.knowledgepixels.com/' # List of servers: https://monitor.petapico.org/.csv NANOPUB_SERVER_LIST = [ 'https://np.petapico.org/', @@ -31,6 +31,7 @@ MAX_NP_PER_INDEX = 1100 MAX_TRIPLES_PER_NANOPUB = 1200 +RSA_KEY_SIZE = 2048 NANOPUB_GRLC_URLS = [ "http://grlc.nanopubs.lod.labs.vu.nl/api/local/local/", @@ -42,4 +43,4 @@ # "https://grlc.nanopubs.knows.idlab.ugent.be/api/local/local/", # "http://grlc.np.scify.org/api/local/local/", ] -NANOPUB_TEST_GRLC_URL = "http://test-grlc.nanopubs.lod.labs.vu.nl/api/local/local/" +NANOPUB_TEST_GRLC_URL = "https://grlc.test.nps.knowledgepixels.com/api/local/local/" diff --git a/nanopub/nanopub.py b/nanopub/nanopub.py index 7803fd0e..26d17506 100644 --- a/nanopub/nanopub.py +++ b/nanopub/nanopub.py @@ -92,6 +92,7 @@ def __init__( self._assertion += assertion self._provenance += provenance self._pubinfo += pubinfo + self._bnode_count = 0 # Concatenate prefixes declarations from all provided graphs in the main graph for user_rdf in [assertion, provenance, pubinfo]: @@ -157,7 +158,7 @@ def _preformat_graph(self, g: ConjunctiveGraph) -> ConjunctiveGraph: g.bind("orcid", ORCID) g.bind("ntemplate", NTEMPLATE) g.bind("foaf", FOAF) - # g = self._replace_blank_nodes(g) + g = self._replace_blank_nodes(g) return g @@ -184,7 +185,8 @@ def sign(self) -> None: raise MalformedNanopubError(f"The nanopub have already been signed: {self.source_uri}") if self.is_valid: - signed_g = add_signature(self.rdf, self._conf.profile, self._metadata.namespace, URIRef(str(self._pubinfo.identifier))) + self._replace_blank_nodes(self._rdf) + signed_g = add_signature(self.rdf, self._conf.profile, self._metadata.namespace, self._pubinfo) self.update_from_signed(signed_g) log.info(f"Signed {self.source_uri}") else: @@ -221,7 +223,6 @@ def update(self, publish=True) -> None: None, )) self._metadata = extract_np_metadata(self._rdf) - print(self._metadata) if publish: self.publish() else: @@ -362,6 +363,8 @@ def profile(self, value): def namespace(self): return self._metadata.namespace + + @property def introduces_concept(self): concepts_introduced = list() @@ -554,28 +557,44 @@ def _validate_nanopub_arguments( "introduces_concept argument" ) - # TODO: we might to use it to convert blank nodes directly as URI here - # instead of doing it through the get_trustyuri() function - # def _replace_blank_nodes(self, rdf: ConjunctiveGraph) -> ConjunctiveGraph: - # """Replace blank nodes. - # Replace any blank nodes in the supplied RDF with a corresponding uri in the - # dummy_namespace.'Blank nodes' here refers specifically to rdflib.term.BNode objects. When - # publishing, the dummy_namespace is replaced with the URI of the actual nanopublication. - # For example, if the nanopub's URI is www.purl.org/ABC123 then the blank node will be - # replaced with a concrete URIRef of the form www.purl.org/ABC123#blanknodename where - # 'blanknodename' is the name of the rdflib.term.BNode object. - # This is to solve the problem that a user may wish to use the nanopublication to introduce - # a new concept. This new concept needs its own URI (it cannot simply be given the - # nanopublication's URI), but it should still lie within the space of the nanopub. - # Furthermore, the URI the nanopub is published to is not known ahead of time. - # """ - # for s, p, o in rdf: - # if isinstance(s, BNode): - # rdf.remove((s, p, o)) - # s = self._metadata.namespace[f"_{str(s)}"] - # rdf.add((s, p, o)) - # if isinstance(o, BNode): - # rdf.remove((s, p, o)) - # o = self._metadata.namespace[f"_{str(o)}"] - # rdf.add((s, p, o)) - # return rdf + + def _replace_blank_nodes(self, g: ConjunctiveGraph) -> ConjunctiveGraph: + """Replace blank nodes. + Replace any blank nodes in the supplied RDF with a corresponding uri in the + dummy_namespace.'Blank nodes' here refers specifically to rdflib.term.BNode objects. When + publishing, the dummy_namespace is replaced with the URI of the actual nanopublication. + For example, if the nanopub's URI is www.purl.org/ABC123 then the blank node will be + replaced with a concrete URIRef of the form www.purl.org/ABC123#blanknodename where + 'blanknodename' is the name of the rdflib.term.BNode object. + This is to solve the problem that a user may wish to use the nanopublication to introduce + a new concept. This new concept needs its own URI (it cannot simply be given the + nanopublication's URI), but it should still lie within the space of the nanopub. + Furthermore, the URI the nanopub is published to is not known ahead of time. + """ + bnode_map: dict = {} + for s, p, o, c in g.quads(): + if isinstance(s, BNode): + g.remove((s, p, o, c)) + if str(s) not in bnode_map: + if re.match(r'^[Na-zA-Z0-9]{33}$', str(s)): + # Unnamed BNode looks like N2c21867a547345d9b8a203a7c1cd7e0c + self._bnode_count += 1 + bnode_map[str(s)] = self._bnode_count + else: + bnode_map[str(s)] = str(s) + s = self._metadata.namespace[f"_{bnode_map[str(s)]}"] + g.add((s, p, o, c)) + + if isinstance(o, BNode): + g.remove((s, p, o, c)) + if str(o) not in bnode_map: + # if str(o).startswith("N") and len(str(o)) == 33: + if re.match(r'^[Na-zA-Z0-9]{33}$', str(s)): + self._bnode_count += 1 + bnode_map[str(o)] = self._bnode_count + else: + bnode_map[str(o)] = str(o) + o = self._metadata.namespace[f"_{bnode_map[str(o)]}"] + + g.add((s, p, o, c)) + return g diff --git a/nanopub/profile.py b/nanopub/profile.py index ea36c2da..713191f8 100644 --- a/nanopub/profile.py +++ b/nanopub/profile.py @@ -1,6 +1,7 @@ """ This module holds objects and functions to load a nanopub user profile. """ +import os from base64 import decodebytes from pathlib import Path from typing import Optional, Union @@ -8,7 +9,7 @@ import yatiml from Crypto.PublicKey import RSA -from nanopub.definitions import DEFAULT_PROFILE_PATH, USER_CONFIG_DIR +from nanopub.definitions import DEFAULT_PROFILE_PATH, RSA_KEY_SIZE, USER_CONFIG_DIR from nanopub.utils import log PROFILE_INSTRUCTIONS_MESSAGE = ''' @@ -62,41 +63,36 @@ def __init__( else: self._private_key = private_key - if not public_key: - log.info('Public key not provided when loading the Nanopub profile, generating it from the provided private key') - key = RSA.importKey(decodebytes(self._private_key.encode())) - self._public_key = key.publickey().export_key().decode('utf-8') - else: - if isinstance(public_key, Path): - try: - with open(public_key) as f: - self._public_key = f.read().strip() - except FileNotFoundError: - raise ProfileError( - f'Private key file {public_key} for nanopub not found.\n' - f'Maybe your nanopub profile was not set up yet or not set up ' - f'correctly. \n{PROFILE_INSTRUCTIONS_MESSAGE}' - ) - else: - self._public_key = public_key - + if not public_key and private_key: + log.info('The public key was not provided when loading the Nanopub profile, generating it from the provided private key') + key = RSA.import_key(decodebytes(self._private_key.encode())) + self._public_key = format_key(key.publickey().export_key().decode('utf-8')) + elif isinstance(public_key, Path): + try: + with open(public_key) as f: + self._public_key = f.read().strip() + except FileNotFoundError: + raise ProfileError( + f'Private key file {public_key} for nanopub not found.\n' + f'Maybe your nanopub profile was not set up yet or not set up ' + f'correctly. \n{PROFILE_INSTRUCTIONS_MESSAGE}' + ) + elif public_key: + self._public_key = public_key def generate_keys(self) -> str: """Generate private/public RSA key pair at the path specified in the profile.yml, to be used to sign nanopubs""" - key = RSA.generate(2048) + key = RSA.generate(RSA_KEY_SIZE) private_key_str = key.export_key('PEM', pkcs=8).decode('utf-8') public_key_str = key.publickey().export_key().decode('utf-8') - # Format private and public keys to remove header/footer and all newlines, as this is required by nanopub-java - private_key_str = private_key_str.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace("\n", "").strip() - public_key_str = public_key_str.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "").strip() - self._private_key = private_key_str - self._public_key = public_key_str + self._private_key = format_key(private_key_str) + self._public_key = format_key(public_key_str) log.info(f"Public/private RSA key pair has been generated for {self.orcid_id} ({self.name})") return public_key_str - def store(self, folder: Path = USER_CONFIG_DIR) -> Path: + def store(self, folder: Path = USER_CONFIG_DIR) -> str: """Stores the nanopub user profile. By default the profile is stored in `HOME_DIR/.nanopub/profile.yaml`. Args: @@ -105,16 +101,17 @@ def store(self, folder: Path = USER_CONFIG_DIR) -> Path: Returns: The path where the profile was stored. """ + folder = Path(folder) folder.mkdir(parents=True, exist_ok=True) - private_key_path = folder / "id_rsa" - public_key_path = folder / "id_rsa.pub" - profile_path = folder / "profile.yml" + private_key_path = os.path.join(folder, "id_rsa") + public_key_path = os.path.join(folder, "id_rsa.pub") + profile_path = os.path.join(folder, "profile.yml") # Store keys - if not private_key_path.exists(): + if not os.path.exists(private_key_path): with open(private_key_path, "w") as f: f.write(self.private_key + '\n') - if not public_key_path.exists(): + if not os.path.exists(public_key_path): with open(public_key_path, "w") as f: f.write(self.public_key) @@ -215,7 +212,8 @@ def load_profile(profile_path: Union[Path, str] = DEFAULT_PROFILE_PATH) -> Profi A Profile containing the data from the configuration file. Raises: - yatiml.RecognitionError: If there is an error in the file. + yatiml.RecognitionError: If there is an + error in the file. """ try: return _load_profile(Path(profile_path)) @@ -227,16 +225,15 @@ def load_profile(profile_path: Union[Path, str] = DEFAULT_PROFILE_PATH) -> Profi def generate_keyfiles(path: Path = USER_CONFIG_DIR) -> str: """Generate private/public RSA key pair at the path specified in the profile.yml, to be used to sign nanopubs""" - if not path.exists(): - path.mkdir() + if not Path(path).exists(): + Path(path).mkdir() - key = RSA.generate(2048) + key = RSA.generate(RSA_KEY_SIZE) private_key_str = key.export_key('PEM', pkcs=8).decode('utf-8') public_key_str = key.publickey().export_key().decode('utf-8') - # Format private and public keys to remove header/footer and all newlines, as this is required by nanopub-java - private_key_str = private_key_str.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace("\n", "").strip() - public_key_str = public_key_str.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "").strip() + private_key_str = format_key(private_key_str) + public_key_str = format_key(public_key_str) private_path = path / "id_rsa" public_path = path / "id_rsa.pub" @@ -250,3 +247,12 @@ def generate_keyfiles(path: Path = USER_CONFIG_DIR) -> str: public_key_file.close() log.info(f"Public/private RSA key pair has been generated in {private_path} and {public_path}") return public_key_str + + +def format_key(key: str) -> str: + """Format private and public keys to remove header/footer and all newlines, as this is required by nanopub-java""" + if key.startswith("-----BEGIN PRIVATE KEY-----"): + key = key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "") + if key.startswith("-----BEGIN PUBLIC KEY-----"): + key = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "") + return key.replace("\n", "").strip() diff --git a/nanopub/sign_utils.py b/nanopub/sign_utils.py index e665370e..9d4bccb0 100644 --- a/nanopub/sign_utils.py +++ b/nanopub/sign_utils.py @@ -4,7 +4,7 @@ from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from rdflib import BNode, ConjunctiveGraph, Literal, Namespace, URIRef +from rdflib import BNode, ConjunctiveGraph, Graph, Literal, Namespace, URIRef from nanopub.definitions import NANOPUB_SERVER_LIST, NP_PURL, NP_TEMP_PREFIX from nanopub.namespaces import NPX @@ -14,25 +14,25 @@ from nanopub.utils import MalformedNanopubError, extract_np_metadata, log -def add_signature(g: ConjunctiveGraph, profile: Profile, dummy_namespace: Namespace, pubinfo_uri: URIRef) -> ConjunctiveGraph: +def add_signature(g: ConjunctiveGraph, profile: Profile, dummy_namespace: Namespace, pubinfo_g: Graph) -> ConjunctiveGraph: """Implementation in python of the process to sign a nanopub with a RSA private key""" g.add(( dummy_namespace["sig"], NPX["hasPublicKey"], Literal(profile.public_key), - pubinfo_uri, + pubinfo_g, )) g.add(( dummy_namespace["sig"], NPX["hasAlgorithm"], Literal("RSA"), - pubinfo_uri, + pubinfo_g, )) g.add(( dummy_namespace["sig"], NPX["hasSignatureTarget"], dummy_namespace[""], - pubinfo_uri, + pubinfo_g, )) # Normalize RDF quads = RdfUtils.get_quads(g) @@ -45,7 +45,7 @@ def add_signature(g: ConjunctiveGraph, profile: Profile, dummy_namespace: Namesp # print(f"NORMED RDF STARTS\n{normed_rdf}\nNORMED RDF ENDS") # Sign the normalized RDF with the private RSA key - private_key = RSA.importKey(decodebytes(profile.private_key.encode())) + private_key = RSA.import_key(decodebytes(profile.private_key.encode())) signer = PKCS1_v1_5.new(private_key) signature_b = signer.sign(SHA256.new(normed_rdf.encode())) signature = encodebytes(signature_b).decode().replace("\n", "") @@ -56,7 +56,7 @@ def add_signature(g: ConjunctiveGraph, profile: Profile, dummy_namespace: Namesp dummy_namespace["sig"], NPX["hasSignature"], Literal(signature), - pubinfo_uri, + pubinfo_g, )) # Generate the trusty URI @@ -92,6 +92,8 @@ def replace_trusty_in_graph(trusty_artefact: str, dummy_ns: str, graph: Conjunct g = c.identifier else: raise Exception("Found a nquads without graph when replacing dummy URIs with trusty URIs. Something went wrong.") + # new_g = Graph(identifier=str(transform(g, trusty_artefact, dummy_ns, bnodemap))) + # Fails and make the nanopub empty new_g = URIRef(transform(g, trusty_artefact, dummy_ns, bnodemap)) new_s = URIRef(transform(s, trusty_artefact, dummy_ns, bnodemap)) new_p = URIRef(transform(p, trusty_artefact, dummy_ns, bnodemap)) @@ -99,8 +101,9 @@ def replace_trusty_in_graph(trusty_artefact: str, dummy_ns: str, graph: Conjunct if isinstance(o, URIRef) or isinstance(o, BNode): new_o = URIRef(transform(o, trusty_artefact, dummy_ns, bnodemap)) - graph.remove((s, p, o, g)) - graph.add((new_s, new_p, new_o, new_g)) + graph.remove((s, p, o, c)) + graph.add((new_s, new_p, new_o, new_g)) # type: ignore + return graph @@ -109,11 +112,10 @@ def publish_graph(g: ConjunctiveGraph, use_server: str = NANOPUB_SERVER_LIST[0]) """ log.info(f"Publishing to the nanopub server {use_server}") headers = {'Content-Type': 'application/trig'} - # Used by nanopub-java: {'Content-Type': 'application/x-www-form-urlencoded'} + # NOTE: nanopub-java uses {'Content-Type': 'application/x-www-form-urlencoded'} data = g.serialize(format="trig") - r = requests.post(use_server, headers=headers, data=data) + r = requests.post(use_server, headers=headers, data=data.encode('utf-8')) r.raise_for_status() - # if r.status_code == 201: return True diff --git a/nanopub/trustyuri/rdf/RdfUtils.py b/nanopub/trustyuri/rdf/RdfUtils.py index 3f03d3a5..fcce31ff 100644 --- a/nanopub/trustyuri/rdf/RdfUtils.py +++ b/nanopub/trustyuri/rdf/RdfUtils.py @@ -29,6 +29,7 @@ def get_trustyuri(resource, baseuri, hashstr, bnodemap): return str(f"{prefix}{hashstr}") return str(f"{prefix}{hashstr}#{suffix}") if isinstance(resource, BNode): + # NOTE: bnodes are replaced in nanopub.py by _replace_blank_nodes() most of the time bnode_unnamed = re.match(r'^[a-zA-Z0-9]{33}$', str(resource)) # Check if BNode in the form of N2b80343001e94f48bdee0901be566ebb # Which means it was automatically generated by rdflib: we use a number in this case diff --git a/nanopub/utils.py b/nanopub/utils.py index 1b676672..6f2ea82e 100644 --- a/nanopub/utils.py +++ b/nanopub/utils.py @@ -1,7 +1,7 @@ import logging import re from dataclasses import asdict, dataclass -from typing import Optional +from typing import Any, Optional from rdflib import ConjunctiveGraph, Namespace, URIRef @@ -58,7 +58,7 @@ def extract_np_metadata(g: ConjunctiveGraph) -> NanopubMetadata: } } """ - qres = g.query(get_np_query) + qres: Any = g.query(get_np_query) if len(qres) < 1: raise MalformedNanopubError( "\033[1mNo nanopublication\033[0m has been found in the provided RDF. " diff --git a/pyproject.toml b/pyproject.toml index 078ca401..319b41ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "nanopub" description = "Python client for Nanopublications" @@ -28,7 +32,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ - "rdflib <7.0.0,>=6.0.2", + "rdflib >=6.0.2", "requests", "typer", "yatiml", @@ -37,22 +41,21 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest >=7.1.3,<8.0.0", - "pytest-cov >=2.12.0,<4.0.0", + "pytest >=7.1.3", + "pytest-cov >=3.0.0", "coveralls", - "mypy ==0.971", - "isort >=5.0.6,<6.0.0", - "flake8 >=3.8.3,<6.0.0", - "Flake8-pyproject>=1.1.0.post0", + "mypy >=0.991", + "isort >=5.11.0", + "flake8 >=5.0.0", + "Flake8-pyproject >=1.2.2", "flaky", ] doc = [ - "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=8.2.7,<9.0.0", - "mkdocstrings[python] >=0.18.1", - "mdx-include >=1.4.1,<2.0.0", - "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", - "jinja2 ==3.0.3", + "mkdocs >=1.4.2", + "mkdocs-material >=8.2.7", + "mkdocstrings[python] >=0.19.1", + "mdx-include >=1.4.1", + "mkdocs-markdownextradata-plugin >=0.2.5", ] dev = [ "pre-commit >=2.17.0,<3.0.0", @@ -75,26 +78,22 @@ Tracker = "https://github.com/fair-workflows/nanopub/issues" Source = "https://github.com/fair-workflows/nanopub" -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - # ENVIRONMENTS AND SCRIPTS [tool.hatch.envs.default] features = [ - "test", - "doc", - "dev", + "test", + "doc", + "dev", ] post-install-commands = [ - "pre-commit install", + "pre-commit install", ] [tool.hatch.envs.default.scripts] dev = "./scripts/dev.sh" test = "./scripts/test.sh {args}" -docs = "./scripts/docs.sh" +docs = "./scripts/docs.sh {args}" format = "./scripts/format.sh" lint = "./scripts/lint.sh" @@ -134,13 +133,16 @@ ignore = [ [tool.mypy] strict = false -disallow_untyped_defs = false +implicit_reexport = true follow_imports = "normal" ignore_missing_imports = true pretty = true show_column_numbers = true -warn_no_return = false -warn_unused_ignores = true +warn_no_return = true +warn_unused_ignores = false +warn_redundant_casts = true +disallow_untyped_defs = false +no_implicit_optional = false [tool.pytest.ini_options] diff --git a/scripts/docs.sh b/scripts/docs.sh index 9adfb331..e8dafa64 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -2,4 +2,4 @@ set -e -mkdocs serve -a localhost:8001 +mkdocs serve -a localhost:8001 ${@} diff --git a/scripts/lint.sh b/scripts/lint.sh index de7b51ac..1484d6a4 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -e +set -x isort nanopub tests --check-only flake8 nanopub tests mypy nanopub -# black nanopub tests --check diff --git a/tests/java_wrapper.py b/tests/java_wrapper.py index 8e98c01e..21b3cd9c 100644 --- a/tests/java_wrapper.py +++ b/tests/java_wrapper.py @@ -38,7 +38,7 @@ def __init__(self, private_key: str = None) -> None: self.private_key = str(private_key_path) public_key_path = os.path.join(keys_dir, "id_rsa.pub") - key = RSA.importKey(decodebytes(private_key.encode())) + key = RSA.import_key(decodebytes(private_key.encode())) public_key = key.publickey().export_key().decode('utf-8').replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", "") with open(public_key_path, "w") as f: f.write(public_key) @@ -88,6 +88,20 @@ def sign(self, np: Nanopub) -> str: return source_uri + def check_trusty_with_signature(self, np: Nanopub) -> str: + tmp_dir = tempfile.mkdtemp() + np_file = os.path.join(tmp_dir, "signed.trig") + with open(np_file, "w") as f: + f.write(np.rdf.serialize(format="trig")) + np_file = str(np_file) + + cmd = f'{NANOPUB_JAVA_SCRIPT} check {np_file}' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + print(str(result.stdout)) + return "1 trusty with signature" in str(result.stdout) + + + def _get_signed_file(self, unsigned_file: str): unsigned_path = Path(unsigned_file) return str(unsigned_path.parent / f'signed.{unsigned_path.name}') diff --git a/tests/resources/many_bnodes_with_annotations.json b/tests/resources/many_bnodes_with_annotations.json new file mode 100644 index 00000000..9ab18dce --- /dev/null +++ b/tests/resources/many_bnodes_with_annotations.json @@ -0,0 +1,142 @@ +{ + "@context": { + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "biolink": "https://w3id.org/biolink/vocab/", + "infores": "https://w3id.org/biolink/infores/", + "dct": "http://purl.org/dc/terms/" + }, + "@graph": [ + { + "@id": "https://w3id.org/biolink/infores/knowledge-collaboratory", + "@type": "biolink:InformationResource", + "biolink:category": "biolink:InformationResource", + "biolink:id": "infores:knowledge-collaboratory" + }, + { + "@id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee", + "@type": "biolink:Publication", + "biolink:category": "biolink:Publication", + "biolink:id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + }, + { + "@type": "biolink:Association", + "biolink:category": "biolink:Association", + "rdf:subject": { + "@id": "http://identifiers.org/pubchem.compound/68617" + }, + "rdf:predicate": { + "@id": "https://w3id.org/biolink/vocab/treats" + }, + "rdf:object": { + "@id": "http://purl.obolibrary.org/obo/MONDO_0008187" + }, + "biolink:id": "collaboratory:PUBCHEM.COMPOUND:68617-biolink:treats-MONDO:0008187", + "biolink:aggregator_knowledge_source": { + "@id": "infores:knowledge-collaboratory" + }, + "https://w3id.org/biolink/vocab/publications": { + "@id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + } + }, + { + "@id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee", + "@type": "biolink:Publication", + "biolink:category": "biolink:Publication", + "biolink:id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + }, + { + "@type": "biolink:Association", + "biolink:category": "biolink:Association", + "rdf:subject": { + "@id": "http://identifiers.org/pubchem.compound/68617" + }, + "rdf:predicate": { + "@id": "https://w3id.org/biolink/vocab/treats" + }, + "rdf:object": { + "@id": "http://identifiers.org/umls/C2188188" + }, + "biolink:id": "collaboratory:PUBCHEM.COMPOUND:68617-biolink:treats-UMLS:C2188188", + "biolink:aggregator_knowledge_source": { + "@id": "infores:knowledge-collaboratory" + }, + "https://w3id.org/biolink/vocab/publications": { + "@id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + } + }, + { + "@id": "http://identifiers.org/pubchem.compound/68617", + "@type": "biolink:ChemicalEntity", + "biolink:id": "PUBCHEM.COMPOUND:68617", + "biolink:category": "biolink:ChemicalEntity", + "rdfs:label": "Sertraline" + }, + { + "@id": "http://purl.obolibrary.org/obo/MONDO_0008187", + "@type": "biolink:DiseaseOrPhenotypicFeature", + "biolink:id": "MONDO:0008187", + "biolink:category": "biolink:DiseaseOrPhenotypicFeature", + "rdfs:label": "panic disorder" + }, + { + "@id": "http://identifiers.org/umls/C2188188", + "@type": "biolink:DiseaseOrPhenotypicFeature", + "biolink:id": "UMLS:C2188188", + "biolink:category": "biolink:DiseaseOrPhenotypicFeature", + "rdfs:label": "agoraphobia" + } + ], + "@annotations": { + "@context": { + "tao": "http://pubannotation.org/ontology/tao.owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#" + }, + "@graph": [ + { + "@id": "http://purl.org/nanopub/temp/np#document", + "@type": "tao:document_text", + "tao:has_value": "Sertraline tablets are indicated for the treatment of panic disorder in adults, with or without agoraphobia, as defined in DSM-IV.", + "rdfs:seeAlso": { + "@id": "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + } + }, + { + "@type": "tao:text_span", + "tao:begins_at": 0, + "tao:ends_at": 10, + "tao:has_value": "Sertraline", + "tao:denotes": { + "@id": "http://identifiers.org/pubchem.compound/68617" + }, + "tao:part_of": { + "@id": "http://purl.org/nanopub/temp/np#document" + } + }, + { + "@type": "tao:text_span", + "tao:begins_at": 54, + "tao:ends_at": 68, + "tao:has_value": "panic disorder", + "tao:denotes": { + "@id": "http://purl.obolibrary.org/obo/MONDO_0008187" + }, + "tao:part_of": { + "@id": "http://purl.org/nanopub/temp/np#document" + } + }, + { + "@type": "tao:text_span", + "tao:begins_at": 96, + "tao:ends_at": 107, + "tao:has_value": "agoraphobia", + "tao:denotes": { + "@id": "http://identifiers.org/umls/C2188188" + }, + "tao:part_of": { + "@id": "http://purl.org/nanopub/temp/np#document" + } + } + ] + } +} diff --git a/tests/test_client.py b/tests/test_client.py index 674de22c..bcc40634 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,15 +1,17 @@ import pytest -from rdflib import FOAF, RDF +from rdflib import FOAF, RDF, URIRef from nanopub import NanopubClient from nanopub.definitions import TEST_RESOURCES_FILEPATH from tests.conftest import skip_if_nanopub_server_unavailable client = NanopubClient(use_test_server=True) +prod_client = NanopubClient(use_test_server=False) + +PUBKEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuZe/66Yn1/kZ+TrhkAUez7n3x/dbukNAxprC2I4n/' \ + 'by5dAgSHvnRm7zhEeDyFFHXNm2PBqj3uLOEM6m2lWDsCmTHojgLUBpvhCzvziSjAzBJ4loLaax+Nt1hSY2/' \ + 'MNB0xJKtxQz7xe8gPf6iyckD/H7Mwa9mx5ncYRzg5XUlvvQIDAQAB' -PUBKEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCC686zsZaQWthNDSZO6unvhtSkXSLT8iSY/UUwD/' \ - '7T9tabrEvFt/9UPsCsg/A4HG6xeuPtL5mVziVnzbxqi9myQOY62LBja85pYLWaZPUYakP' \ - 'HyVm9A0bRC2PUYZde+METkZ6eoqLXP26Qo5b6avPcmNnKkr5OQb7KXaeX2K2zQQIDAQAB' NANOPUB_SAMPLE_SIGNED = str(TEST_RESOURCES_FILEPATH / 'nanopub_sample_signed.trig') @@ -21,7 +23,7 @@ def test_find_nanopubs_with_text(self): """ Check that Nanopub text search is returning results for a few common search terms """ - searches = ['test', 'US'] + searches = ['citation', 'comment'] for search in searches: results = list(client.find_nanopubs_with_text(search)) @@ -32,10 +34,10 @@ def test_find_nanopubs_with_text(self): @pytest.mark.flaky(max_runs=10) @skip_if_nanopub_server_unavailable def test_find_nanopubs_with_text_pubkey(self): - results = list(client.find_nanopubs_with_text('test', pubkey=PUBKEY)) + results = list(client.find_nanopubs_with_text('comment', pubkey=PUBKEY)) assert len(results) > 0 - results = list(client.find_nanopubs_with_text('test', pubkey='wrong')) + results = list(client.find_nanopubs_with_text('comment', pubkey='wrong')) assert len(results) == 0 @pytest.mark.flaky(max_runs=10) @@ -45,7 +47,7 @@ def test_find_nanopubs_with_text_prod(self): production nanopub server """ prod_client = NanopubClient() - searches = ['test', 'US'] + searches = ['citation', "chemical"] for search in searches: results = list(prod_client.find_nanopubs_with_text(search)) assert len(results) > 0 @@ -71,8 +73,8 @@ def test_find_nanopubs_with_pattern(self): Check that Nanopub pattern search is returning results """ searches = [ - ('', RDF.type, FOAF.Person), - ('http://purl.org/np/RA8ui7ddvV25m1qdyxR4lC8q8-G0yb3SN8AC0Bu5q8Yeg', '', '') + ('', RDF.type, URIRef("http://www.w3.org/ns/oa#Annotation")), + ('https://w3id.org/np/RAj75Z7QMYNalgNiMG9IthMuj18VuJbto9sC8Jl6lp9WM#_1', '', '') ] for subj, pred, obj in searches: @@ -87,7 +89,7 @@ def test_find_nanopubs_with_pattern_pubkey(self): Check that Nanopub pattern search is returning results """ subj, pred, obj = ( - 'http://purl.org/np/RA8ui7ddvV25m1qdyxR4lC8q8-G0yb3SN8AC0Bu5q8Yeg', '', '') + 'https://w3id.org/np/RAj75Z7QMYNalgNiMG9IthMuj18VuJbto9sC8Jl6lp9WM#_1', '', '') results = list(client.find_nanopubs_with_pattern(subj=subj, pred=pred, obj=obj, pubkey=PUBKEY)) assert len(results) > 0 @@ -102,22 +104,24 @@ def test_nanopub_find_things(self): """ Check that Nanopub 'find_things' search is returning results """ - results = list(client.find_things(type='http://purl.org/net/p-plan#Plan')) + results = list(prod_client.find_things(type='http://purl.org/net/p-plan#Plan')) assert len(results) > 0 with pytest.raises(Exception): - list(client.find_things()) + list(prod_client.find_things()) with pytest.raises(Exception): - list(client.find_things(type='http://purl.org/net/p-plan#Plan', searchterm='')) + list(prod_client.find_things(type='http://purl.org/net/p-plan#Plan', searchterm='')) @pytest.mark.flaky(max_runs=10) @skip_if_nanopub_server_unavailable def test_find_things_pubkey(self): - results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', pubkey=PUBKEY)) + things_pubkey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoZmUKAHAF0CY2sKahOanR1V8wP62NOw3G0wcVLULWxqXB/gcW25bGPcA5RKoiuhT6dUbfcRXm" \ + "wLknE29h6KWfKYLtNaqdrHbjSnNC65dNmNxCNp0i6ZLZRh51mxw9IPJHZrDqQ9bcLwm9d1G1fDKasA+h1vrF3Hv1YrQsF9aW1QIDAQAB" + results = list(prod_client.find_things(type='http://purl.org/net/p-plan#Plan', pubkey=things_pubkey)) assert len(results) > 0 - results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', pubkey='wrong')) + results = list(prod_client.find_things(type='http://purl.org/net/p-plan#Plan', pubkey='wrong')) assert len(results) == 0 @pytest.mark.flaky(max_runs=10) @@ -129,43 +133,45 @@ def test_nanopub_find_things_empty_searchterm(self): with pytest.raises(Exception): client.find_things(searchterm='') - @pytest.mark.flaky(max_runs=10) - @skip_if_nanopub_server_unavailable - def test_find_things_filter_retracted(self): - filtered_results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', - filter_retracted=True)) - assert len(filtered_results) > 0 - all_results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', - filter_retracted=False)) - assert len(all_results) > 0 - # The filtered results should be a smaller subset of all the results, assuming that some of - # the results are retracted nanopublications. - assert len(all_results) > len(filtered_results) - - @pytest.mark.flaky(max_runs=10) - @skip_if_nanopub_server_unavailable - def test_find_retractions_of(self): - uri = 'http://purl.org/np/RAnksi2yDP7jpe7F6BwWCpMOmzBEcUImkAKUeKEY_2Yus' - results = client.find_retractions_of(uri, valid_only=False) - expected_uris = [ - 'http://purl.org/np/RAYhe0XddJhBsJvVt0h_aq16p6f94ymc2wS-q2BAgnPVY', - 'http://purl.org/np/RACdYpR-6DZnT6JkEr1ItoYYXMAILjOhDqDZsMVO8EBZI' - ] - for expected_uri in expected_uris: - assert expected_uri in results - - - @pytest.mark.flaky(max_runs=10) - @skip_if_nanopub_server_unavailable - def test_find_retractions_of_valid_only(self): - uri = 'http://purl.org/np/RAnksi2yDP7jpe7F6BwWCpMOmzBEcUImkAKUeKEY_2Yus' - results = client.find_retractions_of(uri, valid_only=True) - expected_uri = 'http://purl.org/np/RAYhe0XddJhBsJvVt0h_aq16p6f94ymc2wS-q2BAgnPVY' - assert expected_uri in results - # This is a nanopublication that is signed with a different public key than the nanopub - # it retracts, so it is not valid and should not be returned. - unexpected_uri = 'http://purl.org/np/RACdYpR-6DZnT6JkEr1ItoYYXMAILjOhDqDZsMVO8EBZI' - assert unexpected_uri not in results + # TODO: find retracted in the new nanopub server to fix those tests + + # @pytest.mark.flaky(max_runs=10) + # @skip_if_nanopub_server_unavailable + # def test_find_things_filter_retracted(self): + # filtered_results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', + # filter_retracted=True)) + # assert len(filtered_results) > 0 + # all_results = list(client.find_things(type='http://purl.org/net/p-plan#Plan', + # filter_retracted=False)) + # assert len(all_results) > 0 + # # The filtered results should be a smaller subset of all the results, assuming that some of + # # the results are retracted nanopublications. + # assert len(all_results) > len(filtered_results) + + # @pytest.mark.flaky(max_runs=10) + # @skip_if_nanopub_server_unavailable + # def test_find_retractions_of(self): + # uri = 'http://purl.org/np/RAnksi2yDP7jpe7F6BwWCpMOmzBEcUImkAKUeKEY_2Yus' + # results = client.find_retractions_of(uri, valid_only=False) + # expected_uris = [ + # 'http://purl.org/np/RAYhe0XddJhBsJvVt0h_aq16p6f94ymc2wS-q2BAgnPVY', + # 'http://purl.org/np/RACdYpR-6DZnT6JkEr1ItoYYXMAILjOhDqDZsMVO8EBZI' + # ] + # for expected_uri in expected_uris: + # assert expected_uri in results + + + # @pytest.mark.flaky(max_runs=10) + # @skip_if_nanopub_server_unavailable + # def test_find_retractions_of_valid_only(self): + # uri = 'http://purl.org/np/RAnksi2yDP7jpe7F6BwWCpMOmzBEcUImkAKUeKEY_2Yus' + # results = client.find_retractions_of(uri, valid_only=True) + # expected_uri = 'http://purl.org/np/RAYhe0XddJhBsJvVt0h_aq16p6f94ymc2wS-q2BAgnPVY' + # assert expected_uri in results + # # This is a nanopublication that is signed with a different public key than the nanopub + # # it retracts, so it is not valid and should not be returned. + # unexpected_uri = 'http://purl.org/np/RACdYpR-6DZnT6JkEr1ItoYYXMAILjOhDqDZsMVO8EBZI' + # assert unexpected_uri not in results @pytest.mark.parametrize( "test_input,expected", diff --git a/tests/test_nanopub.py b/tests/test_nanopub.py index b1932b2c..e758043b 100644 --- a/tests/test_nanopub.py +++ b/tests/test_nanopub.py @@ -3,7 +3,7 @@ from nanopub import Nanopub, NanopubClaim, NanopubConf, NanopubRetract, NanopubUpdate, create_nanopub_index, namespaces from nanopub.templates.nanopub_introduction import NanopubIntroduction -from tests.conftest import default_conf, java_wrap, skip_if_nanopub_server_unavailable +from tests.conftest import default_conf, java_wrap, profile_test, skip_if_nanopub_server_unavailable def test_nanopub_sign_uri(): @@ -20,6 +20,7 @@ def test_nanopub_sign_uri(): np.sign() assert np.has_valid_signature assert np.source_uri == expected_np_uri + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -36,6 +37,7 @@ def test_nanopub_sign_uri2(): np.sign() assert np.has_valid_signature assert np.source_uri == expected_np_uri + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -52,6 +54,7 @@ def test_nanopub_sign_bnode(): np.sign() assert np.has_valid_signature assert np.source_uri == expected_np_uri + assert java_wrap.check_trusty_with_signature(np) def test_nanopub_sign_bnode2(): @@ -69,6 +72,7 @@ def test_nanopub_sign_bnode2(): ) np.sign() assert np.source_uri == expected_np_uri + assert java_wrap.check_trusty_with_signature(np) def test_nanopub_publish(): @@ -85,6 +89,7 @@ def test_nanopub_publish(): np.publish() assert np.has_valid_signature assert np.source_uri == expected_np_uri + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -97,6 +102,7 @@ def test_nanopub_claim(): java_np = java_wrap.sign(np) np.sign() assert np.source_uri is not None + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -118,6 +124,7 @@ def test_nanopub_retract(): java_np = java_wrap.sign(np2) np2.sign() assert np2.source_uri is not None + assert java_wrap.check_trusty_with_signature(np) assert np2.source_uri == java_np @@ -144,6 +151,7 @@ def test_nanopub_update(): java_np = java_wrap.sign(np2) np2.sign() assert np2.source_uri is not None + assert java_wrap.check_trusty_with_signature(np) assert np2.source_uri == java_np @@ -155,6 +163,7 @@ def test_nanopub_introduction(): java_np = java_wrap.sign(np) np.sign() assert np.source_uri is not None + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -162,8 +171,8 @@ def test_nanopub_index(): np_list = create_nanopub_index( conf=default_conf, np_list=[ - "https://purl.org/np/RAD28Nl4h_mFH92bsHUrtqoU4C6DCYy_BRTvpimjVFgJo", - "https://purl.org/np/RAEhbEJ1tdhPqM6gNPScX9vIY1ZtUzOz7woeJNzB3sh3E", + "https://purl.org/np/RA5cwuR2b7Or9Pkb50nhPcHa2-cD0-gEPb2B3Ly5IxyuA", + "https://purl.org/np/RAj1G7tgntNvXEgaMDmrc3rhxLekjZX6qsPIaEjUJ49NU", ], title="My nanopub index", description="This is my nanopub index", @@ -173,6 +182,7 @@ def test_nanopub_index(): ) for np in np_list: assert np.source_uri is not None + assert java_wrap.check_trusty_with_signature(np) @pytest.mark.flaky(max_runs=10) @@ -180,9 +190,9 @@ def test_nanopub_index(): def test_nanopub_fetch(): """Check that creating Nanopub from source URI (fetch) works for a few known nanopub URIs.""" known_nps = [ - 'http://purl.org/np/RANGY8fx_EYVeZzJOinH9FoY-WrQBerKKUy2J9RCDWH6U', - 'http://purl.org/np/RAABh3eQwmkdflVp50zYavHUK0NgZE2g2ewS2j4Ur6FHI', - 'http://purl.org/np/RA8to60YFWSVCh2n_iyHZ2yiYEt-hX_DdqbWa5yI9r-gI' + 'http://purl.org/np/RA5cwuR2b7Or9Pkb50nhPcHa2-cD0-gEPb2B3Ly5IxyuA', + 'http://purl.org/np/RAj1G7tgntNvXEgaMDmrc3rhxLekjZX6qsPIaEjUJ49NU', + 'http://purl.org/np/RAj75Z7QMYNalgNiMG9IthMuj18VuJbto9sC8Jl6lp9WM' ] for np_uri in known_nps: np = Nanopub( @@ -202,3 +212,55 @@ def test_unvalid_fetch(): assert publication.is_valid except Exception: assert True + + +def test_specific_file(): + """Test to sign a complex file with many blank nodes""" + import json + + from rdflib import Namespace + from rdflib.namespace import DCTERMS, PROV + np_conf = NanopubConf(profile=profile_test, use_test_server=True) + np_conf.add_prov_generated_time = True, + np_conf.add_pubinfo_generated_time = True, + np_conf.attribute_assertion_to_profile = True, + np_conf.attribute_publication_to_profile = True, + + with open('./tests/resources/many_bnodes_with_annotations.json') as f: + nanopub_rdf = json.loads(f.read()) + + annotations_rdf = nanopub_rdf["@annotations"] + del nanopub_rdf["@annotations"] + nanopub_rdf = str(json.dumps(nanopub_rdf)) + + g = Graph() + g.parse(data=nanopub_rdf, format="json-ld") + + np = Nanopub( + assertion=g, + conf=np_conf, + ) + source = "https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=f9641190-9151-4f7e-89ff-1e7a818c30ee" + if annotations_rdf: + np.provenance.parse(data=str(json.dumps(annotations_rdf)), format="json-ld") + if source: + np.provenance.add((np.assertion.identifier, PROV.hadPrimarySource, URIRef(source))) + + PAV = Namespace("http://purl.org/pav/") + if True: + np.pubinfo.add( + ( + np.metadata.np_uri, + DCTERMS.conformsTo, + URIRef("https://w3id.org/biolink/vocab/"), + ) + ) + np.pubinfo.add( + ( + URIRef("https://w3id.org/biolink/vocab/"), + PAV.version, + Literal("3.1.0"), + ) + ) + np.sign() + assert java_wrap.check_trusty_with_signature(np) diff --git a/tests/test_sign_utils.py b/tests/test_sign_utils.py index 6b53bdfe..fd3e34e9 100644 --- a/tests/test_sign_utils.py +++ b/tests/test_sign_utils.py @@ -24,7 +24,7 @@ def test_nanopub_sign(): np.rdf, profile_test, DUMMY_NAMESPACE, - DUMMY_NAMESPACE.pubinfo + np.pubinfo ) np.update_from_signed(signed_g) assert np.source_uri == expected_np_uri diff --git a/tests/test_testsuite.py b/tests/test_testsuite.py index 9bedde70..000fdf2e 100644 --- a/tests/test_testsuite.py +++ b/tests/test_testsuite.py @@ -43,6 +43,7 @@ def test_testsuite_valid_signed(): assert np.is_valid assert np.metadata.trusty is not None assert np.metadata.signature is not None + assert java_wrap.check_trusty_with_signature(np) # TODO: we should be able to validate this signature? # assert np.has_valid_signature @@ -92,6 +93,7 @@ def test_testsuite_sign_valid(): assert np.has_valid_signature assert np.has_valid_trusty assert np.is_valid + assert java_wrap.check_trusty_with_signature(np) assert np.source_uri == java_np @@ -111,6 +113,7 @@ def test_testsuite_valid_signature(): assert np.is_valid assert np.has_valid_signature assert np.has_valid_trusty + assert java_wrap.check_trusty_with_signature(np) def test_testsuite_invalid_plain():