Skip to content

API Reference

idutils.detectors

Functions for detecting the persistent identifier.

detect_identifier_schemes(val)

Detect persistent identifier scheme for a given value.

Note

Some schemes like PMID are very generic.

Source code in idutils/detectors.py
def detect_identifier_schemes(val):
    """Detect persistent identifier scheme for a given value.

    !!! note
        Some schemes like PMID are very generic.
    """
    schemes = []
    scheme_validators = IDUTILS_PID_SCHEMES + custom_schemes_registry().pick_scheme_key(
        "validator"
    )
    for scheme, test in scheme_validators:
        if test(val):
            schemes.append(scheme)

    # GNDs and ISBNs numbers can clash...
    if "gnd" in schemes and "isbn" in schemes:
        # ...in which case check explicitly if it's clearly a GND
        if val.lower().startswith("gnd:"):
            schemes.remove("isbn")

    if "viaf" in schemes and "url" in schemes:
        # check explicitly if it's a viaf
        for viaf_url in validators.viaf_urls:
            if val.startswith(viaf_url):
                schemes.remove("url")
    if "viaf" in schemes and "handle" in schemes:
        # check explicitly if it's a viaf
        for viaf_url in validators.viaf_urls:
            if val.startswith(viaf_url):
                schemes.remove("handle")

    scheme_filter = IDUTILS_SCHEME_FILTER + custom_schemes_registry().pick_scheme_key(
        "filter"
    )
    for first, remove_schemes in scheme_filter:
        if first in schemes:
            schemes = list(filter(lambda x: x not in remove_schemes, schemes))

    if (
        "handle" in schemes
        and "url" in schemes
        and not val.startswith("http://hdl.handle.net/")
        and not val.startswith("https://hdl.handle.net/")
    ):
        schemes = list(filter(lambda x: x != "handle", schemes))
    elif "handle" in schemes and ("ark" in schemes or "arxiv" in schemes):
        schemes = list(filter(lambda x: x != "handle", schemes))

    return schemes

idutils.validators

Utility file containing ID validators.

is_isbn10 = isbnlib.is_isbn10 module-attribute

Test if argument is an ISBN-10 number.

is_isbn13 = isbnlib.is_isbn13 module-attribute

Test if argument is an ISBN-13 number.

is_ads(val)

Test if argument is an ADS bibliographic code.

Source code in idutils/validators.py
def is_ads(val):
    """Test if argument is an ADS bibliographic code."""
    val = unicodedata.normalize("NFKD", val)
    return ads_regexp.match(val)

is_ark(val)

Test if argument is an ARK.

Source code in idutils/validators.py
def is_ark(val):
    """Test if argument is an ARK."""
    res = urlparse(val)
    return ark_suffix_regexp.match(val) or (
        res.scheme == "http"
        and res.netloc != ""
        and
        # Note res.path includes leading slash, hence [1:] to use same reexp
        ark_suffix_regexp.match(res.path[1:])
        and res.params == ""
    )

is_arrayexpress_array(val)

Test if argument is an ArrayExpress array accession.

Source code in idutils/validators.py
def is_arrayexpress_array(val):
    """Test if argument is an ArrayExpress array accession."""
    return arrayexpress_array_regexp.match(val)

is_arrayexpress_experiment(val)

Test if argument is an ArrayExpress experiment accession.

Source code in idutils/validators.py
def is_arrayexpress_experiment(val):
    """Test if argument is an ArrayExpress experiment accession."""
    return arrayexpress_experiment_regexp.match(val)

is_arxiv(val)

Test if argument is an arXiv ID.

See http://arxiv.org/help/arxiv_identifier and http://arxiv.org/help/arxiv_identifier_for_services.

Source code in idutils/validators.py
def is_arxiv(val):
    """Test if argument is an arXiv ID.

    See http://arxiv.org/help/arxiv_identifier and
        http://arxiv.org/help/arxiv_identifier_for_services.
    """
    return is_arxiv_post_2007(val) or is_arxiv_pre_2007(val)

is_arxiv_post_2007(val)

Test if argument is a post-2007 arXiv ID.

Source code in idutils/validators.py
def is_arxiv_post_2007(val):
    """Test if argument is a post-2007 arXiv ID."""
    return arxiv_post_2007_regexp.match(val) or arxiv_post_2007_with_class_regexp.match(
        val
    )

is_arxiv_pre_2007(val)

Test if argument is a pre-2007 arXiv ID.

Source code in idutils/validators.py
def is_arxiv_pre_2007(val):
    """Test if argument is a pre-2007 arXiv ID."""
    return arxiv_pre_2007_regexp.match(val)

is_ascl(val)

Test if argument is a ASCL accession.

Source code in idutils/validators.py
def is_ascl(val):
    """Test if argument is a ASCL accession."""
    return ascl_regexp.match(val)

is_bioproject(val)

Test if argument is a BioProject accession.

Source code in idutils/validators.py
def is_bioproject(val):
    """Test if argument is a BioProject accession."""
    return bioproject_regexp.match(val)

is_biosample(val)

Test if argument is a BioSample accession.

Source code in idutils/validators.py
def is_biosample(val):
    """Test if argument is a BioSample accession."""
    return biosample_regexp.match(val)

is_cstr(val)

Test if argument is a cstr.

Source code in idutils/validators.py
def is_cstr(val):
    """Test if argument is a cstr."""
    return cstr_regexp.match(val)

is_doi(val)

Test if argument is a DOI.

Source code in idutils/validators.py
def is_doi(val):
    """Test if argument is a DOI."""
    return doi_regexp.match(val)

is_ean(val)

Test if argument is a International Article Number (EAN-13 or EAN-8).

See http://en.wikipedia.org/wiki/International_Article_Number_(EAN).

Source code in idutils/validators.py
def is_ean(val):
    """Test if argument is a International Article Number (EAN-13 or EAN-8).

    See http://en.wikipedia.org/wiki/International_Article_Number_(EAN).
    """
    return is_ean13(val) or is_ean8(val)

is_ean13(val)

Test if argument is a International Article Number (EAN-13).

Source code in idutils/validators.py
def is_ean13(val):
    """Test if argument is a International Article Number (EAN-13)."""
    if len(val) != 13:
        return False
    sequence = [1, 3]
    try:
        r = sum([int(x) * sequence[i % 2] for i, x in enumerate(val[:-1])])
        ck = (10 - r % 10) % 10
        return ck == int(val[-1])
    except ValueError:
        return False

is_ean8(val)

Test if argument is a International Article Number (EAN-8).

Source code in idutils/validators.py
def is_ean8(val):
    """Test if argument is a International Article Number (EAN-8)."""
    if len(val) != 8:
        return False
    sequence = [3, 1]
    try:
        r = sum([int(x) * sequence[i % 2] for i, x in enumerate(val[:-1])])
        ck = (10 - r % 10) % 10
        return ck == int(val[-1])
    except ValueError:
        return False

is_email(val)

Test if argument looks like an email address.

Note this test is designed to distinguish an email from other identifier schemes only. It does not imply a valid address / domain etc.

Source code in idutils/validators.py
def is_email(val):
    """Test if argument looks like an email address.

    Note this test is designed to distinguish an email from other identifier
    schemes only. It does not imply a valid address / domain etc.
    """
    return email_regexp.match(val)

is_ensembl(val)

Test if argument is an Ensembl accession.

Source code in idutils/validators.py
def is_ensembl(val):
    """Test if argument is an Ensembl accession."""
    return ensembl_regexp.match(val)

is_genome(val)

Test if argument is a GenBank or RefSeq genome assembly accession.

Source code in idutils/validators.py
def is_genome(val):
    """Test if argument is a GenBank or RefSeq genome assembly accession."""
    return genome_regexp.match(val)

is_geo(val)

Test if argument is a Gene Expression Omnibus (GEO) accession.

Source code in idutils/validators.py
def is_geo(val):
    """Test if argument is a Gene Expression Omnibus (GEO) accession."""
    return geo_regexp.match(val)

is_gnd(val)

Test if argument is a GND Identifier.

Source code in idutils/validators.py
def is_gnd(val):
    """Test if argument is a GND Identifier."""
    return gnd_regexp.match(val)

is_hal(val)

Test if argument is a HAL identifier.

See (https://hal.archives-ouvertes.fr)

Source code in idutils/validators.py
def is_hal(val):
    """Test if argument is a HAL identifier.

    See (https://hal.archives-ouvertes.fr)
    """
    return hal_regexp.match(val)

is_handle(val)

Test if argument is a Handle.

Note, DOIs are also handles, and handle are very generic so they will also match e.g. any URL your parse.

Source code in idutils/validators.py
def is_handle(val):
    """Test if argument is a Handle.

    Note, DOIs are also handles, and handle are very generic so they will also
    match e.g. any URL your parse.
    """
    return handle_regexp.match(val) and not is_swh(val)

is_isbn(val)

Test if argument is an ISBN-10 or ISBN-13 number.

Source code in idutils/validators.py
def is_isbn(val):
    """Test if argument is an ISBN-10 or ISBN-13 number."""
    if is_isbn10(val) or is_isbn13(val):
        if val[0:3] in ["978", "979"] or not is_ean13(val):
            return True
    return False

is_isni(val)

Test if argument is an International Standard Name Identifier.

Source code in idutils/validators.py
def is_isni(val):
    """Test if argument is an International Standard Name Identifier."""
    val = val.replace("-", "").replace(" ", "").upper()
    if len(val) != 16:
        return False
    try:
        r = 0
        for x in val[:-1]:
            r = (r + int(x)) * 2
        ck = (12 - r % 11) % 11
        return ck == _convert_x_to_10(val[-1])
    except ValueError:
        return False

is_issn(val)

Test if argument is an ISSN number.

Source code in idutils/validators.py
def is_issn(val):
    """Test if argument is an ISSN number."""
    try:
        val = val.replace("-", "").replace(" ", "").upper()
        if len(val) != 8:
            return False
        r = sum([(8 - i) * (_convert_x_to_10(x)) for i, x in enumerate(val)])
        return not (r % 11)
    except ValueError:
        return False

is_istc(val)

Test if argument is a International Standard Text Code.

See http://www.istc-international.org/html/about_structure_syntax.aspx

Source code in idutils/validators.py
def is_istc(val):
    """Test if argument is a International Standard Text Code.

    See http://www.istc-international.org/html/about_structure_syntax.aspx
    """
    val = val.replace("-", "").replace(" ", "").upper()
    if len(val) != 16:
        return False
    sequence = [11, 9, 3, 1]
    try:
        r = sum([int(x, 16) * sequence[i % 4] for i, x in enumerate(val[:-1])])
        ck = hex(r % 16)[2:].upper()
        return ck == val[-1]
    except ValueError:
        return False

is_lsid(val)

Test if argument is a LSID.

Source code in idutils/validators.py
def is_lsid(val):
    """Test if argument is a LSID."""
    return is_urn(val) and lsid_regexp.match(val)

is_openalex(val)

Test if argument is an OpenAlex identifier.

See (https://docs.openalex.org/how-to-use-the-api/get-single-entities)

Source code in idutils/validators.py
def is_openalex(val):
    """Test if argument is an OpenAlex identifier.

    See (https://docs.openalex.org/how-to-use-the-api/get-single-entities)
    """
    return openalex_regexp.match(val)

is_orcid(val)

Test if argument is an ORCID ID.

See http://support.orcid.org/knowledgebase/ articles/116780-structure-of-the-orcid-identifier

Source code in idutils/validators.py
def is_orcid(val):
    """Test if argument is an ORCID ID.

    See http://support.orcid.org/knowledgebase/
        articles/116780-structure-of-the-orcid-identifier
    """
    for orcid_url in orcid_urls:
        if val.startswith(orcid_url):
            val = val[len(orcid_url) :]
            break

    val = val.replace("-", "").replace(" ", "")
    if is_isni(val):
        val = int(val[:-1], 10)  # Remove check digit and convert to int.
        return any(start <= val <= end for start, end in orcid_isni_ranges)
    return False

is_pmcid(val)

Test if argument is a PubMed Central ID.

Source code in idutils/validators.py
def is_pmcid(val):
    """Test if argument is a PubMed Central ID."""
    return pmcid_regexp.match(val)

is_pmid(val)

Test if argument is a PubMed ID.

Warning: PMID are just integers, with no structure, so this function will say any integer is a PubMed ID

Source code in idutils/validators.py
def is_pmid(val):
    """Test if argument is a PubMed ID.

    Warning: PMID are just integers, with no structure, so this function will
    say any integer is a PubMed ID
    """
    return pmid_regexp.match(val)

is_purl(val)

Test if argument is a PURL.

Source code in idutils/validators.py
def is_purl(val):
    """Test if argument is a PURL."""
    res = urlparse(val)
    purl_netlocs = [
        "purl.org",
        "purl.oclc.org",
        "purl.net",
        "purl.com",
        "purl.fdlp.gov",
    ]
    return (
        res.scheme in ["http", "https"]
        and res.netloc in purl_netlocs
        and res.path != ""
    )

is_raid(val)

Test if argument is a RAiD (Research Activity Identifier).

Note, RAiDs are issued as DOIs via DataCite and reuse the DOI "10.xxxx/yyyy" namespace, so a value may also validate as DOI/Handle. RAiD and DOI are format-identical; there is no format-level disambiguator.

See https://www.raid.org/

Source code in idutils/validators.py
def is_raid(val):
    """Test if argument is a RAiD (Research Activity Identifier).

    Note, RAiDs are issued as DOIs via DataCite and reuse the DOI
    "10.xxxx/yyyy" namespace, so a value may also validate as DOI/Handle.
    RAiD and DOI are format-identical; there is no format-level
    disambiguator.

    See https://www.raid.org/
    """
    return raid_regexp.match(val)

is_refseq(val)

Test if argument is a RefSeq accession.

Source code in idutils/validators.py
def is_refseq(val):
    """Test if argument is a RefSeq accession."""
    return refseq_regexp.match(val)

is_rfc3987_ipath_absolute(val)

Test if the argument is an from RFC 3987.

Source code in idutils/validators.py
def is_rfc3987_ipath_absolute(val):
    """Test if the argument is an <ipath-absolute> from RFC 3987."""
    return rfc3987_reg_exps["ipath_absolute"].fullmatch(val) is not None

is_rfc3987_iri(val)

Test if the argment is an from RFC 3987.

Source code in idutils/validators.py
def is_rfc3987_iri(val):
    """Test if the argment is an <iri> from RFC 3987."""
    return rfc3987_reg_exps["iri"].fullmatch(val) is not None

is_ror(val)

Test if argument is a ROR id.

Source code in idutils/validators.py
def is_ror(val):
    """Test if argument is a ROR id."""
    return ror_regexp.match(val)

is_rrid(val)

Test if argument is a RRID.

Source code in idutils/validators.py
def is_rrid(val):
    """Test if argument is a RRID."""
    return rrid_regexp.match(val)

is_sha1(val)

Test if argument is a valid SHA-1 (hex) hash.

Source code in idutils/validators.py
def is_sha1(val):
    """Test if argument is a valid SHA-1 (hex) hash."""
    return sha1_regexp.match(val)

is_sra(val)

Test if argument is an SRA accession.

Source code in idutils/validators.py
def is_sra(val):
    """Test if argument is an SRA accession."""
    return sra_regexp.match(val)

is_swh(val)

Test if argument is a Software Heritage identifier.

https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html#syntax

Source code in idutils/validators.py
def is_swh(val):
    """Test if argument is a Software Heritage identifier.

    https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html#syntax
    """
    m = swh_before_qualifiers_regexp.match(val)
    if m is not None:
        qualifiers = m.group("qualifiers")
        if qualifiers is None:
            return True
        else:
            qualifiers = str(qualifiers)[1:]  # remove the first semi-colon
            qualifiers = qualifiers.split(";")  # split by semi-colon
            for qualifier in qualifiers:
                m = swh_qualifier_values_regexp.match(qualifier)
                if m is None:
                    return False
                else:
                    qualifier_dict = m.groupdict()

                    # origin value must be IRI according to RFC 3987
                    origin_value = qualifier_dict["origin_value"]
                    if origin_value is not None and not is_rfc3987_iri(
                        str(origin_value)
                    ):
                        return False

                    # path value must be an <ipath-absolute>
                    path_value = qualifier_dict["path_value"]
                    if path_value is not None and not is_rfc3987_ipath_absolute(
                        str(path_value)
                    ):
                        return False
            return True
    return False

is_uniprot(val)

Test if argument is a UniProt accession.

Source code in idutils/validators.py
def is_uniprot(val):
    """Test if argument is a UniProt accession."""
    return uniprot_regexp.match(val)

is_url(val)

Test if argument is a URL.

Source code in idutils/validators.py
def is_url(val):
    """Test if argument is a URL."""
    res = urlparse(val)
    return bool(res.scheme and res.netloc)

is_urn(val)

Test if argument is an URN.

Source code in idutils/validators.py
def is_urn(val):
    """Test if argument is an URN."""
    res = urlparse(val)
    return bool(res.scheme == "urn" and res.netloc == "" and res.path != "")

is_viaf(val)

Test if argument is a VIAF id.

Source code in idutils/validators.py
def is_viaf(val):
    """Test if argument is a VIAF id."""
    for viaf_url in viaf_urls:
        if val.startswith(viaf_url):
            return True
    res = viaf_regexp.match(val)
    if res:
        return viaf_regexp.match(val).group() == val
    else:
        return False

is_wikidata(val)

Test if argument is a wikidata QID.

Source code in idutils/validators.py
def is_wikidata(val):
    """Test if argument is a wikidata QID."""
    return qid_regexp.match(val)

idutils.normalizers

ID normalizer helper functions.

normalize_ads(val)

Normalize an ADS bibliographic code.

Source code in idutils/normalizers.py
def normalize_ads(val):
    """Normalize an ADS bibliographic code."""
    val = unicodedata.normalize("NFKD", val)
    m = ads_regexp.match(val)
    return m.group(2)

normalize_arxiv(val)

Normalize an arXiv identifier.

Source code in idutils/normalizers.py
def normalize_arxiv(val):
    """Normalize an arXiv identifier."""
    if not val.lower().startswith("arxiv:"):
        val = "arXiv:{0}".format(val)
    elif val[:6] != "arXiv:":
        val = "arXiv:{0}".format(val[6:])

    # Normalize old identifiers to preferred scheme as specified by
    # http://arxiv.org/help/arxiv_identifier_for_services
    # (i.e. arXiv:math.GT/0309136 -> arXiv:math/0309136)
    m = is_arxiv_pre_2007(val)
    if m and m.group(3):
        val = "".join(m.group(1, 2, 4, 5))
        if m.group(6):
            val += m.group(6)

    m = is_arxiv_post_2007(val)
    if m:
        val = "arXiv:" + ".".join(m.group(2, 3))
        if m.group(4):
            val += m.group(4)
    return val

normalize_doi(val)

Normalize a DOI.

Source code in idutils/normalizers.py
def normalize_doi(val):
    """Normalize a DOI."""
    m = doi_regexp.match(val)
    return m.group(2)

normalize_gnd(val)

Normalize a GND identifier.

Source code in idutils/normalizers.py
def normalize_gnd(val):
    """Normalize a GND identifier."""
    m = gnd_regexp.match(val)
    return f"gnd:{m.group(2)}"

normalize_hal(val)

Normalize a HAL identifier.

Source code in idutils/normalizers.py
def normalize_hal(val):
    """Normalize a HAL identifier."""
    val = val.replace(" ", "").lower().replace("hal:", "")
    return val

normalize_handle(val)

Normalize a Handle identifier.

Source code in idutils/normalizers.py
def normalize_handle(val):
    """Normalize a Handle identifier."""
    m = handle_regexp.match(val)
    return m.group(2)

normalize_isbn(val)

Normalize an ISBN identifier.

Also converts ISBN10 to ISBN13.

Source code in idutils/normalizers.py
def normalize_isbn(val):
    """Normalize an ISBN identifier.

    Also converts ISBN10 to ISBN13.
    """
    if is_isbn10(val):
        val = isbnlib.to_isbn13(val)
    return isbnlib.mask(isbnlib.canonical(val))

normalize_issn(val)

Normalize an ISSN identifier.

Source code in idutils/normalizers.py
def normalize_issn(val):
    """Normalize an ISSN identifier."""
    val = val.replace(" ", "").replace("-", "").strip().upper()
    return "{0}-{1}".format(val[:4], val[4:])

normalize_openalex(val)

Normalize an OpenAlex identifier.

Source code in idutils/normalizers.py
def normalize_openalex(val):
    """Normalize an OpenAlex identifier."""
    m = openalex_regexp.match(val)
    return m.group(2).upper()

normalize_orcid(val)

Normalize an ORCID identifier.

Source code in idutils/normalizers.py
def normalize_orcid(val):
    """Normalize an ORCID identifier."""
    for orcid_url in orcid_urls:
        if val.startswith(orcid_url):
            val = val[len(orcid_url) :]
            break
    val = val.replace("-", "").replace(" ", "")

    return "-".join([val[0:4], val[4:8], val[8:12], val[12:16]])

normalize_pid(val, scheme)

Normalize an identifier.

E.g. doi:10.1234/foo and http://dx.doi.org/10.1234/foo and 10.1234/foo will all be normalized to 10.1234/foo.

Source code in idutils/normalizers.py
def normalize_pid(val, scheme):
    """Normalize an identifier.

    E.g. doi:10.1234/foo and http://dx.doi.org/10.1234/foo and 10.1234/foo
    will all be normalized to 10.1234/foo.
    """
    if not val:
        return val

    if scheme == "doi":
        return normalize_doi(val)
    elif scheme == "handle":
        return normalize_handle(val)
    elif scheme == "ads":
        return normalize_ads(val)
    elif scheme == "pmid":
        return normalize_pmid(val)
    elif scheme == "arxiv":
        return normalize_arxiv(val)
    elif scheme == "orcid":
        return normalize_orcid(val)
    elif scheme == "gnd":
        return normalize_gnd(val)
    elif scheme == "isbn":
        return normalize_isbn(val)
    elif scheme == "issn":
        return normalize_issn(val)
    elif scheme == "hal":
        return normalize_hal(val)
    elif scheme == "ror":
        return normalize_ror(val)
    elif scheme == "urn":
        return normalize_urn(val)
    elif scheme == "viaf":
        return normalize_viaf(val)
    elif scheme == "wikidata":
        return normalize_qid(val)
    elif scheme == "openalex":
        return normalize_openalex(val)
    elif scheme == "raid":
        return normalize_raid(val)
    else:
        for custom_scheme, normalizer in custom_schemes_registry().pick_scheme_key(
            "normalizer"
        ):
            if scheme == custom_scheme:
                return normalizer(val)
    return val

normalize_pmid(val)

Normalize a PubMed ID.

Source code in idutils/normalizers.py
def normalize_pmid(val):
    """Normalize a PubMed ID."""
    m = pmid_regexp.match(val)
    return m.group(2)

normalize_qid(val)

Normalize a wikidata QID.

Source code in idutils/normalizers.py
def normalize_qid(val):
    """Normalize a wikidata QID."""
    m = qid_regexp.match(val)
    return m.group(2)

normalize_raid(val)

Normalize a RAiD.

Source code in idutils/normalizers.py
def normalize_raid(val):
    """Normalize a RAiD."""
    m = raid_regexp.match(val)
    return m.group(2)

normalize_ror(val)

Normalize a ROR.

Source code in idutils/normalizers.py
def normalize_ror(val):
    """Normalize a ROR."""
    m = ror_regexp.match(val)
    return m.group(1)

normalize_urn(val)

Normalize a URN.

Source code in idutils/normalizers.py
def normalize_urn(val):
    """Normalize a URN."""
    if val.startswith(urn_resolver_url):
        val = val[len(urn_resolver_url) :]
    if val.lower().startswith("urn:"):
        val = val[len("urn:") :]
    return "urn:{0}".format(val)

normalize_viaf(val)

Normalize a VIAF identifier.

Source code in idutils/normalizers.py
def normalize_viaf(val):
    """Normalize a VIAF identifier."""
    for viaf_url in viaf_urls:
        if val.startswith(viaf_url):
            val = val[len(viaf_url) :]
            break
    if val.lower().startswith("viaf:"):
        val = val[len("viaf:") :]
    return "viaf:{0}".format(val)

to_url(val, scheme, url_scheme='http')

Convert a resolvable identifier into a URL for a landing page.

Parameters:

Name Type Description Default
val

The identifier's value.

required
scheme

The identifier's scheme.

required
url_scheme

Scheme to use for URL generation, 'http' or 'https'.

'http'

Returns:

Type Description

URL for the identifier.

New in version 0.3.0

url_scheme used for URL generation.

Source code in idutils/normalizers.py
def to_url(val, scheme, url_scheme="http"):
    """Convert a resolvable identifier into a URL for a landing page.

    Args:
        val: The identifier's value.
        scheme: The identifier's scheme.
        url_scheme: Scheme to use for URL generation, 'http' or 'https'.

    Returns:
        URL for the identifier.

    !!! info "New in version 0.3.0"
        `url_scheme` used for URL generation.
    """
    pid = normalize_pid(val, scheme)
    landing_urls = IDUTILS_LANDING_URLS
    if scheme in landing_urls:
        if scheme == "gnd" and pid.startswith("gnd:"):
            pid = pid[len("gnd:") :]
        if scheme == "urn" and not pid.lower().startswith("urn:nbn:"):
            return ""
        if scheme == "ascl":
            pid = val.split(":")[1]
        if scheme == "viaf" and pid.startswith("viaf:"):
            pid = pid[len("viaf:") :]
            url_scheme = "https"
        if scheme == "wikidata":
            url_scheme = "https"
            if pid.startswith("wikidata:"):
                pid = pid[len("wikidata:") :]
        if scheme == "openalex":
            url_scheme = "https"
        return landing_urls[scheme].format(scheme=url_scheme, pid=pid)
    elif scheme in ["purl", "url"]:
        return pid
    else:
        for custom_scheme, url_generator in custom_schemes_registry().pick_scheme_key(
            "url_generator"
        ):
            if scheme == custom_scheme:
                return url_generator(url_scheme, pid)

    return ""