Skip to content

Profile

This module holds objects and functions to load a nanopub user profile.

Profile

Source code in nanopub/profile.py
class Profile:

    def __init__(
            self,
            agent_id: Optional[str] = None,
            name: Optional[str] = None,
            private_key: Optional[Union[Path, str]] = None,
            public_key: Optional[Union[Path, str]] = None,
            introduction_nanopub_uri: Optional[str] = None,
            *,
            orcid_id: Optional[str] = None,
    ) -> None:
        """Represents a user profile.

            Attributes:
                agent_id (str): URI identifying the signer — an ORCID iD or any other agent URI
                name (str): The user's name
                private_key (Optional[Union[Path, str]]): Path to the user's private key, or the key as string
                public_key (Optional[Union[Path, str]]): Path to the user's public key, or the key as string
                introduction_nanopub_uri (Optional[str]): URI of the user's profile nanopub
                orcid_id (str): Deprecated alias for ``agent_id``.
            """
        if orcid_id is not None:
            warnings.warn(
                "The 'orcid_id' argument is deprecated; use 'agent_id' instead.",
                DeprecationWarning, stacklevel=2,
            )
            if agent_id is None:
                agent_id = orcid_id
        self.agent_id = agent_id
        self._name = name
        self._introduction_nanopub_uri = introduction_nanopub_uri

        if not private_key:
            self.generate_keys()
        elif isinstance(private_key, Path):
            try:
                with open(private_key) as f:
                    raw_private_key = f.read()
            except FileNotFoundError:
                raise ProfileError(
                    f'Private key file {private_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}'
                )
            self._private_key = normalize_private_key(raw_private_key)
        else:
            self._private_key = normalize_private_key(private_key)

        if not public_key and private_key:
            logger.info(
                'The public key was not provided when loading the Nanopub profile, generating it from the provided private key')
            self._public_key = normalize_public_key(self._private_key)
        elif isinstance(public_key, Path):
            try:
                with open(public_key) as f:
                    raw_public_key = f.read()
            except FileNotFoundError:
                raise ProfileError(
                    f'Public 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}'
                )
            self._public_key = normalize_public_key(raw_public_key)
        elif public_key:
            self._public_key = normalize_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(RSA_KEY_SIZE)
        public_key_str = key.publickey().export_key().decode('utf-8')

        self._private_key = _encode_private_key(key)
        self._public_key = _encode_public_key(key)
        logger.info(f"Public/private RSA key pair has been generated for {self.agent_id} ({self.name})")
        return public_key_str

    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:
            folder: The path to the folder to store the user's profile files.

        Returns:
            The path where the profile was stored.
        """
        folder = Path(folder)
        folder.mkdir(parents=True, exist_ok=True)
        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 os.path.exists(private_key_path):
            with open(private_key_path, "w") as f:
                f.write(self.private_key + '\n')
        if not os.path.exists(public_key_path):
            with open(public_key_path, "w") as f:
                f.write(self.public_key)

        intro_uri = ''
        if self.introduction_nanopub_uri:
            intro_uri = f" {self.introduction_nanopub_uri}"
        # Store profile.yml
        profile_yaml = f"""agent_id: {self.agent_id}
name: {self.name}
public_key: {public_key_path}
private_key: {private_key_path}
introduction_nanopub_uri:{intro_uri}
"""
        with open(profile_path, "w") as f:
            f.write(profile_yaml)

        return profile_path

    @property
    def agent_id(self):
        return self._agent_id

    @agent_id.setter
    def agent_id(self, value):
        self._agent_id = _validate_agent_id(value)

    @property
    def orcid_id(self):
        warnings.warn(
            "Profile.orcid_id is deprecated; use Profile.agent_id instead.",
            DeprecationWarning, stacklevel=2,
        )
        return self._agent_id

    @orcid_id.setter
    def orcid_id(self, value):
        warnings.warn(
            "Profile.orcid_id is deprecated; use Profile.agent_id instead.",
            DeprecationWarning, stacklevel=2,
        )
        self.agent_id = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def private_key(self):
        return self._private_key

    @private_key.setter
    def private_key(self, value):
        self._private_key = value

    @property
    def public_key(self):
        return self._public_key

    @public_key.setter
    def public_key(self, value):
        self._public_key = value

    @property
    def introduction_nanopub_uri(self):
        return self._introduction_nanopub_uri

    @introduction_nanopub_uri.setter
    def introduction_nanopub_uri(self, value):
        self._introduction_nanopub_uri = value

    def __repr__(self):
        return f"""\033[1mAgent ID\033[0m: {self._agent_id}
\033[1mName\033[0m: {self._name}
\033[1mPrivate key\033[0m: {self._private_key}
\033[1mPublic key\033[0m: {self._public_key}
\033[1mIntro Nanopub URI\033[0m: {self._introduction_nanopub_uri}"""

__init__(agent_id=None, name=None, private_key=None, public_key=None, introduction_nanopub_uri=None, *, orcid_id=None)

Represents a user profile.

Attributes:

Name Type Description
agent_id str

URI identifying the signer — an ORCID iD or any other agent URI

name str

The user’s name

private_key Optional[Union[Path, str]]

Path to the user’s private key, or the key as string

public_key Optional[Union[Path, str]]

Path to the user’s public key, or the key as string

introduction_nanopub_uri Optional[str]

URI of the user’s profile nanopub

orcid_id str

Deprecated alias for agent_id.

Source code in nanopub/profile.py
def __init__(
        self,
        agent_id: Optional[str] = None,
        name: Optional[str] = None,
        private_key: Optional[Union[Path, str]] = None,
        public_key: Optional[Union[Path, str]] = None,
        introduction_nanopub_uri: Optional[str] = None,
        *,
        orcid_id: Optional[str] = None,
) -> None:
    """Represents a user profile.

        Attributes:
            agent_id (str): URI identifying the signer — an ORCID iD or any other agent URI
            name (str): The user's name
            private_key (Optional[Union[Path, str]]): Path to the user's private key, or the key as string
            public_key (Optional[Union[Path, str]]): Path to the user's public key, or the key as string
            introduction_nanopub_uri (Optional[str]): URI of the user's profile nanopub
            orcid_id (str): Deprecated alias for ``agent_id``.
        """
    if orcid_id is not None:
        warnings.warn(
            "The 'orcid_id' argument is deprecated; use 'agent_id' instead.",
            DeprecationWarning, stacklevel=2,
        )
        if agent_id is None:
            agent_id = orcid_id
    self.agent_id = agent_id
    self._name = name
    self._introduction_nanopub_uri = introduction_nanopub_uri

    if not private_key:
        self.generate_keys()
    elif isinstance(private_key, Path):
        try:
            with open(private_key) as f:
                raw_private_key = f.read()
        except FileNotFoundError:
            raise ProfileError(
                f'Private key file {private_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}'
            )
        self._private_key = normalize_private_key(raw_private_key)
    else:
        self._private_key = normalize_private_key(private_key)

    if not public_key and private_key:
        logger.info(
            'The public key was not provided when loading the Nanopub profile, generating it from the provided private key')
        self._public_key = normalize_public_key(self._private_key)
    elif isinstance(public_key, Path):
        try:
            with open(public_key) as f:
                raw_public_key = f.read()
        except FileNotFoundError:
            raise ProfileError(
                f'Public 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}'
            )
        self._public_key = normalize_public_key(raw_public_key)
    elif public_key:
        self._public_key = normalize_public_key(public_key)

generate_keys()

Generate private/public RSA key pair at the path specified in the profile.yml, to be used to sign nanopubs

Source code in nanopub/profile.py
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(RSA_KEY_SIZE)
    public_key_str = key.publickey().export_key().decode('utf-8')

    self._private_key = _encode_private_key(key)
    self._public_key = _encode_public_key(key)
    logger.info(f"Public/private RSA key pair has been generated for {self.agent_id} ({self.name})")
    return public_key_str

store(folder=USER_CONFIG_DIR)

Stores the nanopub user profile. By default the profile is stored in HOME_DIR/.nanopub/profile.yaml.

Parameters:

Name Type Description Default
folder Path

The path to the folder to store the user’s profile files.

USER_CONFIG_DIR

Returns:

Type Description
str

The path where the profile was stored.

Source code in nanopub/profile.py
    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:
            folder: The path to the folder to store the user's profile files.

        Returns:
            The path where the profile was stored.
        """
        folder = Path(folder)
        folder.mkdir(parents=True, exist_ok=True)
        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 os.path.exists(private_key_path):
            with open(private_key_path, "w") as f:
                f.write(self.private_key + '\n')
        if not os.path.exists(public_key_path):
            with open(public_key_path, "w") as f:
                f.write(self.public_key)

        intro_uri = ''
        if self.introduction_nanopub_uri:
            intro_uri = f" {self.introduction_nanopub_uri}"
        # Store profile.yml
        profile_yaml = f"""agent_id: {self.agent_id}
name: {self.name}
public_key: {public_key_path}
private_key: {private_key_path}
introduction_nanopub_uri:{intro_uri}
"""
        with open(profile_path, "w") as f:
            f.write(profile_yaml)

        return profile_path

ProfileError

Bases: RuntimeError

Error to be raised if profile is not setup correctly.

Source code in nanopub/profile.py
class ProfileError(RuntimeError):
    """
    Error to be raised if profile is not setup correctly.
    """

ProfileLoader

Bases: Profile

A class to load a user profile from a local YAML file, only used for YAtiML.

Source code in nanopub/profile.py
class ProfileLoader(Profile):
    """A class to load a user profile from a local YAML file, only used for YAtiML."""

    def __init__(
            self,
            name: str,
            private_key: Path,
            public_key: Optional[Path],
            agent_id: Optional[str] = None,
            orcid_id: Optional[str] = None,
            introduction_nanopub_uri: Optional[str] = None
    ) -> None:
        """Create a ProfileLoader.

        Both the ``agent_id`` key and the legacy ``orcid_id`` key are accepted
        from profile.yml; ``agent_id`` takes precedence when both are present.
        """
        super().__init__(
            agent_id=agent_id if agent_id is not None else orcid_id,
            name=name,
            private_key=private_key,
            public_key=public_key,
            introduction_nanopub_uri=introduction_nanopub_uri,
        )

__init__(name, private_key, public_key, agent_id=None, orcid_id=None, introduction_nanopub_uri=None)

Create a ProfileLoader.

Both the agent_id key and the legacy orcid_id key are accepted from profile.yml; agent_id takes precedence when both are present.

Source code in nanopub/profile.py
def __init__(
        self,
        name: str,
        private_key: Path,
        public_key: Optional[Path],
        agent_id: Optional[str] = None,
        orcid_id: Optional[str] = None,
        introduction_nanopub_uri: Optional[str] = None
) -> None:
    """Create a ProfileLoader.

    Both the ``agent_id`` key and the legacy ``orcid_id`` key are accepted
    from profile.yml; ``agent_id`` takes precedence when both are present.
    """
    super().__init__(
        agent_id=agent_id if agent_id is not None else orcid_id,
        name=name,
        private_key=private_key,
        public_key=public_key,
        introduction_nanopub_uri=introduction_nanopub_uri,
    )

format_key(key)

Format private and public keys to remove header/footer and all newlines, as this is required by nanopub-java.

.. deprecated:: format_key is deprecated and will be removed in an upcoming major version. It only strips PEM armor from a PKCS#8/SPKI key via string replacement and silently mishandles PKCS#1 and CRLF input. Use :func:normalize_private_key / :func:normalize_public_key instead, which accept any standard PEM/DER/bare key and return the same canonical single-line base64 form.

Source code in nanopub/profile.py
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.

    .. deprecated::
        ``format_key`` is deprecated and will be removed in an upcoming major
        version. It only strips PEM armor from a PKCS#8/SPKI key via string
        replacement and silently mishandles PKCS#1 and CRLF input. Use
        :func:`normalize_private_key` / :func:`normalize_public_key` instead,
        which accept any standard PEM/DER/bare key and return the same canonical
        single-line base64 form.
    """
    warnings.warn(
        "format_key() is deprecated and will be removed in an upcoming major "
        "version; use normalize_private_key() / normalize_public_key() instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    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()

generate_keyfiles(path=USER_CONFIG_DIR)

Generate private/public RSA key pair at the path specified in the profile.yml, to be used to sign nanopubs

Source code in nanopub/profile.py
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(path).exists():
        Path(path).mkdir()

    key = RSA.generate(RSA_KEY_SIZE)
    private_key_str = _encode_private_key(key)
    public_key_str = _encode_public_key(key)
    private_path = path / "id_rsa"
    public_path = path / "id_rsa.pub"

    # Store key pair
    private_key_file = open(private_path, "w")
    private_key_file.write(private_key_str)
    private_key_file.close()

    public_key_file = open(public_path, "w")
    public_key_file.write(public_key_str)
    public_key_file.close()
    logger.info(f"Public/private RSA key pair has been generated in {private_path} and {public_path}")
    return public_key_str

load_profile(profile_path=DEFAULT_PROFILE_PATH)

Retrieve nanopub user profile.

By default the profile is stored in HOME_DIR/.nanopub/profile.yaml.

Returns:

Type Description
Profile

A Profile containing the data from the configuration file.

Raises:

Type Description
RecognitionError

If there is an error in the file.

Source code in nanopub/profile.py
def load_profile(profile_path: Union[Path, str] = DEFAULT_PROFILE_PATH) -> Profile:
    """Retrieve nanopub user profile.

    By default the profile is stored in `HOME_DIR/.nanopub/profile.yaml`.

    Returns:
        A Profile containing the data from the configuration file.

    Raises:
        yatiml.RecognitionError: If there is an
            error in the file.
    """
    try:
        return _load_profile(Path(profile_path))
    except (yatiml.RecognitionError, FileNotFoundError) as e:
        msg = (f'{e}\nYour nanopub profile has not been set up yet, or is not set up correctly.\n'
               f'{PROFILE_INSTRUCTIONS_MESSAGE}')
        raise ProfileError(msg)

normalize_private_key(key_data)

Normalize any RSA private key to nanopub’s canonical single-line base64.

The canonical form is the base64 of the PKCS#8 DER with no PEM armor and no newlines, as required by nanopub-java and assumed by the signing code.

Source code in nanopub/profile.py
def normalize_private_key(key_data: str) -> str:
    """Normalize any RSA private key to nanopub's canonical single-line base64.

    The canonical form is the base64 of the PKCS#8 DER with no PEM armor and no
    newlines, as required by nanopub-java and assumed by the signing code.
    """
    key = _load_rsa_key(key_data)
    if not key.has_private():
        raise ProfileError(
            'A public key was provided where a private key was expected.'
        )
    return _encode_private_key(key)

normalize_public_key(key_data)

Normalize any RSA public key to nanopub’s canonical single-line base64.

The canonical form is the base64 of the SubjectPublicKeyInfo DER with no PEM armor and no newlines. A private key may be passed in, in which case the corresponding public key is derived.

Source code in nanopub/profile.py
def normalize_public_key(key_data: str) -> str:
    """Normalize any RSA public key to nanopub's canonical single-line base64.

    The canonical form is the base64 of the SubjectPublicKeyInfo DER with no PEM
    armor and no newlines. A private key may be passed in, in which case the
    corresponding public key is derived.
    """
    key = _load_rsa_key(key_data)
    return _encode_public_key(key)