Skip to content

Commit

Permalink
Show provenance for annotations in web app (#1095)
Browse files Browse the repository at this point in the history
This is a proof of concept showing that we can propagate information
about _where_ a field displayed on the Bioregistry came from. This is
important because the Bioregistry imports data from other registries, so
it's useful to know where an annotation (e.g., the name associated with
a prefix) came from, and under what license it's being reused.
  • Loading branch information
cthoyt authored Feb 14, 2025
1 parent 79f405f commit 38f25fe
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 110 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ignore =
W503
# no quotes in strings
B028
E704
exclude =
.tox,
.git,
Expand Down
43 changes: 34 additions & 9 deletions src/bioregistry/app/templates/resource.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% import "macros.html" as utils %}

{% block title %}{{ config.METAREGISTRY_TITLE }} - {{ name }}{% endblock %}
{% block title %}{{ config.METAREGISTRY_TITLE }} - {{ name_pack.value }}{% endblock %}

{% block styles %}
{{ super() }}
Expand Down Expand Up @@ -57,7 +57,10 @@
<div class="row align-items-center">
<div class="col-8">
<h5 style="margin: 0"><a href="{{ url_for("metaregistry_ui.resources") }}">Registry</a> <i
class="fas fa-angle-right"></i> {{ name }}
class="fas fa-angle-right"></i> {{ name_pack.value }}
<a type="button" data-toggle="modal" data-target="#name-modal">
<i class="far fa-question-circle"></i>
</a>
{{ utils.render_resource_warnings(resource) }}
</h5>
</div>
Expand All @@ -79,7 +82,7 @@ <h5 style="margin: 0"><a href="{{ url_for("metaregistry_ui.resources") }}">Regis
</div>
</div>
<a class="btn btn-sm btn-outline-dark" style="margin-right: 0.5em"
href="https://github.com/biopragmatics/bioregistry/issues/new?labels=Update&template=update-misc.yml&prefix={{ prefix }}&title=Update+{{ name }}">
href="https://github.com/biopragmatics/bioregistry/issues/new?labels=Update&template=update-misc.yml&prefix={{ prefix }}&title=Update+{{ name_pack.value }}">
Suggest
</a>
</div>
Expand All @@ -90,7 +93,7 @@ <h5 style="margin: 0"><a href="{{ url_for("metaregistry_ui.resources") }}">Regis
{% if description %}
<p>
{% if resource.get_logo() %}
<img style="max-height: 70px; float: right;" src="{{ resource.get_logo() }}" alt="Logo for {{ resource.get_name() }}"/>
<img style="max-height: 70px; float: right;" src="{{ resource.get_logo() }}" alt="Logo for {{ name_pack.value }}"/>
{% endif %}
{{ description }}
</p>
Expand Down Expand Up @@ -264,7 +267,7 @@ <h5 style="margin: 0"><a href="{{ url_for("metaregistry_ui.resources") }}">Regis
<dd>
{% if pattern %}
<p>
Local identifiers in {{ name }} should match this
Local identifiers in {{ name_pack.value }} should match this
regular expression:<br/><kbd>{{ pattern }}</kbd>
</p>
{% elif has_no_terms %}
Expand Down Expand Up @@ -299,7 +302,7 @@ <h5 style="margin: 0"><a href="{{ url_for("metaregistry_ui.resources") }}">Regis
<dt>Pattern for CURIES</dt>
<dd>
<p>
Compact URIs (CURIEs) constructed from {{ name }} should match
Compact URIs (CURIEs) constructed from {{ name_pack.value }} should match
this regular expression:<br/><kbd>{{ curie_pattern }}</kbd>
</p>
</dd>
Expand Down Expand Up @@ -502,7 +505,7 @@ <h5 class="card-header">Ontology</h5>
<div class="card" style="margin-top: 20px">
<a id="mappings"></a>
<h5 class="card-header">
Metaregistry <i class="fas fa-angle-right"></i> {{ name }}
Metaregistry <i class="fas fa-angle-right"></i> {{ name_pack.value }}
</h5>
<div class="card-body">
<p>
Expand Down Expand Up @@ -547,7 +550,7 @@ <h5 class="card-header">
{# <td>{{ resource.get_prefix_key("name", mapping.metaprefix) or "" }}</td> #}
<td style="text-align: center;">
<a data-toggle="tooltip" data-html="true"
title="Report misalignment between {{ name }} and <code>{{ mapping.metaprefix }}:{{ mapping.xref }}</code>"
title="Report misalignment between {{ name_pack.value }} and <code>{{ mapping.metaprefix }}:{{ mapping.xref }}</code>"
href="https://github.com/biopragmatics/bioregistry/issues/new?labels=Update%2CPrefix&template=mismatch.yml&title=Remove+incorrect+mapping+from+{{ resource.prefix }}+to+%60{{ mapping.metaprefix }}%3A{{ mapping.xref }}%60&prefix={{ resource.prefix }}&metaprefix={{ mapping.metaprefix }}&external-prefix={{ mapping.xref }}">
<i class="fas fa-pen"></i>
</a>
Expand Down Expand Up @@ -610,7 +613,7 @@ <h5 style="margin: 0">Providers</h5>
{% if providers %}
<p>
The local unique identifier <code>{{ example }}</code> is used to demonstrate the providers
available for {{ name }}. A guide for curating additional providers can be found
available for {{ name_pack.value }}. A guide for curating additional providers can be found
<a href="https://biopragmatics.github.io/bioregistry/curation/providers">here</a>.
</p>
{% else %}
Expand Down Expand Up @@ -703,6 +706,28 @@ <h5 class="modal-title" id="versionModalLabel">Programmatic Access to Version</h
</div>
{% endif %}

<div class="modal fade" id="name-modal" tabindex="-1" role="dialog" aria-labelledby="nameModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="nameModalLabel">Programmatic Access to Name</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
This name annotation was originally annotated in
<a href="{{ url_for("metaregistry_ui.metaresource", metaprefix=name_pack.metaprefix) }}">{{ name_pack.name }}</a>
and is redistributed under the {{ name_pack.license }} license.
</p>
<p>Get the name:</p>
{{ utils.code_example(prefix, "get_name", name_pack.value) }}
</div>
</div>
</div>
</div>

{% if resource_license %}
<div class="modal fade" id="license-modal" tabindex="-1" role="dialog" aria-labelledby="licenseModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
Expand Down
3 changes: 2 additions & 1 deletion src/bioregistry/app/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,14 @@ def resource(prefix: str) -> str | flask.Response:
example_curie_extras = [
_resource.get_curie(example_extra, use_preferred=True) for example_extra in example_extras
]
name_pack = manager._repack(_resource.get_name(provenance=True))
return render_template(
"resource.html",
zip=zip,
prefix=prefix,
resource=_resource,
bioschemas=json.dumps(_resource.get_bioschemas_jsonld(), ensure_ascii=False),
name=manager.get_name(prefix),
name_pack=name_pack,
example=example,
example_extras=example_extras,
example_curie=example_curie,
Expand Down
37 changes: 27 additions & 10 deletions src/bioregistry/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import typing
from collections.abc import Mapping
from functools import lru_cache
from typing import Any
from typing import Any, Literal, overload

import curies

from .constants import MaybeCURIE
from .resource_manager import manager
from .resource_manager import MetaresourceAnnotatedValue, manager
from .schema import Attributable, Resource

__all__ = [
Expand Down Expand Up @@ -82,9 +82,25 @@ def get_resource(prefix: str) -> Resource | None:
return manager.get_resource(prefix)


def get_name(prefix: str) -> str | None:
"""Get the name for the given prefix, it it's available."""
return manager.get_name(prefix)
# docstr-coverage:excused `overload`
@overload
def get_name(prefix: str, *, provenance: Literal[False] = False) -> None | str: ...


# docstr-coverage:excused `overload`
@overload
def get_name(
prefix: str, *, provenance: Literal[True] = True
) -> None | MetaresourceAnnotatedValue[str]: ...


def get_name(
prefix: str, *, provenance: bool = False
) -> str | MetaresourceAnnotatedValue[str] | None:
"""Get the name for the given prefix, if it's available."""
if provenance:
return manager.get_name(prefix, provenance=True)
return manager.get_name(prefix, provenance=False)


def get_description(prefix: str, *, use_markdown: bool = False) -> str | None:
Expand Down Expand Up @@ -160,12 +176,13 @@ def get_pattern(prefix: str) -> str | None:
return manager.get_pattern(prefix)


def get_namespace_in_lui(prefix: str) -> bool | None:
def get_namespace_in_lui(
prefix: str, *, provenance: bool = False
) -> bool | MetaresourceAnnotatedValue[bool] | None:
"""Check if the namespace should appear in the LUI."""
entry = get_resource(prefix)
if entry is None:
return None
return entry.get_namespace_in_lui()
if provenance:
return manager.get_namespace_in_lui(prefix, provenance=True)
return manager.get_namespace_in_lui(prefix, provenance=False)


def get_appears_in(prefix: str) -> list[str] | None:
Expand Down
101 changes: 98 additions & 3 deletions src/bioregistry/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
import typing
from collections import Counter, defaultdict
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from functools import cache
from pathlib import Path
from typing import (
Any,
Callable,
Generic,
Literal,
TypeVar,
Union,
cast,
overload,
)

import curies
Expand Down Expand Up @@ -39,6 +45,7 @@
Resource,
sanitize_model,
)
from .schema.struct import MetaprefixAnnotatedValue
from .schema_utils import (
_collections_from_path,
_contexts_from_path,
Expand All @@ -56,6 +63,31 @@

logger = logging.getLogger(__name__)

X = TypeVar("X", bound=Union[int, str])


@dataclass
class MetaresourceAnnotatedValue(Generic[X]):
"""A pack for a value that has extra information."""

value: X
registry: Registry

@property
def metaprefix(self) -> str:
"""Get prefix for the source registry for the annotation."""
return self.registry.prefix

@property
def name(self) -> str:
"""Get the name of the source registry for the annotation."""
return self.registry.name

@property
def license(self) -> str:
"""Get the license for the annotation."""
return self.registry.license or "unknown"


def _synonym_to_canonical(registry: Mapping[str, Resource]) -> NormDict:
"""Return a mapping from several variants of each synonym to the canonical namespace."""
Expand Down Expand Up @@ -505,12 +537,75 @@ def get_uri_prefix(self, prefix: str, priority: Sequence[str] | None = None) ->
return None
return entry.get_uri_prefix(priority=priority)

def get_name(self, prefix: str) -> str | None:
"""Get the name for the given prefix, it it's available."""
# docstr-coverage:excused `overload`
@overload
def _repack(self, obj: None) -> None: ...

# docstr-coverage:excused `overload`
@overload
def _repack(self, obj: X) -> X: ...

# docstr-coverage:excused `overload`
@overload
def _repack(self, obj: MetaprefixAnnotatedValue[X]) -> MetaresourceAnnotatedValue[X]: ...

def _repack(
self, obj: None | X | MetaprefixAnnotatedValue[X]
) -> MetaresourceAnnotatedValue[X] | X | None:
if obj is None:
return None
elif isinstance(obj, MetaprefixAnnotatedValue):
mp = self.get_registry(obj.metaprefix)
if mp is None:
raise ValueError
return MetaresourceAnnotatedValue(obj.value, mp)
else:
return obj

# docstr-coverage:excused `overload`
@overload
def get_name(self, prefix: str, *, provenance: Literal[False] = False) -> str | None: ...

# docstr-coverage:excused `overload`
@overload
def get_name(
self, prefix: str, *, provenance: Literal[True] = True
) -> MetaresourceAnnotatedValue[str] | None: ...

def get_name(
self, prefix: str, *, provenance: bool = False
) -> str | MetaresourceAnnotatedValue[str] | None:
"""Get the name for the given prefix, if it's available."""
entry = self.get_resource(prefix)
if entry is None:
return None
if provenance:
_tmp = entry.get_name(provenance=True)
return self._repack(_tmp)
return entry.get_name(provenance=False)

# docstr-coverage:excused `overload`
@overload
def get_namespace_in_lui(
self, prefix: str, *, provenance: Literal[False] = False
) -> bool | None: ...

# docstr-coverage:excused `overload`
@overload
def get_namespace_in_lui(
self, prefix: str, *, provenance: Literal[True] = True
) -> None | MetaresourceAnnotatedValue[bool]: ...

def get_namespace_in_lui(
self, prefix: str, *, provenance: bool = False
) -> None | bool | MetaresourceAnnotatedValue[bool]:
"""Get the name for the given prefix, if it's available."""
entry = self.get_resource(prefix)
if entry is None:
return None
return entry.get_name()
if provenance:
return self._repack(entry.get_namespace_in_lui(provenance=True))
return entry.get_namespace_in_lui(provenance=False)

def get_description(self, prefix: str, *, use_markdown: bool = False) -> str | None:
"""Get the description for the given prefix, it it's available."""
Expand Down
Loading

0 comments on commit 38f25fe

Please sign in to comment.