ShExpose#
ShExpose generates a REST API from ShEx shapes through a LinkML → ShEx → LDO → Express pipeline: every shape becomes a set of CRUD routes, and the SPARQL is derived from the shape instead of being written by hand. It binds one endpoint and addresses a resource by its RDF subject IRI, so the lookup here uses OMID instead of DOI. The shape below describes a bibliographic resource as a fabio:Expression carrying its title and its authors’ names.
from urllib.parse import quote
from helper import call
omid = "https://w3id.org/oc/meta/br/061202127149"
uri = quote(omid, safe="")
A simple request#
ShExpose addresses a resource by its URL-encoded subject IRI. Looking up the OMID returns the attributes named in the shape: the title from OpenCitations Meta together with the author surnames and given names, each value wrapped in a {language, value} object.
call(f"http://localhost:8090/expression/{uri}/")
curl http://localhost:8090/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2F061202127149/
# 200 OK
{
"title": {
"language": "@none",
"value": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
},
"familyName": [
{
"language": "@none",
"value": "Peroni"
},
{
"language": "@none",
"value": "Moretti"
},
{
"language": "@none",
"value": "Massari"
},
{
"language": "@none",
"value": "Coppini"
},
{
"language": "@none",
"value": "Shahidzadeh"
},
{
"language": "@none",
"value": "Cioffi"
},
{
"language": "@none",
"value": "Santini"
}
],
"givenName": [
{
"language": "@none",
"value": "Silvio"
},
... (26 more lines)
The join#
Not supported.
Output#
JSON only, with no content negotiation.
call(f"http://localhost:8090/expression/{uri}/title")
curl http://localhost:8090/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2F061202127149/title
# 200 OK
{
"language": "@none",
"value": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
}
Pagination#
Not supported.
Versioning#
Not supported.
API description#
OpenAPI 3.0, generated from the shapes with zod-to-openapi. ShExpose serves a Swagger UI at http://localhost:8090/docs and the spec at http://localhost:8090/openapi.json.
import requests
from helper import embed_swagger
spec = requests.get("http://localhost:8090/openapi.json", timeout=120).json()
embed_swagger(spec, base_url="http://localhost:8090/")
Consumer authentication#
Not supported for consumers: the generated API is open. For the upstream endpoint, ShExpose can send HTTP Basic credentials on reads and a QLever bearer token on writes.
Endpoint authentication#
HTTP Basic on queries, from rdf.auth in the configuration.
from pathlib import Path
print(Path("shexpose/config/config.yaml").read_text())
call(f"http://localhost:8090/expression/{uri}/title")
app:
port: 3000
rdf:
sparql_endpoint: http://meta-basic:3030/sparql
auth:
username: demo
password: demo
data:
base_uri: https://w3id.org/oc/meta/
debug:
do_sparql_update: false
curl http://localhost:8090/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2F061202127149/title
# 200 OK
{
"language": "@none",
"value": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
}
Operations#
GET, POST, PUT, and DELETE on every shape and on each of its attributes. A second server is bound to a copy of OpenCitations Meta that accepts updates, with do_sparql_update switched on. A POST creates a resource under a generated IRI.
import requests
created = requests.post("http://localhost:8091/expression/", json={"title": {"value": "A title"}}, timeout=120)
print(created.status_code, created.json())
new_uri = quote(created.json()["uri"], safe="")
call(f"http://localhost:8091/expression/{new_uri}/title")
201 {'uri': 'https://w3id.org/oc/meta/expression-5e7cfddb-8fce-42ac-a6c0-830524d84821'}
curl http://localhost:8091/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fexpression-5e7cfddb-8fce-42ac-a6c0-830524d84821/title
# 200 OK
{
"language": "@none",
"value": "A title"
}
A PUT replaces the title.
call(f"http://localhost:8091/expression/{new_uri}/title", method="PUT", data={"value": "Another title"})
call(f"http://localhost:8091/expression/{new_uri}/title")
curl -X PUT -H 'Content-type: application/json' --data '{"value": "Another title"}' http://localhost:8091/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fexpression-5e7cfddb-8fce-42ac-a6c0-830524d84821/title
# 200 OK
{
"success": true
}
curl http://localhost:8091/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fexpression-5e7cfddb-8fce-42ac-a6c0-830524d84821/title
# 200 OK
{
"language": "@none",
"value": "Another title"
}
A DELETE removes the resource.
call(f"http://localhost:8091/expression/{new_uri}/", method="DELETE")
call(f"http://localhost:8091/expression/{new_uri}/")
curl -X DELETE http://localhost:8091/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fexpression-5e7cfddb-8fce-42ac-a6c0-830524d84821/
# 204 No Content
curl http://localhost:8091/expression/https%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fexpression-5e7cfddb-8fce-42ac-a6c0-830524d84821/
# 404 Not Found
{
"error": "Not found",
"message": "Resource not found"
}
Caching#
Not supported. The server with the write routes reaches its store through a proxy that records every request it forwards, and two identical calls send the same SPARQL request twice.
from helper import count_endpoint_requests
count_endpoint_requests(f"http://localhost:8091/expression/{uri}/title", control="http://localhost:7003")
First call: 200 OK, SPARQL requests that reached the endpoint: 1
Second call: 200 OK, SPARQL requests that reached the endpoint: 1
Control over JSON#
Not supported. The keys of the JSON are the names of the slots in the LinkML model, as the first request shows, but the shape is fixed: every value comes wrapped in a {language, value} object, and the attributes of nested shapes are lifted to the top level.