OBA#
OBA derives the API from an OWL ontology, turning each class into a collection and an instance operation. It builds JSON-LD at request time, frames it with the generated context, then strips @context, so consumers see plain JSON. It binds a single endpoint, so it answers from OpenCitations Meta only.
from helper import call
A simple request#
Looking up a DOI returns the article resource from OpenCitations Meta.
call("http://localhost:8087/v1.0.0/articles?label=10.1007/s11192-022-04367-w")
curl 'http://localhost:8087/v1.0.0/articles?label=10.1007/s11192-022-04367-w'
# 200 OK
{
"datacite:hasIdentifier": [
{
"id": "https://w3id.org/oc/meta/id/061202156316",
"type": [
"datacite:Identifier"
]
},
{
"id": "https://w3id.org/oc/meta/id/06640462627",
"type": [
"datacite:Identifier"
]
}
],
"http://prismstandard.org/namespaces/basic/2.0/publicationDate": {
"@value": "2022-06",
"type": "http://www.w3.org/2001/XMLSchema#gYearMonth"
},
"http://purl.org/dc/terms/title": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data",
"http://purl.org/spar/pro/isDocumentContextFor": [
{
"id": "https://w3id.org/oc/meta/ar/061209588726",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/061209588721",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/061209588727",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
... (58 more lines)
The join#
No join. OBA binds a single endpoint, so it cannot reach OpenCitations Index to add the reference count.
Output#
JSON.
Pagination#
call("http://localhost:8087/v1.0.0/journalarticles?page=1&per_page=2", show_headers=True)
curl -i 'http://localhost:8087/v1.0.0/journalarticles?page=1&per_page=2'
# 200 OK
# Content-Type: application/json
[
{
"http://prismstandard.org/namespaces/basic/2.0/publicationDate": {
"@value": "1999-05-01",
"type": "http://www.w3.org/2001/XMLSchema#date"
},
"http://purl.org/dc/terms/title": "Serum CA125 Elevation And Risk Of Clinical Detection Of Cancer In Asymptomatic Postmenopausal Women",
"http://purl.org/spar/datacite/hasIdentifier": {
"id": "https://w3id.org/oc/meta/id/06012",
"type": [
"http://purl.org/spar/datacite/Identifier"
]
},
"http://purl.org/spar/pro/isDocumentContextFor": [
{
"id": "https://w3id.org/oc/meta/ar/06068",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/06069",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/06065",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/06066",
"type": [
"http://purl.org/spar/pro/RoleInTime"
]
},
{
"id": "https://w3id.org/oc/meta/ar/06067",
... (129 more lines)
Versioning#
The version is carried in the base path (/v1.0.0), derived from oba/config.yaml.
API description#
OpenAPI 3.0.
call("http://localhost:8087/v1.0.0/openapi.json")
curl http://localhost:8087/v1.0.0/openapi.json
# 200 OK
{
"components": {
"schemas": {
"JournalArticle": {
"description": "Description not available",
"example": {
"value": {
"id": "some_id"
}
},
"properties": {
"description": {
"description": "small description",
"items": {
"type": "string"
},
"nullable": true,
"type": "array"
},
"id": {
"description": "identifier",
"nullable": false,
"type": "string"
},
"label": {
"description": "short description of the resource",
"items": {
"type": "string"
},
"nullable": true,
"type": "array"
},
"publicationDate": {
"description": "Description not available",
"items": {
"type": "string"
},
"nullable": true,
"type": "array"
},
... (222 more lines)
Consumer authentication#
With write paths enabled, only the write methods carry a Bearer requirement; reads stay open. OBA verifies credentials against Firebase, the only provider.
Endpoint authentication#
HTTP Basic. OBA’s config.yaml has no field for endpoint credentials, but the generated server reads user and password from a config.ini and sends them with every query.
from pathlib import Path
print(Path("oba/config.ini").read_text())
call("http://localhost:8087/v1.0.0/articles?label=10.1007/s11192-022-04367-w", max_lines=12)
[defaults]
user = demo
password = demo
curl 'http://localhost:8087/v1.0.0/articles?label=10.1007/s11192-022-04367-w'
# 200 OK
{
"datacite:hasIdentifier": [
{
"id": "https://w3id.org/oc/meta/id/061202156316",
"type": [
"datacite:Identifier"
]
},
{
"id": "https://w3id.org/oc/meta/id/06640462627",
"type": [
"datacite:Identifier"
... (86 more lines)
Operations#
GET, POST, PUT, and DELETE, each enabled by its own flag in the configuration. A second server, generated from oba/config-write.yaml with all the flags on, is bound to a copy of OpenCitations Meta that accepts updates. The write methods require a bearer token, which is a JWT signed with the secret of the generated server; only the login route that issues it depends on Firebase, so this demo signs one locally. Without a token, a POST is rejected.
import base64
import hashlib
import hmac
import json
import time
from pathlib import Path
def b64(data):
return base64.urlsafe_b64encode(data).rstrip(b"=")
signed = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()) + b"." + b64(json.dumps({"iss": "com.zalando.connexion", "exp": int(time.time()) + 3600, "sub": "demo"}).encode())
TOKEN = (signed + b"." + b64(hmac.new(b"change_this", signed, hashlib.sha256).digest())).decode()
AUTH = {"Authorization": f"Bearer {TOKEN}"}
print(Path("oba/config-write.yaml").read_text())
call("http://localhost:8092/v1.0.0/journalarticles?user=demo", method="POST", data={"title": ["A title"]})
name: oba
output_dir: outputs
openapi:
openapi: 3.0.1
info:
description: OBA demo over OpenCitations Meta
title: OpenCitations OBA demo
version: v1.0.0
servers:
- url: http://localhost:8092/v1.0.0
ontologies:
- ontology.ttl
endpoint:
url: http://meta-write-counted:7001/sparql
prefix: https://w3id.org/oc/meta/br/
graph_base: https://w3id.org/oc/meta/br/
classes:
- http://purl.org/spar/fabio/JournalArticle
enable_get_paths: true
enable_post_paths: true
enable_delete_paths: true
enable_put_paths: true
auth:
enable: true
provider: firebase
firebase:
key: unused
follow_references: false
custom_queries_directory: custom
curl -X POST -H 'Content-type: application/json' --data '{"title": ["A title"]}' 'http://localhost:8092/v1.0.0/journalarticles?user=demo'
# 401 UNAUTHORIZED
{
"detail": "No authorization token provided",
"status": 401,
"title": "Unauthorized",
"type": "about:blank"
}
With the token, a POST creates a resource under a generated identifier. OBA writes it to the named graph of the user, so the triples are read here from the store itself.
import requests
GRAPH = "http://localhost:3031/sparql?query=SELECT%20%3Fs%20%3Fp%20%3Fo%20WHERE%20%7B%20GRAPH%20%3Chttps%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2Fdemo%3E%20%7B%20%3Fs%20%3Fp%20%3Fo%20%7D%20%7D"
CSV = {"Accept": "text/csv"}
created = requests.post("http://localhost:8092/v1.0.0/journalarticles?user=demo", headers=AUTH, json={"title": ["A title"]}, timeout=120)
print(created.status_code, created.json())
ARTICLE = f"http://localhost:8092/v1.0.0/journalarticles/{created.json()['id']}?user=demo"
call(GRAPH, headers=CSV)
201 {'id': 'd07daed4-557a-46ec-a4dc-ee4943402a43', 'title': ['A title'], 'type': ['http://purl.org/spar/fabio/JournalArticle']}
curl -H 'Accept: text/csv' 'http://localhost:3031/sparql?query=SELECT%20%3Fs%20%3Fp%20%3Fo%20WHERE%20%7B%20GRAPH%20%3Chttps%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2Fdemo%3E%20%7B%20%3Fs%20%3Fp%20%3Fo%20%7D%20%7D'
# 200 OK
s,p,o
https://w3id.org/oc/meta/br/d07daed4-557a-46ec-a4dc-ee4943402a43,http://www.w3.org/1999/02/22-rdf-syntax-ns#type,http://purl.org/spar/fabio/JournalArticle
https://w3id.org/oc/meta/br/d07daed4-557a-46ec-a4dc-ee4943402a43,http://purl.org/dc/terms/title,A title
A PUT replaces the resource and a DELETE removes it.
call(ARTICLE, method="PUT", headers=AUTH, data={"title": ["Another title"]})
call(GRAPH, headers=CSV)
call(ARTICLE, method="DELETE", headers=AUTH)
call(GRAPH, headers=CSV)
curl -X PUT -H 'Content-type: application/json' --data '{"title": ["Another title"]}' -H 'Authorization: Bearer eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3MiOiAiY29tLnphbGFuZG8uY29ubmV4aW9uIiwgImV4cCI6IDE3ODk2NjQxMzEsICJzdWIiOiAiZGVtbyJ9.zUg0ny2NNoYX-M42b8pddjiH7eYCZ-eBc7GayuxQ0IU' 'http://localhost:8092/v1.0.0/journalarticles/d07daed4-557a-46ec-a4dc-ee4943402a43?user=demo'
# 201 CREATED
{
"id": "https://w3id.org/oc/meta/br/d07daed4-557a-46ec-a4dc-ee4943402a43",
"title": [
"Another title"
]
}
curl -H 'Accept: text/csv' 'http://localhost:3031/sparql?query=SELECT%20%3Fs%20%3Fp%20%3Fo%20WHERE%20%7B%20GRAPH%20%3Chttps%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2Fdemo%3E%20%7B%20%3Fs%20%3Fp%20%3Fo%20%7D%20%7D'
# 200 OK
s,p,o
https://w3id.org/oc/meta/br/d07daed4-557a-46ec-a4dc-ee4943402a43,http://purl.org/dc/terms/title,Another title
curl -X DELETE -H 'Authorization: Bearer eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3MiOiAiY29tLnphbGFuZG8uY29ubmV4aW9uIiwgImV4cCI6IDE3ODk2NjQxMzEsICJzdWIiOiAiZGVtbyJ9.zUg0ny2NNoYX-M42b8pddjiH7eYCZ-eBc7GayuxQ0IU' 'http://localhost:8092/v1.0.0/journalarticles/d07daed4-557a-46ec-a4dc-ee4943402a43?user=demo'
# 202 ACCEPTED
"Deleted"
curl -H 'Accept: text/csv' 'http://localhost:3031/sparql?query=SELECT%20%3Fs%20%3Fp%20%3Fo%20WHERE%20%7B%20GRAPH%20%3Chttps%3A%2F%2Fw3id.org%2Foc%2Fmeta%2Fbr%2Fdemo%3E%20%7B%20%3Fs%20%3Fp%20%3Fo%20%7D%20%7D'
# 200 OK
s,p,o
Caching#
Not supported. The server with the write paths 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("http://localhost:8092/v1.0.0/journalarticles?page=1&per_page=2", 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. OBA derives the JSON of a resource from the ontology: the keys are the names of the properties and every value is a list, as the first request shows, and the configuration has no field to rename or regroup them.