CRAFTS#

CRAFTS is configured with a JSON file where each property names its endpoint and predicate. A query resolves a DOI to its OMID and title in OpenCitations Meta. A resource call then merges properties from both endpoints, but the join is keyed by the resource IRI: the same IRI must identify the entity in every store, and the client must supply it. You cannot start the join from the DOI, so the DOI is resolved to the OMID first, then the resource is fetched.

from helper import call

A simple request#

Resolve the DOI to its OMID and title in OpenCitations Meta.

call("http://localhost:8085/apis/oc/query?id=articleByDoi&doi=10.1007/s11192-022-04367-w", headers={"Authorization": "Bearer oc-read-token"})
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/query?id=articleByDoi&doi=10.1007/s11192-022-04367-w' 

# 200 OK

{
  "head": {
    "vars": [
      "br",
      "title"
    ]
  },
  "results": {
    "bindings": [
      {
        "br": {
          "type": "uri",
          "value": "https://w3id.org/oc/meta/br/061202127149"
        },
        "title": {
          "type": "literal",
          "value": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
        }
      }
    ]
  },
  "query": "PREFIX n0: <http://purl.org/spar/datacite/>\nPREFIX n1: <http://www.essepuntato.it/2010/06/literalreification/>\nPREFIX n2: <http://purl.org/dc/terms/>\nSELECT ?br ?title WHERE { ?id n0:usesIdentifierScheme n0:doi ; n1:hasLiteralValue \"10.1007/s11192-022-04367-w\" . ?br n0:hasIdentifier ?id ; n2:title ?title . }"
}

The join#

Fetch the resource keyed by that IRI: CRAFTS merges the title from Meta with the references from Index.

call("http://localhost:8085/apis/oc/resource?id=article&iri=https://w3id.org/oc/meta/br/061202127149", headers={"Authorization": "Bearer oc-read-token"})
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/resource?id=article&iri=https://w3id.org/oc/meta/br/061202127149' 

# 200 OK

{
  "iri": "https://w3id.org/oc/meta/br/061202127149",
  "title": {
    "nolang": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
  },
  "references": [
    "https://w3id.org/oc/index/ci/061202127149-062501777134",
    "https://w3id.org/oc/index/ci/061202127149-061302130520",
    "https://w3id.org/oc/index/ci/061202127149-062601255589",
    "https://w3id.org/oc/index/ci/061202127149-061302130471",
    "https://w3id.org/oc/index/ci/061202127149-06903303973",
    "https://w3id.org/oc/index/ci/061202127149-06902330758",
    "https://w3id.org/oc/index/ci/061202127149-06250648394",
    "https://w3id.org/oc/index/ci/061202127149-061403569058",
    "https://w3id.org/oc/index/ci/061202127149-061503593762",
    "https://w3id.org/oc/index/ci/061202127149-061402111914",
    "https://w3id.org/oc/index/ci/061202127149-061202300315",
    "https://w3id.org/oc/index/ci/061202127149-06230495229",
    "https://w3id.org/oc/index/ci/061202127149-061402112592",
    "https://w3id.org/oc/index/ci/061202127149-0630685531",
    "https://w3id.org/oc/index/ci/061202127149-062103559970",
    "https://w3id.org/oc/index/ci/061202127149-060504627",
    "https://w3id.org/oc/index/ci/061202127149-062501777138",
    "https://w3id.org/oc/index/ci/061202127149-061702317089",
    "https://w3id.org/oc/index/ci/061202127149-061303572746",
    "https://w3id.org/oc/index/ci/061202127149-061903578905",
    "https://w3id.org/oc/index/ci/061202127149-061403572753",
    "https://w3id.org/oc/index/ci/061202127149-06180173853",
    "https://w3id.org/oc/index/ci/061202127149-062301987890",
    "https://w3id.org/oc/index/ci/061202127149-06250648343",
    "https://w3id.org/oc/index/ci/061202127149-061302130714",
    "https://w3id.org/oc/index/ci/061202127149-06504020104",
    "https://w3id.org/oc/index/ci/061202127149-062202182112",
    "https://w3id.org/oc/index/ci/061202127149-06903005993",
    "https://w3id.org/oc/index/ci/061202127149-06250648347",
    "https://w3id.org/oc/index/ci/061202127149-061602967302"
  ]
}

Output#

JSON only; CRAFTS does not negotiate other formats.

Pagination#

Not supported.

Versioning#

Not supported.

API description#

A Swagger UI serving an OpenAPI 3.0 spec, at http://localhost:8085/docs/. The spec is pulled from the running container and rendered below.

import json
import re

import requests

from helper import embed_swagger

init_js = requests.get("http://localhost:8085/docs/swagger-ui-init.js", timeout=120).text
spec = json.loads(re.search(r'"swaggerDoc":\s*(\{.*?\}),\s*"customOptions"', init_js, re.S).group(1))
embed_swagger(spec, base_url="http://localhost:8085/")

Consumer authentication#

Every operation requires credentials (the calls above carry a Bearer read token). Without a token the request is rejected with 401.

call("http://localhost:8085/apis/oc/query?id=articleByDoi&doi=10.1007/s11192-022-04367-w")
curl 'http://localhost:8085/apis/oc/query?id=articleByDoi&doi=10.1007/s11192-022-04367-w' 

# 401 Unauthorized

{
  "status": 401,
  "message": "Unauthorized: Authorization header required"
}

Endpoint authentication#

An endpoint can carry authInfo with a user, a password, and the type basic or digest, which CRAFTS uses whenever it queries that endpoint. The titleBasic template reads meta-basic, a copy of Meta behind HTTP Basic.

call("http://localhost:8085/apis/oc/query?id=titleBasic&doi=10.1007/s11192-022-04367-w", headers={"Authorization": "Bearer oc-read-token"})
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/query?id=titleBasic&doi=10.1007/s11192-022-04367-w' 

# 200 OK

{
  "head": {
    "vars": [
      "title"
    ]
  },
  "results": {
    "bindings": [
      {
        "title": {
          "type": "literal",
          "value": "Identifying And Correcting Invalid Citations Due To DOI Errors In Crossref Data"
        }
      }
    ]
  },
  "query": "PREFIX n0: <http://www.essepuntato.it/2010/06/literalreification/>\nPREFIX n1: <http://purl.org/spar/datacite/>\nPREFIX n2: <http://purl.org/dc/terms/>\nSELECT ?title WHERE { ?id n0:hasLiteralValue \"10.1007/s11192-022-04367-w\" . ?br n1:hasIdentifier ?id ; n2:title ?title . }"
}

Operations#

GET, PUT, PATCH, and DELETE on a resource, the write methods with the write token of the API. An endpoint accepts writes when its configuration has a sparqlUpdate entry. CRAFTS sends the update in the query parameter, as Virtuoso accepts, while Fuseki follows the SPARQL protocol and expects update, so in this demo the updates pass through a proxy that renames the parameter. The titles carry a language tag because CRAFTS deletes an untagged literal with a @nolang tag that matches nothing in the store. A PUT creates the resource.

WRITABLE = "http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699"
READ = {"Authorization": "Bearer oc-read-token"}
WRITE = {"Authorization": "Bearer oc-write-token"}

call(WRITABLE, method="PUT", headers=WRITE, data={"iri": "https://w3id.org/oc/meta/br/0699", "title": {"en": "A title"}})
call(WRITABLE, headers=READ)
curl -X PUT -H 'Content-type: application/json' --data '{"iri": "https://w3id.org/oc/meta/br/0699", "title": {"en": "A title"}}' -H 'Authorization: Bearer oc-write-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 
# 201 Created

{
  "location": "/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699",
  "url": "http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699",
  "status": 201,
  "message": "Resource created. #queries: 1. #inserted triples: 1"
}
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 

# 200 OK

{
  "iri": "https://w3id.org/oc/meta/br/0699",
  "title": {
    "en": "A title"
  }
}

A PATCH in JSON Patch notation replaces the title.

call(WRITABLE, method="PATCH", headers=WRITE, data=[{"op": "replace", "path": "/title", "value": {"en": "Another title"}}])
call(WRITABLE, headers=READ)
curl -X PATCH -H 'Content-type: application/json' --data '[{"op": "replace", "path": "/title", "value": {"en": "Another title"}}]' -H 'Authorization: Bearer oc-write-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 
# 200 OK

{
  "status": 200,
  "message": "Resource updated. #queries: 2. #deleted triples: 1. #inserted triples: 1"
}
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 

# 200 OK

{
  "iri": "https://w3id.org/oc/meta/br/0699",
  "title": {
    "en": "Another title"
  }
}

A DELETE removes the data of the resource, and the read token cannot write.

call(WRITABLE, method="DELETE", headers=WRITE)
call(WRITABLE, headers=READ)
call(WRITABLE, method="DELETE", headers=READ)
curl -X DELETE -H 'Authorization: Bearer oc-write-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 
# 200 OK

{
  "status": 200,
  "message": "Resource deleted. #queries: 1. #deleted triples: 1"
}
curl -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 

# 200 OK

{
  "iri": "https://w3id.org/oc/meta/br/0699"
}
curl -X DELETE -H 'Authorization: Bearer oc-read-token' 'http://localhost:8085/apis/oc/resource?id=writableArticle&iri=https://w3id.org/oc/meta/br/0699' 
# 401 Unauthorized

{
  "status": 401,
  "message": "Unauthorized. Your token is not valid"
}

Caching#

CRAFTS keeps the data of each resource in memory for daysCache days. The countedArticle model element reaches OpenCitations Meta through a proxy that records every request it forwards. Once the owner of the API has emptied the cache, the first call reaches the endpoint, while the second one is answered from the cache.

from helper import count_endpoint_requests

call("http://localhost:8085/apis/oc/cleanCache", method="POST", basic_auth=("root", "changeme"))
count_endpoint_requests(
    "http://localhost:8085/apis/oc/resource?id=countedArticle&iri=https://w3id.org/oc/meta/br/061202127149",
    headers={"Authorization": "Bearer oc-read-token"},
)
curl -X POST -u root:changeme http://localhost:8085/apis/oc/cleanCache 

# 200 OK

{
  "status": 200,
  "message": "The cache of API \"oc\" has been cleaned"
}
First call: 200 OK, SPARQL requests that reached the endpoint: 1
Second call: 200 OK, SPARQL requests that reached the endpoint: 0