diff --git a/src/nodenorm/handlers/semantic_types.py b/src/nodenorm/handlers/semantic_types.py index 79df56c..c3bd974 100644 --- a/src/nodenorm/handlers/semantic_types.py +++ b/src/nodenorm/handlers/semantic_types.py @@ -28,10 +28,20 @@ async def get(self) -> dict: ) raise network_error from gen_exc + # The aggregation's bucket keys are the indexed keyword values of `type`, which the + # index mapping lowercases ("biolink:smallmolecule"), unlike the `_source` that + # /get_normalized_nodes reads. Don't add the keys directly: look each one up in the + # Biolink model (bmt matches case-insensitively) and report its `class_uri` + # ("biolink:SmallMolecule"). A key the model doesn't know is reported as is. + # + # These lookups don't need memoizing here: bmt's Toolkit wraps get_element() and + # get_ancestors() in functools.lru_cache (1024 entries, more than the model's + # classes), so after the first request the loop costs about a millisecond. semantic_types = set() for bucket in type_aggregation_result.body["aggregations"]["unique_types"]["buckets"]: biolink_type = bucket["key"] - semantic_types.add(biolink_type) + element = toolkit.get_element(biolink_type) + semantic_types.add(element["class_uri"] if element else biolink_type) for ancestor in toolkit.get_ancestors(biolink_type): semantic_types.add(toolkit.get_element(ancestor)["class_uri"]) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 131a2fa..3493e54 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -271,7 +271,8 @@ def __init__(self): async def search(self, **kwargs): self.calls.append(kwargs) - return SimpleNamespace(body={"aggregations": {"unique_types": {"buckets": [{"key": "biolink:Disease"}]}}}) + # Real indices lowercase the `type` keyword, so aggregation keys come back lowercased. + return SimpleNamespace(body={"aggregations": {"unique_types": {"buckets": [{"key": "biolink:disease"}]}}}) async def msearch(self, **kwargs): self.calls.append(kwargs) @@ -356,7 +357,8 @@ def test_status_matches_current_metadata_schema(self): class TestSemanticTypes(AsyncHTTPTestCase): - """Regression: the handler lower-cased every class ("biolink:namedthing").""" + """Regression: the handler lower-cased every class ("biolink:namedthing"), and then + reported the lowercased aggregation key next to its capitalised class_uri.""" def get_app(self) -> tornado.web.Application: app = tornado.web.Application([(r"/get_semantic_types", SemanticTypeHandler)]) @@ -374,6 +376,7 @@ def test_types_are_biolink_class_curies(self): types = body["semantic_types"]["types"] assert "biolink:Disease" in types and "biolink:NamedThing" in types assert all(re.fullmatch(r"biolink:[A-Z][A-Za-z]*", t) for t in types), types + assert len({t.lower() for t in types}) == len(types), types class UnreachableElasticsearch: