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.