REST server mode¶
create_app wraps any BaseLinker
as a REST service, mainly by exposing
BaseLinker.link as POST /link/{linker_name}.
The deployer constructs whatever linker(s)/EntitySource(s)
they need in Python -- however they need to (model paths, device,
checkpoints, API keys, ...) -- and registers them by name; there's no
generic "build a linker from JSON config" layer, since the ~25 linkers in
this library have very different constructors.
"""Offer a linker as a REST service, wrapping BaseLinker.link.
Run with: `uv run python examples/rest_server_example.py`, then e.g.:
curl -X POST localhost:8000/link/exact-match \\
-F 'request={"dataset1":[{"id":"s1","labels":["cat"]}],
"dataset2_entities":[{"id":"t1","labels":["cat"]},
{"id":"t2","labels":["dog"]}]}'
`app` is also importable directly for production-style deployment, e.g.
`uvicorn examples.rest_server_example:app`.
"""
from linkingtk.algorithms.base import DEFAULT_BLOCKING, BaseLinker
from linkingtk.core.result import AlignmentResult
from linkingtk.server import create_app
class ExactMatchLinker(BaseLinker):
"""Turns ExactMatch's candidate pairs directly into links, for demo purposes."""
def link(self, dataset1, dataset2, graph=None, blocking=DEFAULT_BLOCKING):
pairs = blocking.candidate_pairs(dataset1, dataset2)
return [AlignmentResult(source_id=a.id, target_id=b.id) for a, b in pairs]
# A pre-registered EntitySource can be referenced by name too, instead of
# sending dataset2 inline -- see docs/examples/rest_server.md.
app = create_app(linkers={"exact-match": ExactMatchLinker()})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
Run with:
uv run python examples/rest_server_example.py
or, for production-style deployment where uvicorn manages the process:
uvicorn examples.rest_server_example:app
Requires the server extra: uv sync --extra server (or pip install
"linkingtk[server]").
Endpoints¶
GET /health-- liveness check.GET /linkers-- names of the registered linkers.GET /sources-- names of the registered entity sources.POST /link/{linker_name}-- runslink(). Amultipart/form-datarequest with:request(required): a JSON object withdataset1(a list of entities) and exactly one ofdataset2_entities(a list of entities, sent inline) ordataset2_source(the name of a pre-registeredEntitySource, passed straight through tolink()so search-based blocking still works instead of materializing every candidate).graph(optional): an uploaded file with the supporting graph.graph_format(optional, default"triples"):"triples"for a JSON list of[subject, predicate, object]triples,"rdf"for any RDF serializationrdflibcan parse (Turtle, N-Triples, RDF/XML, JSON-LD, ...), or"graphml"/"gexf"/"edgelist"for the matching NetworkX file format.
Response: a JSON list of {"source_id", "target_id", "score",
"alternatives"} objects, mirroring
AlignmentResult.
Targeting an EntitySource by name¶
Pass a sources dict to create_app alongside linkers to let requests
reference a live source (Wikidata, a local vector index, ...) instead of
sending dataset2 inline:
app = create_app(
linkers={"exact-match": ExactMatchLinker()},
sources={"wikidata": WikidataEntitySource()},
)
curl -X POST localhost:8000/link/exact-match \
-F 'request={"dataset1":[{"id":"s1","labels":["cat"]}],"dataset2_source":"wikidata"}'
Uploading a graph¶
curl -X POST localhost:8000/link/my-linker \
-F 'request={"dataset1":[...],"dataset2_entities":[...]}' \
-F 'graph_format=rdf' \
-F 'graph=@my_graph.ttl'
Blocking-strategy selection isn't exposed over REST yet -- each linker always runs with its own default blocking strategy.