<filename>manubot/cite/curie/__init__.py
"""
Compact Uniform Resource Identifiers

Manubot keeps a local versions of the identifier.org registry.
Repository developers can run the following commands to update the Manubot version.

```shell
# regenerate manubot/cite/curie/namespaces.json
python manubot/cite/curie/__init__.py
# if namespaces.json has changed, the following test will likely fail:
pytest manubot/cite/tests/test_handlers.py::test_prefix_to_handler
# copy captured stdout from failed test_prefix_to_handler to
# manubot.cite.handlers.prefix_to_handler. Pre-commit hook will reformat file.
```

References:

- https://en.wikipedia.org/wiki/CURIE
- https://identifiers.org/
- https://github.com/manubot/manubot/issues/218
- https://docs.identifiers.org/articles/api.html
- https://n2t.net/e/compact_ids.html
- https://n2t.net/e/cdl_ebi_prefixes.yaml
- https://en.wikipedia.org/wiki/MIRIAM_Registry
- [Identifiers.org and MIRIAM Registry: community resources to provide persistent identification](https://doi.org/10.1093/nar/gkr1097)
- [On the road to robust data citation](https://doi.org/10.1038/sdata.2018.95)
- [Uniform Resolution of Compact Identifiers for Biomedical Data](https://doi.org/10.1038/sdata.2018.29)
"""
import dataclasses
import functools
import json
import logging
import pathlib
import re
import typing

from manubot.cite.handlers import Handler

_keep_namespace_fields = {
    "prefix",
    "mirId",  # MIRIAM Registry identifier
    "name",
    "pattern",  # regex pattern
    "description",
    "sampleId",  # example identifier
    "namespaceEmbeddedInLui",  # whether prefix is included in the local unique identifier
    "curiePrefix",  # a computed field for the actual prefix used by CURIEs including required capitalization
}


namespace_path = pathlib.Path(__file__).parent.joinpath("namespaces.json")


@dataclasses.dataclass
class Handler_CURIE(Handler):
    def __post_init__(self):
        prefix_to_namespace = get_prefix_to_namespace()
        self.namespace = prefix_to_namespace[self.prefix_lower]
        self.standard_prefix = self.namespace["prefix"]
        self.prefixes = sorted(
            {self.namespace["prefix"], self.namespace["curiePrefix"].lower()}
        )
        self.accession_pattern = self.namespace["pattern"]

    def get_csl_item(self, citekey):
        from ..url import get_url_csl_item

        url = curie_to_url(citekey.standard_id)
        return get_url_csl_item(url)

    def _get_lui(self, citekey) -> str:
        """
        When namespaceEmbeddedInLui is true, some identifiers.org namespace
        metadata, such as pattern, include curiePrefix. This function
        returns the local unique identifier (lui / accession) based on this
        caveat.
        """
        lui = citekey.accession
        if self.namespace.get("namespaceEmbeddedInLui", False):
            lui = f"{self.namespace['curiePrefix']}:{lui}"
        return lui

    def inspect(self, citekey):
        pattern = self._get_pattern("accession_pattern")
        lui = self._get_lui(citekey)
        if not pattern.fullmatch(lui):
            return f"{lui} does not match regex {pattern.pattern}"


def get_curie_handlers():
    """Get all possible CURIE handlers"""
    namespaces = get_namespaces(compile_patterns=True)
    handlers = [Handler_CURIE(ns["prefix"]) for ns in namespaces]
    return handlers


def _download_namespaces():
    """
    Download all namespaces from the Identifiers.org Central Registry.

    Example of a single namespace JSON data at
    <https://registry.api.identifiers.org/restApi/namespaces/230>
    """
    import requests

    params = dict(size=5000, sort="prefix")
    url = "https://registry.api.identifiers.org/restApi/namespaces"
    response = requests.get(url, params)
    response.raise_for_status()
    results = response.json()
    if results["page"]["totalPages"] > 1:
        logging.warning(
            "_download_curie_registry does not support multi-page results\n"
            f"{response.url}\n{json.dumps(results['page'])}"
        )
    namespaces = results["_embedded"]["namespaces"]
    # filter namespace fields to reduce diskspace
    for namespace in namespaces:
        namespace["curiePrefix"] = get_curie_prefix(namespace)
        for field in set(namespace) - _keep_namespace_fields:
            del namespace[field]
    json_text = json.dumps(namespaces, indent=2, ensure_ascii=False)
    namespace_path.write_text(json_text + "\n", encoding="utf-8")


def get_curie_prefix(namespace):
    """
    The prefix portion of a CURIE is not always the same as the identifiers.org namespace prefix.
    This occurs when namespaceEmbeddedInLui is true.
    When namespaceEmbeddedInLui, CURIEs require a specific prefix capitalization.
    The actual prefix and capitalization is reverse engineered from the regex pattern.

    References:
    https://github.com/identifiers-org/identifiers-org.github.io/issues/100#issuecomment-614679142

    """
    if not namespace["namespaceEmbeddedInLui"]:
        return namespace["prefix"]
    import exrex

    example_curie = exrex.getone(namespace["pattern"])
    curie_prefix, _ = example_curie.split(":", 1)
    return curie_prefix


def get_namespaces(compile_patterns=False) -> typing.List[dict]:
    with namespace_path.open(encoding="utf-8-sig") as read_file:
        namespaces = json.load(read_file)
    if compile_patterns:
        for namespace in namespaces:
            namespace["compiled_pattern"] = re.compile(namespace["pattern"])
    return namespaces


@functools.lru_cache()
def get_prefix_to_namespace() -> typing.Dict[str, typing.Dict]:
    prefix_to_namespace = dict()
    for ns in get_namespaces():
        for key in "prefix", "curiePrefix":
            prefix_to_namespace[ns[key].lower()] = ns
    return prefix_to_namespace


def standardize_curie(curie):
    """
    Return CURIE with identifiers.org expected capitalization.
    `curie` should be in `prefix:accession` format.
    If `curie` is malformed or uses an unrecognized prefix, raise ValueError.
    """
    if not isinstance(curie, str):
        raise TypeError(
            f"curie parameter should be string. Received {curie.__class__.__name__} instead for {curie}"
        )
    try:
        prefix, accession = curie.split(":", 1)
    except ValueError:
        raise ValueError(
            f"curie must be splittable by `:` and formatted like `prefix:accession`. Received {curie}"
        )
    # do not yet understand capitalization
    # https://github.com/identifiers-org/identifiers-org.github.io/issues/100
    prefix_lower = prefix.lower()
    prefix_to_namespaces = get_prefix_to_namespace()
    try:
        namespace = prefix_to_namespaces[prefix_lower]
    except KeyError:
        raise ValueError(
            f"prefix {prefix_lower} for {curie} is not a recognized prefix"
        )
    return f"{namespace['curiePrefix']}:{accession}"


def curie_to_url(curie):
    """
    `curie` should be in `prefix:accession` format
    """
    curie = standardize_curie(curie)
    resolver_url = "https://identifiers.org"
    return f"{resolver_url}/{curie}"


if __name__ == "__main__":
    _download_namespaces()
    namespaces = get_namespaces()

<filename>manubot/cite/curie/__init__.py
"""
Compact Uniform Resource Identifiers

Manubot keeps a local versions of the identifier.org registry.
Repository developers can run the following commands to update the Manubot version.

```shell
# regenerate manubot/cite/curie/namespaces.json
python manubot/cite/curie/__init__.py
# if namespaces.json has changed, the following test will likely fail:
pytest manubot/cite/tests/test_handlers.py::test_prefix_to_handler
# copy captured stdout from failed test_prefix_to_handler to
# manubot.cite.handlers.prefix_to_handler. Pre-commit hook will reformat file.
```

References:

- https://en.wikipedia.org/wiki/CURIE
- https://identifiers.org/
- https://github.com/manubot/manubot/issues/218
- https://docs.identifiers.org/articles/api.html
- https://n2t.net/e/compact_ids.html
- https://n2t.net/e/cdl_ebi_prefixes.yaml
- https://en.wikipedia.org/wiki/MIRIAM_Registry
- [Identifiers.org and MIRIAM Registry: community resources to provide persistent identification](https://doi.org/10.1093/nar/gkr1097)
- [On the road to robust data citation](https://doi.org/10.1038/sdata.2018.95)
- [Uniform Resolution of Compact Identifiers for Biomedical Data](https://doi.org/10.1038/sdata.2018.29)
"""
import dataclasses
import functools
import json
import logging
import pathlib
import re
import typing

from manubot.cite.handlers import Handler

_keep_namespace_fields = {
    "prefix",
    "mirId",  # MIRIAM Registry identifier
    "name",
    "pattern",  # regex pattern
    "description",
    "sampleId",  # example identifier
    "namespaceEmbeddedInLui",  # whether prefix is included in the local unique identifier
    "curiePrefix",  # a computed field for the actual prefix used by CURIEs including required capitalization
}


namespace_path = pathlib.Path(__file__).parent.joinpath("namespaces.json")


@dataclasses.dataclass
class Handler_CURIE(Handler):
    def __post_init__(self):
        prefix_to_namespace = get_prefix_to_namespace()
        self.namespace = prefix_to_namespace[self.prefix_lower]
        self.standard_prefix = self.namespace["prefix"]
        self.prefixes = sorted(
            {self.namespace["prefix"], self.namespace["curiePrefix"].lower()}
        )
        self.accession_pattern = self.namespace["pattern"]

    def get_csl_item(self, citekey):
        from ..url import get_url_csl_item

        url = curie_to_url(citekey.standard_id)
        return get_url_csl_item(url)

    def _get_lui(self, citekey) -> str:
        """
        When namespaceEmbeddedInLui is true, some identifiers.org namespace
        metadata, such as pattern, include curiePrefix. This function
        returns the local unique identifier (lui / accession) based on this
        caveat.
        """
        lui = citekey.accession
        if self.namespace.get("namespaceEmbeddedInLui", False):
            lui = f"{self.namespace['curiePrefix']}:{lui}"
        return lui

    def inspect(self, citekey):
        pattern = self._get_pattern("accession_pattern")
        lui = self._get_lui(citekey)
        if not pattern.fullmatch(lui):
            return f"{lui} does not match regex {pattern.pattern}"


def get_curie_handlers():
    """Get all possible CURIE handlers"""
    namespaces = get_namespaces(compile_patterns=True)
    handlers = [Handler_CURIE(ns["prefix"]) for ns in namespaces]
    return handlers


def _download_namespaces():
    """
    Download all namespaces from the Identifiers.org Central Registry.

    Example of a single namespace JSON data at
    <https://registry.api.identifiers.org/restApi/namespaces/230>
    """
    import requests

    params = dict(size=5000, sort="prefix")
    url = "https://registry.api.identifiers.org/restApi/namespaces"
    response = requests.get(url, params)
    response.raise_for_status()
    results = response.json()
    if results["page"]["totalPages"] > 1:
        logging.warning(
            "_download_curie_registry does not support multi-page results\n"
            f"{response.url}\n{json.dumps(results['page'])}"
        )
    namespaces = results["_embedded"]["namespaces"]
    # filter namespace fields to reduce diskspace
    for namespace in namespaces:
        namespace["curiePrefix"] = get_curie_prefix(namespace)
        for field in set(namespace) - _keep_namespace_fields:
            del namespace[field]
    json_text = json.dumps(namespaces, indent=2, ensure_ascii=False)
    namespace_path.write_text(json_text + "\n", encoding="utf-8")


def get_curie_prefix(namespace):
    """
    The prefix portion of a CURIE is not always the same as the identifiers.org namespace prefix.
    This occurs when namespaceEmbeddedInLui is true.
    When namespaceEmbeddedInLui, CURIEs require a specific prefix capitalization.
    The actual prefix and capitalization is reverse engineered from the regex pattern.

    References:
    https://github.com/identifiers-org/identifiers-org.github.io/issues/100#issuecomment-614679142

    """
    if not namespace["namespaceEmbeddedInLui"]:
        return namespace["prefix"]
    import exrex

    example_curie = exrex.getone(namespace["pattern"])
    curie_prefix, _ = example_curie.split(":", 1)
    return curie_prefix


def get_namespaces(compile_patterns=False) -> typing.List[dict]:
    with namespace_path.open(encoding="utf-8-sig") as read_file:
        namespaces = json.load(read_file)
    if compile_patterns:
        for namespace in namespaces:
            namespace["compiled_pattern"] = re.compile(namespace["pattern"])
    return namespaces


@functools.lru_cache()
def get_prefix_to_namespace() -> typing.Dict[str, typing.Dict]:
    prefix_to_namespace = dict()
    for ns in get_namespaces():
        for key in "prefix", "curiePrefix":
            prefix_to_namespace[ns[key].lower()] = ns
    return prefix_to_namespace


def standardize_curie(curie):
    """
    Return CURIE with identifiers.org expected capitalization.
    `curie` should be in `prefix:accession` format.
    If `curie` is malformed or uses an unrecognized prefix, raise ValueError.
    """
    if not isinstance(curie, str):
        raise TypeError(
            f"curie parameter should be string. Received {curie.__class__.__name__} instead for {curie}"
        )
    try:
        prefix, accession = curie.split(":", 1)
    except ValueError:
        raise ValueError(
            f"curie must be splittable by `:` and formatted like `prefix:accession`. Received {curie}"
        )
    # do not yet understand capitalization
    # https://github.com/identifiers-org/identifiers-org.github.io/issues/100
    prefix_lower = prefix.lower()
    prefix_to_namespaces = get_prefix_to_namespace()
    try:
        namespace = prefix_to_namespaces[prefix_lower]
    except KeyError:
        raise ValueError(
            f"prefix {prefix_lower} for {curie} is not a recognized prefix"
        )
    return f"{namespace['curiePrefix']}:{accession}"


def curie_to_url(curie):
    """
    `curie` should be in `prefix:accession` format
    """
    curie = standardize_curie(curie)
    resolver_url = "https://identifiers.org"
    return f"{resolver_url}/{curie}"


if __name__ == "__main__":
    _download_namespaces()
    namespaces = get_namespaces()

Compact Uniform Resource Identifiers  Manubot keeps a local versions of the identifier.org registry. Repository developers can run the following commands to update the Manubot version.  ```shell # regenerate manubot/cite/curie/namespaces.json python manubot/cite/curie/__init__.py # if namespaces.json has changed, the following test will likely fail: pytest manubot/cite/tests/test_handlers.py::test_prefix_to_handler # copy captured stdout from failed test_prefix_to_handler to # manubot.cite.handlers.prefix_to_handler. Pre-commit hook will reformat file. ```  References:  - https://en.wikipedia.org/wiki/CURIE - https://identifiers.org/ - https://github.com/manubot/manubot/issues/218 - https://docs.identifiers.org/articles/api.html - https://n2t.net/e/compact_ids.html - https://n2t.net/e/cdl_ebi_prefixes.yaml - https://en.wikipedia.org/wiki/MIRIAM_Registry - [Identifiers.org and MIRIAM Registry: community resources to provide persistent identification](https://doi.org/10.1093/nar/gkr1097) - [On the road to robust data citation](https://doi.org/10.1038/sdata.2018.95) - [Uniform Resolution of Compact Identifiers for Biomedical Data](https://doi.org/10.1038/sdata.2018.29) # MIRIAM Registry identifier # regex pattern # example identifier # whether prefix is included in the local unique identifier # a computed field for the actual prefix used by CURIEs including required capitalization When namespaceEmbeddedInLui is true, some identifiers.org namespace         metadata, such as pattern, include curiePrefix. This function         returns the local unique identifier (lui / accession) based on this         caveat. Get all possible CURIE handlers Download all namespaces from the Identifiers.org Central Registry.      Example of a single namespace JSON data at     <https://registry.api.identifiers.org/restApi/namespaces/230> # filter namespace fields to reduce diskspace The prefix portion of a CURIE is not always the same as the identifiers.org namespace prefix.     This occurs when namespaceEmbeddedInLui is true.     When namespaceEmbeddedInLui, CURIEs require a specific prefix capitalization.     The actual prefix and capitalization is reverse engineered from the regex pattern.      References:     https://github.com/identifiers-org/identifiers-org.github.io/issues/100#issuecomment-614679142 Return CURIE with identifiers.org expected capitalization.     `curie` should be in `prefix:accession` format.     If `curie` is malformed or uses an unrecognized prefix, raise ValueError. # do not yet understand capitalization # https://github.com/identifiers-org/identifiers-org.github.io/issues/100 `curie` should be in `prefix:accession` format