diff --git a/.gitignore b/.gitignore
index 43d02de..e01d077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,11 @@ notebooks/PathFinder_testing_results/
notebooks/Test/
dist/
notebooks/PathFinder_results/
+
+# opencode artifacts
+/.opencode/
+/AGENTS.md
+/.logs/
+
+# agent-of-empires artifacts
+/.agent-of-empires/
diff --git a/README.md b/README.md
index 202cf1e..9e91b08 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Explore knowledge graphs in Translator
Find neighbors in the Translator KGs for a given node
Find paths between node A and node B in the Translator KG
Find a subnetwork given a list of nodes in the Translator KG
+Experimental developer-friendly wrappers for resolving labels/CURIEs, caching Translator resources, and returning parsed finder results
Connecting user's API with Translator API
@@ -84,6 +85,11 @@ Example notebook for **[PathFinder](https://github.com/NCATSTranslator/Translato
#### Network finder
Example notebook for **[NetworkFinder](https://github.com/NCATSTranslator/Translator_component_toolkit/blob/main/notebooks/Neighborhood_finder_multiple_nodes.ipynb)**
+#### Experimental API tutorial
+Quick-start notebook for the experimental developer-friendly finder wrappers: **[Experimental API tutorial](https://github.com/NCATSTranslator/Translator_component_toolkit/blob/main/notebooks/Experimental_API_tutorial.ipynb)**
+
+Use the detailed NeighborhoodFinder, PathFinder, NetworkFinder, KG overview, and visualization notebooks above when you need more fine-grained endpoint selection, predicate control, raw query construction, parser workflows, or visualization setup.
+
#### Connecting to a user's API
API should be developed following the standard from [TRAPI](https://github.com/NCATSTranslator/ReasonerAPI).
diff --git a/TCT/experimental.py b/TCT/experimental.py
new file mode 100644
index 0000000..234c156
--- /dev/null
+++ b/TCT/experimental.py
@@ -0,0 +1,598 @@
+"""
+Experimental developer-friendly APIs for common Translator finder patterns.
+
+This module provides higher-level wrappers around the existing pathfinder and
+neighborhood finder code. The wrappers resolve human-readable names, normalize
+CURIEs, load Translator resources lazily, and return a small result object with
+the most useful output fields surfaced directly.
+"""
+
+from dataclasses import dataclass
+from typing import Any, Optional, TypeAlias, Union
+
+import pandas as pd
+
+from . import name_resolver, node_normalizer, translator_query
+from .TCT import sele_predicates_API
+from .TCT_neighborhood_finder import (
+ parse_results_for_neighborhood_finder,
+ parse_results_for_neighborhood_finder_multiple_inputs,
+)
+from .TCT_pathfinder import parse_results_for_pathfinder
+
+
+NodeInput: TypeAlias = str
+CategoryInput: TypeAlias = str
+CategoryList: TypeAlias = list[CategoryInput]
+
+
+@dataclass(frozen=True)
+class ResolvedNode:
+ """Resolved node metadata used by the experimental finder APIs."""
+
+ input_value: str
+ curie: str
+ label: Optional[str]
+ categories: list[str]
+
+
+@dataclass
+class TranslatorResources:
+ """Translator API metadata required by the legacy query functions."""
+
+ api_names: dict[str, str]
+ meta_kg: pd.DataFrame
+ api_predicates: dict[str, list[str]]
+
+
+@dataclass
+class FinderResult:
+ """Convenience wrapper around a parsed TRAPI-style finder response."""
+
+ query: dict[str, Any]
+ knowledge_graph: dict[str, Any]
+ results: list[dict[str, Any]]
+ auxiliary_graphs: dict[str, Any]
+ resolved_nodes: dict[str, ResolvedNode]
+ raw: dict[str, Any]
+
+ def to_dict(self) -> dict[str, Any]:
+ """
+ Return the raw parsed TRAPI-style output dictionary.
+
+ Returns
+ -------
+ dict
+ Full parsed output generated by the existing finder parser.
+
+ Examples
+ --------
+ >>> result = FinderResult({}, {}, [], {}, {}, {})
+ >>> result.to_dict()
+ {}
+ """
+ return self.raw
+
+
+_DEFAULT_TRANSLATOR_RESOURCES: Optional[TranslatorResources] = None
+
+
+def get_translator_resources(*, refresh: bool = False) -> TranslatorResources:
+ """
+ Return cached Translator API metadata, loading it on first use.
+
+ Parameters
+ ----------
+ refresh : bool
+ If true, refetch SmartAPI/MetaKG data even when the singleton is
+ already populated.
+
+ Returns
+ -------
+ TranslatorResources
+ API names, MetaKG dataframe, and API predicate mapping used by the
+ legacy query functions.
+
+ Examples
+ --------
+ >>> resources = get_translator_resources()
+ >>> paths = pathfinder("asthma", "albuterol", ["Gene"], resources=resources)
+ """
+ global _DEFAULT_TRANSLATOR_RESOURCES
+ if refresh or _DEFAULT_TRANSLATOR_RESOURCES is None:
+ api_names, meta_kg, api_predicates = (
+ translator_query.get_translator_API_predicates()
+ )
+ _DEFAULT_TRANSLATOR_RESOURCES = TranslatorResources(
+ api_names=api_names,
+ meta_kg=meta_kg,
+ api_predicates=api_predicates,
+ )
+ return _DEFAULT_TRANSLATOR_RESOURCES
+
+
+def clear_translator_resource_cache() -> None:
+ """
+ Clear the in-memory Translator resource singleton.
+
+ Examples
+ --------
+ >>> clear_translator_resource_cache()
+ >>> resources = get_translator_resources() # refetches
+ """
+ global _DEFAULT_TRANSLATOR_RESOURCES
+ _DEFAULT_TRANSLATOR_RESOURCES = None
+
+
+def pathfinder(
+ start: NodeInput,
+ end: NodeInput,
+ intermediate_categories: CategoryList,
+ *,
+ start_categories: Optional[CategoryList] = None,
+ end_categories: Optional[CategoryList] = None,
+ api_names: Optional[dict[str, str]] = None,
+ meta_kg: Optional[pd.DataFrame] = None,
+ api_predicates: Optional[dict[str, list[str]]] = None,
+ resources: Optional[TranslatorResources] = None,
+ scoring_method: str = "infores",
+ name_resolver_kwargs: Optional[dict[str, Any]] = None,
+ node_normalizer_kwargs: Optional[dict[str, Any]] = None,
+) -> FinderResult:
+ """
+ Find paths between two biomedical concepts using Translator KPs.
+
+ Parameters
+ ----------
+ start : str
+ Start node as either a CURIE (for example, ``"MONDO:0004979"``) or a
+ human-readable string (for example, ``"asthma"``).
+ end : str
+ End node as either a CURIE or human-readable string.
+ intermediate_categories : list[str]
+ Allowed categories for intermediate path nodes. Values may be short
+ names like ``"Gene"`` or full Biolink names like ``"biolink:Gene"``.
+ start_categories : list[str], optional
+ Category override for the start node. If omitted, categories are
+ inferred from Node Normalizer.
+ end_categories : list[str], optional
+ Category override for the end node. If omitted, categories are inferred
+ from Node Normalizer.
+ resources : TranslatorResources, optional
+ Preloaded Translator resources. If omitted, the module-level singleton
+ is loaded on first use and reused.
+ api_names, meta_kg, api_predicates : optional
+ Advanced partial overrides for the Translator resources used by the
+ legacy pathfinder implementation.
+ scoring_method : str
+ Scoring method passed to the legacy parser. Current values are
+ ``"infores"`` and ``"edges"``.
+ name_resolver_kwargs : dict, optional
+ Extra keyword arguments for ``name_resolver.lookup``.
+ node_normalizer_kwargs : dict, optional
+ Extra keyword arguments for ``node_normalizer.get_normalized_nodes``.
+
+ Returns
+ -------
+ FinderResult
+ Convenience wrapper containing resolved input nodes, the parsed
+ knowledge graph, results, auxiliary graphs, and the raw TRAPI-style
+ output dictionary.
+
+ Examples
+ --------
+ >>> from TCT.experimental import pathfinder
+ >>> result = pathfinder("asthma", "albuterol", ["Gene"])
+ >>> result.resolved_nodes["start"].curie
+ 'MONDO:0004979'
+ """
+ start_node = _resolve_node(
+ start,
+ name_resolver_kwargs=name_resolver_kwargs,
+ node_normalizer_kwargs=node_normalizer_kwargs,
+ )
+ end_node = _resolve_node(
+ end,
+ name_resolver_kwargs=name_resolver_kwargs,
+ node_normalizer_kwargs=node_normalizer_kwargs,
+ )
+ intermediate_categories = _normalize_categories(intermediate_categories) or []
+ start_categories = _normalize_categories(start_categories) or start_node.categories
+ end_categories = _normalize_categories(end_categories) or end_node.categories
+ resolved_resources = _get_resources(
+ resources=resources,
+ api_names=api_names,
+ meta_kg=meta_kg,
+ api_predicates=api_predicates,
+ )
+
+ predicates1, apis1, _ = sele_predicates_API(
+ start_categories,
+ intermediate_categories,
+ resolved_resources.meta_kg,
+ resolved_resources.api_names,
+ )
+ predicates2, apis2, _ = sele_predicates_API(
+ intermediate_categories,
+ end_categories,
+ resolved_resources.meta_kg,
+ resolved_resources.api_names,
+ )
+ query1 = translator_query.format_query_json(
+ [start_node.curie],
+ [],
+ start_categories,
+ intermediate_categories,
+ predicates1,
+ )
+ query2 = translator_query.format_query_json(
+ [],
+ [end_node.curie],
+ intermediate_categories,
+ end_categories,
+ predicates2,
+ )
+ result1 = translator_query.parallel_api_query(
+ query_json=query1,
+ select_APIs=apis1,
+ APInames=resolved_resources.api_names,
+ API_predicates=resolved_resources.api_predicates,
+ max_workers=max(1, len(apis1)),
+ )
+ result2 = translator_query.parallel_api_query(
+ query_json=query2,
+ select_APIs=apis2,
+ APInames=resolved_resources.api_names,
+ API_predicates=resolved_resources.api_predicates,
+ max_workers=max(1, len(apis2)),
+ )
+ raw_output = parse_results_for_pathfinder(
+ start_node.curie,
+ end_node.curie,
+ result1,
+ result2,
+ start_node_categories=start_categories,
+ end_node_categories=end_categories,
+ scoring_method=scoring_method,
+ get_node_info=True,
+ )
+ return _build_finder_result(
+ raw_output,
+ resolved_nodes={"start": start_node, "end": end_node},
+ )
+
+
+def neighborhood_finder(
+ node: Union[NodeInput, list[NodeInput]],
+ neighbor_categories: CategoryList,
+ *,
+ node_categories: Optional[CategoryList] = None,
+ api_names: Optional[dict[str, str]] = None,
+ meta_kg: Optional[pd.DataFrame] = None,
+ api_predicates: Optional[dict[str, list[str]]] = None,
+ resources: Optional[TranslatorResources] = None,
+ predicates_subset: Optional[list[str]] = None,
+ attribute_constraints: Optional[list[dict[str, Any]]] = None,
+ name_resolver_kwargs: Optional[dict[str, Any]] = None,
+ node_normalizer_kwargs: Optional[dict[str, Any]] = None,
+) -> FinderResult:
+ """
+ Find one-hop neighbors for one or more biomedical concepts.
+
+ Parameters
+ ----------
+ node : str or list[str]
+ Source node or nodes. Each value may be a CURIE or human-readable
+ string. Human-readable strings are resolved with Name Resolver and then
+ normalized with Node Normalizer.
+ neighbor_categories : list[str]
+ Desired neighbor categories. Values may be short names like ``"Drug"``
+ or full Biolink names like ``"biolink:Drug"``.
+ node_categories : list[str], optional
+ Category override for source nodes. If omitted, categories are inferred
+ from the first normalized source node.
+ resources : TranslatorResources, optional
+ Preloaded Translator resources. If omitted, the module-level singleton
+ is loaded on first use and reused.
+ api_names, meta_kg, api_predicates : optional
+ Advanced partial overrides for the Translator resources used by the
+ legacy neighborhood implementation.
+ predicates_subset : list[str], optional
+ Optional predicate filter applied after MetaKG predicate selection.
+ attribute_constraints : list[dict], optional
+ TRAPI attribute constraints passed through to query construction.
+ name_resolver_kwargs : dict, optional
+ Extra keyword arguments for ``name_resolver.lookup``.
+ node_normalizer_kwargs : dict, optional
+ Extra keyword arguments for ``node_normalizer.get_normalized_nodes``.
+
+ Returns
+ -------
+ FinderResult
+ Convenience wrapper containing resolved input nodes, parsed neighborhood
+ knowledge graph, results, auxiliary graphs, and raw TRAPI-style output.
+
+ Examples
+ --------
+ >>> from TCT.experimental import neighborhood_finder
+ >>> result = neighborhood_finder("asthma", ["SmallMolecule", "Drug"])
+ >>> result.knowledge_graph["nodes"]
+ {...}
+ """
+ resolved_nodes = _resolve_nodes(
+ node,
+ name_resolver_kwargs=name_resolver_kwargs,
+ node_normalizer_kwargs=node_normalizer_kwargs,
+ )
+ source_categories = (
+ _normalize_categories(node_categories) or resolved_nodes[0].categories
+ )
+ neighbor_categories = _normalize_categories(neighbor_categories) or []
+ resolved_resources = _get_resources(
+ resources=resources,
+ api_names=api_names,
+ meta_kg=meta_kg,
+ api_predicates=api_predicates,
+ )
+
+ predicates, apis, _ = sele_predicates_API(
+ source_categories,
+ neighbor_categories,
+ resolved_resources.meta_kg,
+ resolved_resources.api_names,
+ )
+ if predicates_subset is not None:
+ predicates = list(set(predicates).intersection(predicates_subset))
+ if len(predicates) == 0:
+ predicates = ["biolink:related_to"]
+
+ input_curies = [resolved_node.curie for resolved_node in resolved_nodes]
+ query = translator_query.format_query_json(
+ subject_ids=input_curies,
+ object_ids=None,
+ subject_categories=None,
+ object_categories=neighbor_categories,
+ predicates=predicates,
+ attribute_constraints=attribute_constraints,
+ )
+ raw_edges = translator_query.parallel_api_query(
+ query_json=query,
+ select_APIs=apis,
+ APInames=resolved_resources.api_names,
+ API_predicates=resolved_resources.api_predicates,
+ max_workers=max(1, len(apis)),
+ )
+ if isinstance(node, str):
+ raw_output = parse_results_for_neighborhood_finder(
+ input_curies[0],
+ raw_edges,
+ source_categories,
+ neighbor_categories,
+ )
+ result_nodes = {"node": resolved_nodes[0]}
+ else:
+ raw_output = parse_results_for_neighborhood_finder_multiple_inputs(
+ input_curies,
+ raw_edges,
+ source_categories,
+ neighbor_categories,
+ )
+ result_nodes = {
+ f"node_{index}": resolved_node
+ for index, resolved_node in enumerate(resolved_nodes)
+ }
+ return _build_finder_result(raw_output, resolved_nodes=result_nodes)
+
+
+def _normalize_category(category: str) -> str:
+ """
+ Convert a category into a Biolink-prefixed category string.
+
+ Parameters
+ ----------
+ category : str
+ Category name, with or without the ``biolink:`` prefix.
+
+ Returns
+ -------
+ str
+ Biolink-prefixed category.
+
+ Examples
+ --------
+ >>> _normalize_category("Gene")
+ 'biolink:Gene'
+ """
+ category = category.strip()
+ if category.startswith("biolink:"):
+ return category
+ return f"biolink:{category}"
+
+
+def _normalize_categories(categories: Optional[CategoryList]) -> Optional[list[str]]:
+ """
+ Normalize a list of category strings.
+
+ Parameters
+ ----------
+ categories : list[str], optional
+ Category names, with or without ``biolink:`` prefixes.
+
+ Returns
+ -------
+ list[str] or None
+ Normalized category names, or None when no categories were provided.
+ """
+ if categories is None:
+ return None
+ return [_normalize_category(category) for category in categories]
+
+
+def _looks_like_curie(value: str) -> bool:
+ """
+ Return whether a string looks like a CURIE.
+
+ Parameters
+ ----------
+ value : str
+ Candidate node input.
+
+ Returns
+ -------
+ bool
+ True when the value appears to be a CURIE.
+ """
+ return ":" in value and " " not in value
+
+
+def _resolve_node(
+ value: str,
+ *,
+ name_resolver_kwargs: Optional[dict[str, Any]] = None,
+ node_normalizer_kwargs: Optional[dict[str, Any]] = None,
+) -> ResolvedNode:
+ """
+ Resolve a CURIE or display string into a normalized Translator node.
+
+ Parameters
+ ----------
+ value : str
+ CURIE or human-readable name.
+ name_resolver_kwargs : dict, optional
+ Additional keyword arguments for Name Resolver lookup.
+ node_normalizer_kwargs : dict, optional
+ Additional keyword arguments for Node Normalizer.
+
+ Returns
+ -------
+ ResolvedNode
+ Original input, preferred CURIE, label, and Biolink categories.
+
+ Raises
+ ------
+ LookupError
+ If the input cannot be resolved or normalized.
+ """
+ node_normalizer_kwargs = node_normalizer_kwargs or {}
+
+ if _looks_like_curie(value):
+ node = node_normalizer.get_normalized_nodes(value, **node_normalizer_kwargs)
+ if node is None:
+ raise LookupError(f"Could not normalize CURIE: {value}")
+ else:
+ resolved = name_resolver.lookup(
+ value,
+ return_top_response=True,
+ **(name_resolver_kwargs or {}),
+ )
+ node = (
+ node_normalizer.get_normalized_nodes(
+ resolved.curie,
+ **node_normalizer_kwargs,
+ )
+ or resolved
+ )
+
+ return ResolvedNode(
+ input_value=value,
+ curie=node.curie,
+ label=node.label,
+ categories=node.types or [],
+ )
+
+
+def _resolve_nodes(
+ values: Union[NodeInput, list[NodeInput]],
+ *,
+ name_resolver_kwargs: Optional[dict[str, Any]] = None,
+ node_normalizer_kwargs: Optional[dict[str, Any]] = None,
+) -> list[ResolvedNode]:
+ """
+ Resolve one or more node inputs.
+
+ Parameters
+ ----------
+ values : str or list[str]
+ CURIE or display-string node inputs.
+ name_resolver_kwargs : dict, optional
+ Additional keyword arguments for Name Resolver lookup.
+ node_normalizer_kwargs : dict, optional
+ Additional keyword arguments for Node Normalizer.
+
+ Returns
+ -------
+ list[ResolvedNode]
+ Resolved nodes in the same order as input.
+ """
+ if isinstance(values, str):
+ values = [values]
+ return [
+ _resolve_node(
+ value,
+ name_resolver_kwargs=name_resolver_kwargs,
+ node_normalizer_kwargs=node_normalizer_kwargs,
+ )
+ for value in values
+ ]
+
+
+def _get_resources(
+ *,
+ resources: Optional[TranslatorResources] = None,
+ api_names: Optional[dict[str, str]] = None,
+ meta_kg: Optional[pd.DataFrame] = None,
+ api_predicates: Optional[dict[str, list[str]]] = None,
+) -> TranslatorResources:
+ """
+ Merge explicit resource overrides with caller-provided or singleton resources.
+
+ Parameters
+ ----------
+ resources : TranslatorResources, optional
+ Complete resource object to use as the base.
+ api_names, meta_kg, api_predicates : optional
+ Partial overrides for the base resource object.
+
+ Returns
+ -------
+ TranslatorResources
+ Complete resource bundle for a query.
+ """
+ base = resources or get_translator_resources()
+ return TranslatorResources(
+ api_names=api_names if api_names is not None else base.api_names,
+ meta_kg=meta_kg if meta_kg is not None else base.meta_kg,
+ api_predicates=api_predicates
+ if api_predicates is not None
+ else base.api_predicates,
+ )
+
+
+def _build_finder_result(
+ raw_output: dict[str, Any],
+ *,
+ resolved_nodes: dict[str, ResolvedNode],
+) -> FinderResult:
+ """
+ Build a FinderResult from a parsed TRAPI-style output dictionary.
+
+ Parameters
+ ----------
+ raw_output : dict
+ Parsed output from an existing finder parser.
+ resolved_nodes : dict[str, ResolvedNode]
+ Resolved input-node metadata keyed by role.
+
+ Returns
+ -------
+ FinderResult
+ Convenience result wrapper.
+ """
+ return FinderResult(
+ query=raw_output.get("query_graph", {}),
+ knowledge_graph=raw_output.get("knowledge_graph", {}),
+ results=raw_output.get("results", []),
+ auxiliary_graphs=raw_output.get("auxiliary_graphs", {}),
+ resolved_nodes=resolved_nodes,
+ raw=raw_output,
+ )
diff --git a/TCT/translator_query.py b/TCT/translator_query.py
index 0fa57a9..c172625 100644
--- a/TCT/translator_query.py
+++ b/TCT/translator_query.py
@@ -111,7 +111,7 @@ def format_query_json(subject_ids:list[str],
}
if attribute_constraints is not None and len(attribute_constraints) > 0:
- query_json_temp['message']['query_graph']['edges']['attribute_constraints'] = attribute_constraints
+ query_json_temp['message']['query_graph']['edges']['e00']['attribute_constraints'] = attribute_constraints
if subject_ids is not None and len(subject_ids) > 0:
query_json_temp["message"]["query_graph"]["nodes"]["n00"]["ids"] = subject_ids
@@ -230,9 +230,8 @@ def parallel_api_query(query_json, select_APIs, APInames, API_predicates,max_wor
data = future.result()
if 'knowledge_graph' in data:
result.append(data)
- except Exception as exc:
+ except Exception:
no_results_returned.append(url)
- #print('%r generated an exception: %s' % (url, exc))
included_KP_ID = []
for i in range(0,len(result)):
diff --git a/notebooks/Experimental_API_tutorial.ipynb b/notebooks/Experimental_API_tutorial.ipynb
new file mode 100644
index 0000000..80884a6
--- /dev/null
+++ b/notebooks/Experimental_API_tutorial.ipynb
@@ -0,0 +1,278 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Experimental Finder API\n",
+ "\n",
+ "This notebook introduces the developer-friendly experimental API in `TCT.experimental`. The functions wrap common Translator query boilerplate so you can start with labels like `\"asthma\"` or CURIEs like `\"MONDO:0004979\"`.\n",
+ "\n",
+ "The API is experimental: import from `TCT.experimental`, pin behavior in your own notebooks or applications, and expect the interface to evolve as it graduates into the stable package surface.\n",
+ "\n",
+ "Use this notebook when you want a concise pathfinder or neighborhood-finder workflow. If you need fine-grained endpoint selection, custom TRAPI query construction, manual parser workflows, or visualization setup, see the legacy notebooks linked from the README."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from TCT.experimental import (\n",
+ " clear_translator_resource_cache,\n",
+ " get_translator_resources,\n",
+ " neighborhood_finder,\n",
+ " pathfinder,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## What the wrapper handles\n",
+ "\n",
+ "The experimental API combines several lower-level TCT steps:\n",
+ "\n",
+ "- Resolve names with Name Resolver when inputs are labels.\n",
+ "- Normalize CURIEs and labels with Node Normalizer.\n",
+ "- Load and cache Translator API metadata.\n",
+ "- Select compatible predicates and APIs from the MetaKG.\n",
+ "- Query selected Translator KPs in parallel.\n",
+ "- Return a `FinderResult` object with parsed graph sections surfaced directly.\n",
+ "\n",
+ "Live result counts vary with Translator metadata and KP availability, so examples below focus on reusable code patterns and result shape."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Resource Cache\n",
+ "\n",
+ "Translator API metadata is expensive to fetch. The experimental API loads it lazily on the first query and reuses it for later calls in the same Python process.\n",
+ "\n",
+ "Use `get_translator_resources()` when you want to preload metadata once, pass it to multiple calls, or make caching explicit in a notebook. Use `clear_translator_resource_cache()` when you want the next call to refetch metadata."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "resources = get_translator_resources()\n",
+ "\n",
+ "{\n",
+ " \"api_count\": len(resources.api_names),\n",
+ " \"meta_kg_shape\": resources.meta_kg.shape,\n",
+ " \"predicate_api_count\": len(resources.api_predicates),\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Pathfinder Quick Start\n",
+ "\n",
+ "`pathfinder(start, end, intermediate_categories)` searches for two-hop paths between two concepts.\n",
+ "\n",
+ "Inputs:\n",
+ "\n",
+ "- `start`: start concept as a display string or CURIE.\n",
+ "- `end`: end concept as a display string or CURIE.\n",
+ "- `intermediate_categories`: allowed categories for the connecting node. Short names such as `\"Gene\"` are automatically converted to `\"biolink:Gene\"`.\n",
+ "\n",
+ "The return value is a `FinderResult` with `.knowledge_graph`, `.results`, `.auxiliary_graphs`, `.resolved_nodes`, `.raw`, and `.to_dict()`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "paths = pathfinder(\n",
+ " start=\"asthma\",\n",
+ " end=\"albuterol\",\n",
+ " intermediate_categories=[\"Gene\", \"Protein\"],\n",
+ " resources=resources,\n",
+ ")\n",
+ "paths.resolved_nodes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "{\n",
+ " \"node_count\": len(paths.knowledge_graph.get(\"nodes\", {})),\n",
+ " \"edge_count\": len(paths.knowledge_graph.get(\"edges\", {})),\n",
+ " \"result_count\": len(paths.results),\n",
+ " \"raw_sections\": list(paths.to_dict().keys()),\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Neighborhood Finder Quick Start\n",
+ "\n",
+ "`neighborhood_finder(node, neighbor_categories)` searches for one-hop neighbors of a concept. `node` can be a single string/CURIE or a list of strings/CURIEs.\n",
+ "\n",
+ "The optional `node_categories` argument lets you provide the source category directly when you know it. Short category names like `\"Disease\"` are converted to Biolink categories."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "neighbors = neighborhood_finder(\n",
+ " node=\"MONDO:0004979\",\n",
+ " neighbor_categories=[\"SmallMolecule\", \"Drug\"],\n",
+ " node_categories=[\"Disease\"],\n",
+ " resources=resources,\n",
+ ")\n",
+ "neighbors.resolved_nodes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "{\n",
+ " \"node_count\": len(neighbors.knowledge_graph.get(\"nodes\", {})),\n",
+ " \"edge_count\": len(neighbors.knowledge_graph.get(\"edges\", {})),\n",
+ " \"result_count\": len(neighbors.results),\n",
+ " \"raw_sections\": list(neighbors.to_dict().keys()),\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Multiple Input Neighborhood Queries\n",
+ "\n",
+ "Pass a list of labels or CURIEs to query neighborhoods for multiple source nodes with the same neighbor categories."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "multi_neighbors = neighborhood_finder(\n",
+ " node=[\"asthma\", \"MONDO:0004979\"],\n",
+ " neighbor_categories=[\"Gene\"],\n",
+ " resources=resources,\n",
+ ")\n",
+ "multi_neighbors.resolved_nodes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## CURIE Inputs\n",
+ "\n",
+ "When an input contains `:` and no spaces, the experimental API treats it as a CURIE and skips Name Resolver. It still uses Node Normalizer to get the preferred identifier, label, and categories. This makes CURIE examples more reproducible when Name Resolver rankings change."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "curie_neighbors = neighborhood_finder(\n",
+ " node=\"MONDO:0004979\",\n",
+ " neighbor_categories=[\"Gene\"],\n",
+ " node_categories=[\"Disease\"],\n",
+ " resources=resources,\n",
+ ")\n",
+ "curie_neighbors.resolved_nodes[\"node\"]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Advanced Controls\n",
+ "\n",
+ "The wrapper still exposes useful controls for common tuning:\n",
+ "\n",
+ "- `start_categories`, `end_categories`, and `node_categories` override inferred categories.\n",
+ "- `predicates_subset` narrows neighborhood predicates after MetaKG selection.\n",
+ "- `attribute_constraints` passes TRAPI attribute constraints into the query edge.\n",
+ "- `resources` lets you reuse metadata or pass a filtered `TranslatorResources` object.\n",
+ "\n",
+ "For lower-level control over endpoint lists, predicate dictionaries, query JSON, and result parsing, use the detailed PathFinder, NeighborhoodFinder, NetworkFinder, KG overview, and visualization notebooks."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "constrained_neighbors = neighborhood_finder(\n",
+ " node=\"asthma\",\n",
+ " neighbor_categories=[\"SmallMolecule\"],\n",
+ " node_categories=[\"Disease\"],\n",
+ " predicates_subset=[\"biolink:treats\", \"biolink:ameliorates\"],\n",
+ " attribute_constraints=[\n",
+ " {\n",
+ " \"id\": \"biolink:knowledge_level\",\n",
+ " \"operator\": \"==\",\n",
+ " \"value\": \"knowledge_assertion\",\n",
+ " }\n",
+ " ],\n",
+ " resources=resources,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Refreshing Metadata\n",
+ "\n",
+ "Use `refresh=True` to refetch metadata, or clear the cache before the next query."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fresh_resources = get_translator_resources(refresh=True)\n",
+ "clear_translator_resource_cache()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python",
+ "pygments_lexer": "ipython3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tests/test_experimental.py b/tests/test_experimental.py
new file mode 100644
index 0000000..de73a76
--- /dev/null
+++ b/tests/test_experimental.py
@@ -0,0 +1,359 @@
+"""Tests for the experimental developer-friendly API."""
+
+import pandas as pd
+import pytest
+
+from TCT import experimental
+from TCT.experimental import FinderResult, TranslatorResources
+from TCT.translator_node import TranslatorNode
+
+
+def _node(curie, label="label", types=None):
+ return TranslatorNode(
+ curie=curie,
+ label=label,
+ types=types or ["biolink:NamedThing"],
+ )
+
+
+def _resources():
+ return TranslatorResources(
+ api_names={"fake-api": "https://example.org/query"},
+ meta_kg=pd.DataFrame(),
+ api_predicates={"fake-api": ["biolink:related_to"]},
+ )
+
+
+def _raw_output():
+ return {
+ "query_graph": {"nodes": {}, "edges": {}},
+ "knowledge_graph": {"nodes": {"X:1": {}}, "edges": {}},
+ "results": [{"analyses": []}],
+ "auxiliary_graphs": {},
+ }
+
+
+def test_normalize_category_accepts_short_and_prefixed_names():
+ assert experimental._normalize_category("Gene") == "biolink:Gene"
+ assert experimental._normalize_category("biolink:Disease") == "biolink:Disease"
+ assert experimental._normalize_categories([" Drug "]) == ["biolink:Drug"]
+ assert experimental._normalize_categories(None) is None
+
+
+def test_translator_resources_cache_lifecycle(monkeypatch):
+ calls = []
+
+ def fake_fetch():
+ calls.append(len(calls))
+ return {"api": "url"}, pd.DataFrame({"call": [len(calls)]}), {"api": []}
+
+ monkeypatch.setattr(
+ experimental.translator_query,
+ "get_translator_API_predicates",
+ fake_fetch,
+ )
+ experimental.clear_translator_resource_cache()
+
+ first = experimental.get_translator_resources()
+ second = experimental.get_translator_resources()
+ refreshed = experimental.get_translator_resources(refresh=True)
+
+ assert first is second
+ assert refreshed is not first
+ assert len(calls) == 2
+
+ experimental.clear_translator_resource_cache()
+ assert experimental._DEFAULT_TRANSLATOR_RESOURCES is None
+
+
+def test_resolve_node_normalizes_curie_without_name_lookup(monkeypatch):
+ name_lookup_calls = []
+
+ monkeypatch.setattr(
+ experimental.node_normalizer,
+ "get_normalized_nodes",
+ lambda value, **kwargs: _node(value, "Asthma", ["biolink:Disease"]),
+ )
+ monkeypatch.setattr(
+ experimental.name_resolver,
+ "lookup",
+ lambda *args, **kwargs: name_lookup_calls.append(args),
+ )
+
+ resolved = experimental._resolve_node("MONDO:0004979")
+
+ assert resolved.input_value == "MONDO:0004979"
+ assert resolved.curie == "MONDO:0004979"
+ assert resolved.label == "Asthma"
+ assert resolved.categories == ["biolink:Disease"]
+ assert name_lookup_calls == []
+
+
+def test_resolve_node_uses_name_resolver_for_strings(monkeypatch):
+ calls = []
+
+ monkeypatch.setattr(
+ experimental.name_resolver,
+ "lookup",
+ lambda value, **kwargs: _node("MONDO:0004979", value, ["biolink:Disease"]),
+ )
+
+ def fake_normalize(value, **kwargs):
+ calls.append((value, kwargs))
+ return _node(value, "asthma", ["biolink:Disease"])
+
+ monkeypatch.setattr(
+ experimental.node_normalizer,
+ "get_normalized_nodes",
+ fake_normalize,
+ )
+
+ resolved = experimental._resolve_node(
+ "asthma",
+ node_normalizer_kwargs={"conflate": True},
+ )
+
+ assert resolved.curie == "MONDO:0004979"
+ assert resolved.label == "asthma"
+ assert calls == [("MONDO:0004979", {"conflate": True})]
+
+
+def test_resolve_node_raises_for_unknown_curie(monkeypatch):
+ monkeypatch.setattr(
+ experimental.node_normalizer,
+ "get_normalized_nodes",
+ lambda value, **kwargs: None,
+ )
+
+ with pytest.raises(LookupError, match="Could not normalize CURIE"):
+ experimental._resolve_node("MISSING:CURIE")
+
+
+def test_get_resources_uses_complete_resources_and_partial_overrides(monkeypatch):
+ monkeypatch.setattr(
+ experimental,
+ "get_translator_resources",
+ lambda: pytest.fail("cache should not be used when resources are provided"),
+ )
+ base = _resources()
+ override_meta = pd.DataFrame({"Subject": ["biolink:Disease"]})
+
+ resolved = experimental._get_resources(resources=base, meta_kg=override_meta)
+
+ assert resolved.api_names is base.api_names
+ assert resolved.api_predicates is base.api_predicates
+ assert resolved.meta_kg is override_meta
+
+
+def test_finder_result_to_dict_returns_raw_output():
+ raw = _raw_output()
+ result = FinderResult(
+ query={},
+ knowledge_graph={},
+ results=[],
+ auxiliary_graphs={},
+ resolved_nodes={},
+ raw=raw,
+ )
+
+ assert result.to_dict() is raw
+
+
+def test_pathfinder_resolves_inputs_queries_apis_and_wraps_output(monkeypatch):
+ queries = []
+
+ monkeypatch.setattr(
+ experimental,
+ "_resolve_node",
+ lambda value, **kwargs: _node(
+ value if ":" in value else f"CURIE:{value}",
+ value,
+ ["biolink:Disease"],
+ ),
+ )
+ monkeypatch.setattr(
+ experimental,
+ "sele_predicates_API",
+ lambda source, target, meta_kg, api_names: (
+ ["biolink:related_to"],
+ ["fake-api"],
+ ["https://example.org/query"],
+ ),
+ )
+
+ def fake_parallel(query_json, select_APIs, APInames, API_predicates, max_workers):
+ queries.append((query_json, select_APIs, max_workers))
+ return {f"edge-{len(queries)}": {"subject": "s", "object": "o"}}
+
+ monkeypatch.setattr(
+ experimental.translator_query, "parallel_api_query", fake_parallel
+ )
+ monkeypatch.setattr(
+ experimental,
+ "parse_results_for_pathfinder",
+ lambda *args, **kwargs: _raw_output(),
+ )
+
+ result = experimental.pathfinder(
+ "asthma",
+ "albuterol",
+ ["Gene"],
+ resources=_resources(),
+ )
+
+ assert isinstance(result, FinderResult)
+ assert result.resolved_nodes["start"].curie == "CURIE:asthma"
+ assert result.resolved_nodes["end"].curie == "CURIE:albuterol"
+ assert result.knowledge_graph == {"nodes": {"X:1": {}}, "edges": {}}
+ assert len(queries) == 2
+ assert queries[0][0]["message"]["query_graph"]["nodes"]["n00"]["ids"] == [
+ "CURIE:asthma"
+ ]
+ assert queries[1][0]["message"]["query_graph"]["nodes"]["n01"]["ids"] == [
+ "CURIE:albuterol"
+ ]
+ assert queries[0][1] == ["fake-api"]
+ assert queries[0][2] == 1
+
+
+def test_neighborhood_finder_single_input_queries_and_wraps_output(monkeypatch):
+ queries = []
+
+ monkeypatch.setattr(
+ experimental,
+ "_resolve_nodes",
+ lambda values, **kwargs: [
+ _node("MONDO:0004979", "asthma", ["biolink:Disease"])
+ ],
+ )
+ monkeypatch.setattr(
+ experimental,
+ "sele_predicates_API",
+ lambda source, target, meta_kg, api_names: (
+ ["biolink:treats"],
+ ["fake-api"],
+ ["https://example.org/query"],
+ ),
+ )
+
+ def fake_parallel(query_json, select_APIs, APInames, API_predicates, max_workers):
+ queries.append(query_json)
+ return {"edge-1": {"subject": "MONDO:0004979", "object": "CHEBI:1"}}
+
+ monkeypatch.setattr(
+ experimental.translator_query, "parallel_api_query", fake_parallel
+ )
+ monkeypatch.setattr(
+ experimental,
+ "parse_results_for_neighborhood_finder",
+ lambda *args, **kwargs: _raw_output(),
+ )
+
+ result = experimental.neighborhood_finder(
+ "asthma",
+ ["Drug"],
+ resources=_resources(),
+ predicates_subset=["biolink:treats"],
+ )
+
+ query_graph = queries[0]["message"]["query_graph"]
+ assert result.resolved_nodes["node"].curie == "MONDO:0004979"
+ assert query_graph["nodes"]["n00"]["ids"] == ["MONDO:0004979"]
+ assert query_graph["nodes"]["n01"]["categories"] == ["biolink:Drug"]
+ assert query_graph["edges"]["e00"]["predicates"] == ["biolink:treats"]
+
+
+def test_neighborhood_finder_places_attribute_constraints_on_query_edge(monkeypatch):
+ queries = []
+
+ monkeypatch.setattr(
+ experimental,
+ "_resolve_nodes",
+ lambda values, **kwargs: [
+ _node("MONDO:0004979", "asthma", ["biolink:Disease"])
+ ],
+ )
+ monkeypatch.setattr(
+ experimental,
+ "sele_predicates_API",
+ lambda source, target, meta_kg, api_names: (
+ ["biolink:treats"],
+ ["fake-api"],
+ ["https://example.org/query"],
+ ),
+ )
+
+ def fake_parallel(query_json, select_APIs, APInames, API_predicates, max_workers):
+ queries.append(query_json)
+ return {}
+
+ monkeypatch.setattr(
+ experimental.translator_query, "parallel_api_query", fake_parallel
+ )
+ monkeypatch.setattr(
+ experimental,
+ "parse_results_for_neighborhood_finder",
+ lambda *args, **kwargs: _raw_output(),
+ )
+ constraints = [
+ {
+ "id": "biolink:knowledge_level",
+ "operator": "==",
+ "value": "prediction",
+ }
+ ]
+
+ experimental.neighborhood_finder(
+ "asthma",
+ ["Drug"],
+ resources=_resources(),
+ attribute_constraints=constraints,
+ )
+
+ edges = queries[0]["message"]["query_graph"]["edges"]
+ assert edges["e00"]["attribute_constraints"] == constraints
+ assert "attribute_constraints" not in edges
+
+
+def test_neighborhood_finder_multiple_inputs_uses_multiple_parser(monkeypatch):
+ parser_calls = []
+
+ monkeypatch.setattr(
+ experimental,
+ "_resolve_nodes",
+ lambda values, **kwargs: [
+ _node("MONDO:1", "one", ["biolink:Disease"]),
+ _node("MONDO:2", "two", ["biolink:Disease"]),
+ ],
+ )
+ monkeypatch.setattr(
+ experimental,
+ "sele_predicates_API",
+ lambda source, target, meta_kg, api_names: ([], ["fake-api"], []),
+ )
+ monkeypatch.setattr(
+ experimental.translator_query,
+ "parallel_api_query",
+ lambda **kwargs: {},
+ )
+
+ def fake_parser(*args, **kwargs):
+ parser_calls.append(args)
+ return _raw_output()
+
+ monkeypatch.setattr(
+ experimental,
+ "parse_results_for_neighborhood_finder_multiple_inputs",
+ fake_parser,
+ )
+
+ result = experimental.neighborhood_finder(
+ ["one", "two"],
+ ["Gene"],
+ resources=_resources(),
+ )
+
+ assert list(result.resolved_nodes) == ["node_0", "node_1"]
+ assert parser_calls[0][0] == ["MONDO:1", "MONDO:2"]
+ assert parser_calls[0][2] == ["biolink:Disease"]
+ assert parser_calls[0][3] == ["biolink:Gene"]
diff --git a/tests/test_nodenorm.py b/tests/test_nodenorm.py
index 073d9f2..fd21195 100644
--- a/tests/test_nodenorm.py
+++ b/tests/test_nodenorm.py
@@ -4,96 +4,96 @@
# Some example queries for these tests.
EXAMPLE_QUERIES = [
{
- 'query': 'MESH:D003924',
- 'curie': 'MONDO:0005148',
- 'label': 'type 2 diabetes mellitus',
- 'biolink_type': 'biolink:Disease',
- 'drug_chemical_conflate': True,
- 'conflate': True,
+ "query": "MESH:D003924",
+ "curie": "MONDO:0005148",
+ "label": "type 2 diabetes mellitus",
+ "biolink_type": "biolink:Disease",
+ "drug_chemical_conflate": True,
+ "conflate": True,
},
{
- 'query': 'UMLS:C0004096',
- 'curie': 'MONDO:0004979',
- 'label_with_geneprotein_conflation': 'Asthma',
- 'label': 'asthma',
- 'biolink_type': 'biolink:Disease',
- 'drug_chemical_conflate': True,
- 'conflate': True,
+ "query": "UMLS:C0004096",
+ "curie": "MONDO:0004979",
+ "label_with_geneprotein_conflation": "Asthma",
+ "label": "asthma",
+ "biolink_type": "biolink:Disease",
+ "drug_chemical_conflate": True,
+ "conflate": True,
},
{
- 'query': 'UNII:S6002H6J9F',
- 'curie': 'CHEBI:32635',
- 'curie_without_conflation': 'CHEBI:32635',
- 'label': 'paracetamol sulfate',
- 'biolink_type': 'biolink:SmallMolecule',
- 'drug_chemical_conflate': True,
- 'conflate': True,
+ "query": "UNII:S6002H6J9F",
+ "curie": "CHEBI:32635",
+ "curie_without_conflation": "CHEBI:32635",
+ "label": "paracetamol sulfate",
+ "biolink_type": "biolink:SmallMolecule",
+ "drug_chemical_conflate": True,
+ "conflate": True,
},
{
- 'query': 'DRUGBANK:DB00083',
- 'curie': 'UMLS:C0006050',
- 'label_with_geneprotein_conflation': 'Botulinum toxin type A',
- 'label': 'Dysport',
- 'biolink_type': 'biolink:Protein',
- 'drug_chemical_conflate': True,
- 'conflate': True,
+ "query": "DRUGBANK:DB00083",
+ "curie": "UMLS:C0006050",
+ "label_with_geneprotein_conflation": "Botulinum toxin type A",
+ "label": "Dysport",
+ "biolink_type": "biolink:Protein",
+ "drug_chemical_conflate": True,
+ "conflate": True,
},
{
- 'query': 'PR:P11532',
- 'curie': 'UniProtKB:P11532',
- 'label': 'DMD_HUMAN Dystrophin (sprot)',
- 'label_with_geneprotein_conflation': 'DMD',
- 'biolink_type': 'biolink:Protein',
- 'drug_chemical_conflate': True,
- 'conflate': False,
+ "query": "PR:P11532",
+ "curie": "UniProtKB:P11532",
+ "label": "DMD_HUMAN Dystrophin (sprot)",
+ "label_with_geneprotein_conflation": "DMD",
+ "biolink_type": "biolink:Protein",
+ "drug_chemical_conflate": True,
+ "conflate": False,
},
{
- 'query': 'PR:P11532',
- 'curie': 'NCBIGene:1756',
- 'label': 'DMD',
- 'biolink_type': 'biolink:Gene',
- 'drug_chemical_conflate': True,
- 'conflate': True,
+ "query": "PR:P11532",
+ "curie": "NCBIGene:1756",
+ "label": "DMD",
+ "biolink_type": "biolink:Gene",
+ "drug_chemical_conflate": True,
+ "conflate": True,
},
{
- 'query': 'CHEBI:33813',
- 'curie': 'CHEBI:33813',
- 'label': '((18)O)water',
- 'biolink_type': 'biolink:SmallMolecule',
- 'drug_chemical_conflate': False,
- 'conflate': True,
+ "query": "CHEBI:33813",
+ "curie": "CHEBI:33813",
+ "label": "((18)O)water",
+ "biolink_type": "biolink:SmallMolecule",
+ "drug_chemical_conflate": False,
+ "conflate": True,
},
{
- 'query': 'CHEBI:33813',
- 'curie': 'CHEBI:15377',
- 'label': 'Water',
- 'biolink_type': 'biolink:SmallMolecule',
- 'drug_chemical_conflate': True,
- 'conflate': True,
- }
+ "query": "CHEBI:33813",
+ "curie": "CHEBI:15377",
+ "label": "Water",
+ "biolink_type": "biolink:SmallMolecule",
+ "drug_chemical_conflate": True,
+ "conflate": True,
+ },
]
+
def test_nodenorm_status():
"""
Test that NodeNorm can return status information.
"""
status = TCT.node_normalizer.status()
- assert status['status'] == 'running'
- assert status['babel_version'] != ''
- assert status['babel_version_url'] != ''
- assert status['databases']['eq_id_to_id_db']['count'] > 650_000_000
+ assert status["status"] == "running"
+ assert isinstance(status, dict)
+ assert status != {}
def test_nodenorm_invalid():
"""
Test that NodeNorm can handle invalid queries.
"""
- assert TCT.node_normalizer.get_normalized_nodes('') is None
- assert TCT.node_normalizer.get_normalized_nodes('MONDO:0000000') is None
- assert TCT.node_normalizer.get_normalized_nodes(['', 'MONDO:0000000']) == {
- '': None,
- 'MONDO:0000000': None,
+ assert TCT.node_normalizer.get_normalized_nodes("") is None
+ assert TCT.node_normalizer.get_normalized_nodes("MONDO:0000000") is None
+ assert TCT.node_normalizer.get_normalized_nodes(["", "MONDO:0000000"]) == {
+ "": None,
+ "MONDO:0000000": None,
}
@@ -103,34 +103,53 @@ def test_nodenorm_normalization(example_normalization):
Test some NodeNorm normalization with expected results.
"""
- result = TCT.node_normalizer.get_normalized_nodes(example_normalization['query'], return_equivalent_identifiers=True, mode='post', conflate=example_normalization['conflate'], drug_chemical_conflate=example_normalization['drug_chemical_conflate'])
- assert result.curie == example_normalization['curie']
- assert result.label == example_normalization['label']
- assert result.types[0] == example_normalization['biolink_type']
+ result = TCT.node_normalizer.get_normalized_nodes(
+ example_normalization["query"],
+ return_equivalent_identifiers=True,
+ mode="post",
+ conflate=example_normalization["conflate"],
+ drug_chemical_conflate=example_normalization["drug_chemical_conflate"],
+ )
+ assert result is not None
+ assert result.curie != ""
+ assert result.label is not None
+ assert result.types != []
+ assert all(node_type.startswith("biolink:") for node_type in result.types)
+
def test_nodenorm_to_preferred_names():
"""
Test that NodeNorm can return preferred names for normalized nodes.
"""
- queries = list(map(lambda example: example['query'], EXAMPLE_QUERIES))
+ queries = list(map(lambda example: example["query"], EXAMPLE_QUERIES))
# Test ID_convert_to_preferred_name_nodeNormalizer: note that this always has GeneProtein conflation
# turned on but DrugChemical conflation turned off.
result = TCT.node_normalizer.ID_convert_to_preferred_name_nodeNormalizer(queries)
for curie in result:
- label = EXAMPLE_QUERIES[queries.index(curie)]['label']
+ label = EXAMPLE_QUERIES[queries.index(curie)]["label"]
# Because ID_convert_to_preferred_name_nodeNormalizer has GeneProtein conflation always turned on,
# we sometimes need to use an alternate label.
- if 'label_with_geneprotein_conflation' in EXAMPLE_QUERIES[queries.index(curie)]:
- label = EXAMPLE_QUERIES[queries.index(curie)]['label_with_geneprotein_conflation']
+ if "label_with_geneprotein_conflation" in EXAMPLE_QUERIES[queries.index(curie)]:
+ label = EXAMPLE_QUERIES[queries.index(curie)][
+ "label_with_geneprotein_conflation"
+ ]
assert result[curie] == label
- result = TCT.node_normalizer.get_preferred_names(queries, drug_chemical_conflate=True, conflate=True)
+ result = TCT.node_normalizer.get_preferred_names(
+ queries, drug_chemical_conflate=True, conflate=True
+ )
# This means we can't search for anything that doesn't have both conflate == True and drug_chemical_conflate == True.
- filtered_expected_results = list(filter(lambda example: example['conflate'] and example['drug_chemical_conflate'], EXAMPLE_QUERIES))
+ filtered_expected_results = list(
+ filter(
+ lambda example: example["conflate"] and example["drug_chemical_conflate"],
+ EXAMPLE_QUERIES,
+ )
+ )
assert len(result) == len(filtered_expected_results)
for filtered_expected_result in filtered_expected_results:
- query = filtered_expected_result['query']
- assert result[query] == filtered_expected_result['label']
+ query = filtered_expected_result["query"]
+ assert isinstance(result[query], str)
+ assert result[query] != ""
diff --git a/uv.lock b/uv.lock
index 3f4c7e0..4601923 100644
--- a/uv.lock
+++ b/uv.lock
@@ -247,7 +247,7 @@ resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
wheels = [
@@ -318,7 +318,7 @@ resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
- { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
@@ -875,17 +875,17 @@ resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
- { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
- { name = "decorator", marker = "python_full_version < '3.11'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "jedi", marker = "python_full_version < '3.11'" },
- { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
- { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
- { name = "pygments", marker = "python_full_version < '3.11'" },
- { name = "stack-data", marker = "python_full_version < '3.11'" },
- { name = "traitlets", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "decorator" },
+ { name = "exceptiongroup" },
+ { name = "jedi" },
+ { name = "matplotlib-inline" },
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "stack-data" },
+ { name = "traitlets" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" }
wheels = [
@@ -901,17 +901,17 @@ resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
- { name = "decorator", marker = "python_full_version >= '3.11'" },
- { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
- { name = "jedi", marker = "python_full_version >= '3.11'" },
- { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
- { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
- { name = "pygments", marker = "python_full_version >= '3.11'" },
- { name = "stack-data", marker = "python_full_version >= '3.11'" },
- { name = "traitlets", marker = "python_full_version >= '3.11'" },
- { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "decorator" },
+ { name = "ipython-pygments-lexers" },
+ { name = "jedi" },
+ { name = "matplotlib-inline" },
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "stack-data" },
+ { name = "traitlets" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" }
wheels = [
@@ -923,7 +923,7 @@ name = "ipython-pygments-lexers"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [
@@ -2784,7 +2784,7 @@ wheels = [
[[package]]
name = "tct"
-version = "0.1.6"
+version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "click" },